1、Java线程工厂Executors提供的四种线程池
newCachedThreadPool-缓存线程池,okhttp里使用的是这个
newFixedThreadPool-固定线程数的线程池
newSingleThreadExecutor-单线程线程池
newScheduledThreadPool-实现定时周期性任务的线程池
这四种线程池都直接或间接地获取了ThreadPoolExecutor实例,如果这四种不合适,我们还可以自己创建。ThreadPoolExecutor的构造方法如下,本文解析的重点目标就是搞清楚这个构造方法中的七个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码
2、线程池的七个参数
1、corePoolSize:核心线程数
线程池总会维护一个最小的线程数量,即使这些线程处于空闲状态也不会被销毁。除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
这里你可能会问,那如果我创建了一个核心线程为3的线程池,线程池一创建就会有3个线程吗?
并不是的,默认情况下并不会创建出来核心线程,是在任务来到时,才创建的。并且在线程数少于核心线程数时,任务到来,无论当前的线程是否处于空闲状态,都会创建线程。
接着又可能会疑问,创建线程相对而言耗时还是比较大,如果我想在线程池创建时就把核心线程创建好呢?我就是傲娇,我就是要这么干呢?
办法还是有的,重写prestartCoreThread方法实现预启动核心线程或者调用prestartAllCoreThreads方法实现
2、maximumPoolSize:线程池最大线程数量
一个任务被提交到线程池后,首先看有没有空闲线程,如果有直接执行任务。如果没有,会缓存到工作队列,如果工作队列满了,则会创建新线程,然后从工作队列中取出一个任务由新线程来处理,而刚提交的任务放入工作队列。线程池不会无限制地创建新线程,它会有一个最大线程数量的限制,这个数量由maximumPoolSize来指定
3、keepAliveTime:空闲线程存活时间
一个线程如果处于空闲状态,并且当前线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁。这里的指定时间由keepAliveTime来指定
这里你可能会问, 我想对核心线程也来个存活时间呢?
可以通过设置allowCoreThreadTimeOut来实现。但是注意,只在keepAliveTime不等于0时才会生效。也就是说非核心线程可以用完即释放,但是核心线程不行
这里对核心线程和非核心线程的待遇就不同了。核心线程好比公司正牌员工,非核心线程是活多的时候招过来的临时工,活干完了,就把正牌员工解雇(卸磨杀驴不合适吧),临时工就不一样了,原本就是临时的~
4、unit:空闲线程存活时间单位
5、workQueue:工作队列-java里提供了四种阻塞队列
(1)、ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。当线程池中的线程数量达到corePoolSize后,再有新任务进来,会将任务放到队尾,等待调度。如果队列已经是满的,则会创建新线程。如果线程数达到了maximumPoolSize,则会执行拒绝策略
(2)、LinkedBlockingQueue
基于链表的无界阻塞队列(默认情况下)。按FIFO排序。该队列近似无界。当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列。而不会去创建新线程。因此这种情况下的maximumPoolSize实际上是不起作用的
(3)、SynchronousQueue
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说,线程数量达到corePoolSize后,新任务到来,会创建新线程。如果线程数量达到maxPoolSize,则会执行拒绝策略
(4)、PriorityBlockQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现
都是阻塞队列,那阻塞队列和普通队列有什么区别呢?
阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
2个附加操作:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。
6、threadFactory线程工厂
创建一个新线程时使用的工厂,可以用来指定线程名,是否为daemon线程等
7、handle拒绝策略
当工作队列中的任务达到最大限制,并且线程池中的线程数量也达到最大限制,新任务到来,会执行拒绝策略。java中提供了4中拒绝策略
(1)、CallerRunsPolicy
在该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shotdown,则直接抛弃任务
(2)、AbortPolicy
该策略下,直接丢弃任务,并抛出
RejectedExecutionException异常
(3)、DiscardPolicy
在该策略下,直接丢弃任务,什么都不做。
(4)、DiscardOldestPolicy
该策略下,抛弃进入队列最早的任务,然后尝试把这次拒绝的任务加入队列
3、小结一下
1、线程池运行整体流程
在不做特殊设置的情况下,比如上文提到的几类设置(预先创建核心线程、给核心线程设置存活时间等)
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
1.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
2.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
3.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
4.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
5.当一个线程完成任务时,它会从队列中取下一个任务来执行。
6.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
2、案例:先加入任务就一定会先执行吗?
假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4-13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17-20 则会执行拒绝策略。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。
3、聊聊排队这件事
1、不排队,一个不错的默认选择
使用SynchronousQueue+无限大的maximumPoolSize。上文提到SynchronousQueue是一个不缓存的队列,每来一个任务如果没达到maximumPoolSize,就会有线程去执行它(创建或复用),无需排队。举个案例:newCachedThreadPool的实现
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
复制代码
newCachedThreadPool(缓存线程池),使用了SynchronousQueue+无限大的maximumPoolSize组合(Integer.MAX_VALUE可近似看成无限大了)。
同时核心线程还为0,让所有线程都遵循了60秒的空闲线程存活时间规则。
对于执行数量多的短期异步任务,有提高性能的作用,当任务到来时,如果没有可用的空闲线程,就创建线程添加到池子里。而在足够长的闲置时间后,将全部释放不消耗资源。
2、排长龙。
使用LinkedBlockingQueue+有限线程数(最大线程数设置和核心线程一样)。当任务到来,如果没有空闲可用的线程了,就排到队列里,无限大的队列防止任务丢失。实际上newFixedTheradPool和newSingleThreadExecutor是这么实现的
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
复制代码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
复制代码
3、控制排队,多租点地盘-高端操作
使用有界队列ArrayBlockingQueue+控制maximumPoolSize的大小。如果是任务多,耗时短,就采用大队列+小的线程数,降低cpu使用率、操作系统资源和上下文切换消耗。当出现任务耗时时间长,经常阻塞,那就用更大的池来避免阻塞。
原文链接:
https://juejin.cn/post/6907833986316238856
如果觉得本文对你有帮助,可以转发关注支持一下