一、线程池的自我介绍
一个线程
package threadpool; public class EveryTaskOneThread { public static void main(String[] args) { Thread thread = new Thread(new Task()); thread.start(); } static class Task implements Runnable { @Override public void run() { System.out.println("执行了任务"); } } }
for循环创建多个线程
package threadpool; public class ForLoop { public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Task()); thread.start(); } } static class Task implements Runnable { @Override public void run() { System.out.println("执行了任务"); } } }
如果不使用线程池,每个任务都新开一个线程处理,当任务数量上升到1000,这样开销太大,我们希望有固定数量的线程来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题。
1、为什么要使用线程池
问题一:反复创建线程开销大
问题二:过多的线程会占用太多内存
解决以上两个问题的思路
用少量的线程——避免内存占用过多
让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗
2、线程池的好处
加快响应速度
合理利用CPU和内存
统一管理
3、线程池适合应用的场合
服务器接收到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理
二、创建和停止线程池
1、线程池构造方法的参数如下:
corePoolSize指的是核心线程数
线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务来时,再创建新线程去执行任务
maxPoolSize最大量
在核心线程的基础上,额外增加的线程数上线
keepAliveTime
如果线程池当前的线程数多余corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,它们就会被终止。
ThreadFactory用来创建线程
默认使用Executors.defaultThreadFactory()
创建出来的线程都在同一个线程组
如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等
workQueue工作队列
有三种常见的队列类型:
直接交接:SynchronousQueue
无界队列:LinkedBlockingQueue
有界队列:ArrayBlockingQueue
2、添加线程的规则
1、如果线程数小于corePoolSize,创建一个新线程来运行新任务
2、如果线程数等于(或大于)corePoolSize但小于maximumPoolSize,则将任务放入队列
3、如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程
4、如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝
是否需要增加线程的判断顺序是:
corePoolSize
workQueue
maxPoolSize
举个例子:
线程池:核心池大小为5,最大池大小为10,队列为100
因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建最新的线程maxPoolSize,最多到10个线程,如果再来任务就拒绝。
增减线程的特点:
1、通过设置corePoolSize和maxmumPoolSize相同,就可以创建固定大小的线程池。
2、线程池希望保持较小的线程数,并且只有在负载变得很大时才增加它。
3、通过设置maximumPoolSize为很高的值,可以允许线程池容纳任意数量的并发任务。
4、只有在队列填满时才创建多于corePoolSize的线程,如果使用的是无界队列,那么线程数就不会超过corePoolSize
3、线程池应该手动创建还是自动创建
手动创建更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险
自动创建线程池(即直接调用JDK封装好的构造方法)可能带来哪些问题?
newFixedThreadPool:容易造成大量内存占用,可能会导致OOM
newSingleThreadExecutor:当请求堆积的时候,可能会占用大量的内存
newCachedThreadPool:可缓存线程池
特点:具有自动回收多余线程的功能
弊端:第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM
newScheduledThreadPool:支持定时及周期性任务执行的线程池
以上4种线程池的构造方法的参数比较如下图:
正确的创建线程池的方法:
根据不同的业务场景,设置线程池参数
比如:内存有多大,给线程取什么名字等等
4、线程池里的线程数量设定为多少比较合适
CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。
耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍。
参考Brain Goetz推荐的计算方法:
线程数 = CPU核心数*(1+平均等待时间/平均工作时间)
5、停止线程池的正确方法
1、shutdown
会将正在执行和队列中的任务执行完成之后才关闭,新的任务不会继续加入。
isShutdown方法可以判断线程是否进入停止状态。
isTerminated方法可以判断线程是否已完全停止并且已完成正在执行和队列的任务。
2、awaitTermination
等待一段时间线程是否执行结束。
3、shutdownNow
立即停止正在执行的线程并发出中断信号,队列中还未执行的任务则会返回一个列表。
三、任务太多,怎么拒绝
1、拒绝时机:
1、当Executor关闭时,提交新任务会被拒绝。
2、当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时提交新任务会被拒绝。
2、4种拒绝策略:
1、AbortPolicy 直接抛出异常告诉你任务没有提交成功。
2、DiscardPolicy 默默丢弃掉任务,不会得到通知。
3、DiscardOldestPolicy 丢弃最老的,存在时间最长的任务。
4、CallerRunsPolicy 让提交任务的线程执行该任务。
四、钩子方法,给线程池加点料
每个任务执行前后
日志、统计
五、线程池实现原理
1、线程池组成部分
线程池管理器
工作线程
任务队列
任务接口(Task)
2、线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池相关的类,都是什么关系?
3、线程池实现任务复用的原理
相同线程执行不同任务
4、线程池的状态
六、使用线程池的注意点
避免任务堆积
避免线程数过度增加
排查线程泄露