一、序
今天就借这个问题来说说线程池中维护的线程。它的增长和回收策略是什么?
00-101010
二、线程池的策略
当我们谈到线程池中线程的增长策略时,最引人注目的是它的核心线程大小和最大线程大小,但只看这两个参数是不够全面的,线程数的增长也与任务等待队列有关。
我们先来看看ThreadPoolExecutor最完整参数的构造方法:
简要解释每个参数的含义:
CorePoolSize:核心线程的数量;MaximumPoolSize:线程池中的最大线程数;KeepAliveTime:核心线程以外的线程的最大空闲生存时间;单位:时间单位:keepAliveTimeWorkQueue:线程池的任务等待队列;ThreadFractionary:为线程池创建线程的ThreadFractionary处理程序:拒绝策略,线程池无法处理任务时的拒绝方法;许多这些参数的配置是相互影响的。例如,如果任务等待队列工作队列配置不当,线程池中的线程可能永远不会增长到最大线程数配置的线程数。
00-1010您应该从这里清楚地知道,线程池中线程的增长策略与三个参数有关:
CorePoolSize:核心线程数maximumPoolSize:最大线程数;工作队列:等待任务队列;他们以前的关系如下:
接下来,让我们理想地看看线程池中线程的增长策略。
默认情况下,线程池一开始是空的。当新任务到来时,线程池开始通过线程池创建线程来处理任务。
新任务将持续触发线程池中线程的创建,直到线程数量达到核心线程大小。然后,线程的创建将被停止,新任务将被放入任务等待工作队列。
新任务不断进入任务等待队列。当队列已满时,开始重新创建线程处理任务,直到线程池中的线程数达到maximumPoolSize配置的数量。
此时线程池中的线程数达到最大,没有空闲线程,任务队列中满是任务。此时,如果有新的任务进来,就会触发线程池的拒绝策略(处理程序),如果没有配置拒绝策略,就会抛出RejectedExecutionException异常。
至此,线程增长策略清晰了,我们可以通过下图了解完整的过程。
关键的一个是任务的等待队列。无论等待队列的实现结构是什么,线程池中的线程数量只有在满的时候才会增加到最大。但是队列可以满的前提是它必须是有界队列。
这是文章开头给出的例子中的隐藏漏洞。让我们回顾一下前面构建的线程池。
如你所见,这个
里虽然最大线程数是大于核心线程数的,但是它的等待队列配置的是一个 LinkedBlockingQueue,从名字上可以看出这是一个基于链表实现的阻塞队列,而用它的默认构造方法构造时,其容量设定为 Integer.MAX_VALUE,可以简单理解它是一个无界队列。
这也就是为什么说,这样构造的线程池,核心线程数的配置参数,永远都用不到,因为它的等待队列永远没有满的时候。
2.3 线程池中线程的收缩策略
线程池中执行的任务,总有执行结束的时候。那么线程池当线程池中存在大量空闲线程时,也会有一定的收缩策略,来回收线程池中多余的线程。
线程池中线程的收缩策略,和以下几个参数相关:
corePoolSize:核心线程数;maximumPoolSize:线程池的最大线程数;keepAliveTime:核心线程数之外的线程,空闲存活的时长;unit:keepAliveTime 的时间单位;
corePoolSize 和 maximumPoolSize 我们比较熟悉了,另外能够控制它的就是 keepAliveTime 空闲存活时长,以及这个时长的单位。
当线程池中的线程数,超过核心线程数时。此时如果任务量下降,肯定会出现有一些线程处于无任务执行的空闲状态。那么如果这个线程的空闲时间超过了 keepAliveTime&unit 配置的时长后,就会被回收。
需要注意的是,对于线程池来说,它只负责管理线程,对于创建的线程是不区分所谓的「核心线程」和「非核心线程」的,它只对线程池中的线程总数进行管理,当回收的线程数达到 corePoolSize 时,回收的过程就会停止。
对于线程池的核心线程数中的线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit 配置的时间,也将其回收掉。
allowCoreThreadTimeOut() 能被设置的前提是 keepAliveTime 不能为 0。
2.3 查缺补漏
1. 等待队列还会影响拒绝策略
等待队列如果配置成了无界队列,不光影响线程数量从核心线程数向最大线程数的增长,还会导致配置的拒绝策略永远得不到执行。
因为只有在线程池中的工作线程数量已经达到核心线程数,并且此时等待队列也满了的情况下,拒绝策略才能生效。
2. 核心线程数可以被「预热」
前面提到默认的情况下,线程池中的线程是根据任务来增长的。但如果有需要,我们也可以提前准备好线程池的核心线程,来应对突然的高并发任务,例如在抢购系统中就经常有这样的需要。
此时就可以利用 prestartCoreThread() 或者 prestartAllCoreThreads() 来提前创建核心线程,这种方式被我们称为「预热」。
3. 对于需要无界队列的场景,怎么办?
需求是多变的,我们肯定会碰到需要使用无界队列的场景,那么这种场景下配置的 maximumPoolSize 就是无效的。
此时就可以参考 Executors 中 newFixedThreadPool() 创建线程池的过程,将 corePoolSize 和 maximumPoolSize 保持一致即可。
此时核心线程数就是最大线程数,只有增长到这个数量才会将任务放入等待队列,来保证我们配置的线程数量都得到了使用。
4. 线程池是公平的吗?
所谓的公平,就是先到的任务会被先执行。这在线程池中,显然是不公平的。
不提线程池中线程执行任务是通过系统去调度的,这一点就决定了任务的执行顺序是无法保证的,这就是是非公平的。另外只从线程池本身的角度来看,我们只看提交的任务顺序来看,它也是非公平的。
首先前面到的任务,如果线程池的核心线程已经分配出去了,此时这个任务就会进入任务队列,那么如果任务队列满了之后,新到的任务会直接由线程池新创建线程去处理,直到线程数达到最大线程数。
那么此时,任务队列中的任务,虽然先添加进线程池等待处理,但是这些任务的处理时机,是晚于后续新创建线程去处理的任务的,所以说仅从任务的角度,依然是非公平的。
三、小结时刻
本文我们聊到了线程池中,对于线程数量的增长和收缩策略。
在这里我们简单总结一下:
1. 增长策略。默认情况下,线程池是根据任务先创建足够核心线程数的线程去执行任务,当核心线程满了时将任务放入等待队列。待队列满了的时候,继续创建新线程执行任务直到到达最大线程数停止。再有新任务的话,那就只能执行拒绝策略或是抛出异常。
2. 收缩策略。当线程池线程数量大于核心线程数 && 当前有空闲线程 && 空闲线程的空闲时间大于 keepAliveTime 时,会对该空闲线程进行回收,直到线程数量等于核心线程数为止。
总之要谨记,慎用无界队列。
最后,本文对你有帮助吗?留言、转发、点赞是最大的支持,谢谢!
在头条号私信我。我会送你一些我整理的学习资料,包含:Android反编译、算法、设计模式、虚拟机、Linux、Kotlin、Python、爬虫、Web项目源码。