漫画:聊聊线程池中,线程的增长/回收策略
一、序
public static ExecutorService newThreadPool() { return new ThreadPoolExecutor( 30, 60, 60L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue
我们今天就来借这个问题,聊聊线程池中维护的线程,它增长和回收的策略是什么样的?
二、线程池的策略
2.1 线程池的各项参数
当我们聊到线程池中线程的增长策略的时候,最抓眼球的就是它的核心线程数(corePoolSize)和最大线程数(maximumPoolSize),但是仅看这两个参数是不够全面的,线程数量的增长,还与任务等待队列有关系。
我们先来看看 ThreadPoolExecutor 最全参数的构造方法:
- public ThreadPoolExecutor(
- int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- // ...
- }
简单解释一下各个参数是什么意思:
- corePoolSize:核心线程数;
- maximumPoolSize:线程池的最大线程数;
- keepAliveTime:核心线程数之外的线程,最大空闲存活的时长;
- unit:keepAliveTime 的时间单位;
- workQueue:线程池的任务等待队列;
- threadFractory:线程工厂,用来为线程池创建线程;
- handler:拒绝策略,当线程池无法处理任务时的拒绝方式;
这其中很多参数的配置,都是相互影响的。例如任务等待队列 workQueue 配置不当,可能导致线程池中的线程,永远无法增长到核心线程数(maximumPoolSize)配置的线程数。
2.2 线程池中线程的增长策略
看到这里你应该就清楚了,线程池线程的增长策略,和 3 个参数有关系:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数;
- workQueue:等待任务队列;
它们之前的关系是这样的:
接下来我们看看理想情况下,线程池中线程的增长策略。
默认情况下,初始时线程池是空的,当有新任务来了时,线程池开始通过线程工厂(threadFractory)创建线程来处理任务。
新的任务会不断的触发线程池中线程的创建,直到线程数量达到核心线程数(corePoolSize),接下来会停止线程的创建,而是将这个新任务放入任务等待队列(workQueue)。
新任务不断进入任务等待队列,当该队列满了时,开始重新创建线程处理任务,直到线程池中线程的数量,到达 maximumPoolSize 配置的数量。
到这一步时,线程池的线程数达到最大值,并且没有空闲的线程,任务队列也存满了任务,这时如果还有新的任务进来,就会触发线程池的拒绝策略(handler),如果没有配置拒绝策略就会抛出 RejectedExecutionException 异常。
到这里线程的增长策略就说清楚了,我们可以通过下图来了解完整的流程。
其中比较关键的就是任务的等待队列,无论等待队列的实现结构是什么样的,只有在它满的时候,线程池中的线程才会向最大线程数增长。但是一个能够满的队列,它的前提是必须是一个有界队列。
这就是文章开头举的例子暗藏的坑,我们回顾一下前面构造的线程池。
- public static ExecutorService newThreadPool() {
- return new ThreadPoolExecutor(
- 30, 60,
- 60L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
可以看到,这里虽然最大线程数是大于核心线程数的,但是它的等待队列配置的是一个 LinkedBlockingQueue,从名字上可以看出这是一个基于链表实现的阻塞队列,而用它的默认构造方法构造时,其容量设定为 Integer.MAX_VALUE,可以简单理解它是一个无界队列。
- public LinkedBlockingQueue() {
- this(Integer.MAX_VALUE);
- }
- public LinkedBlockingQueue(int capacity) {
- if (capacity <= 0) throw new IllegalArgumentException();
- this.capacity = capacity;
- last = head = new Node<E>(null);
- }
这也就是为什么说,这样构造的线程池,核心线程数的配置参数,永远都用不到,因为它的等待队列永远没有满的时候。
2.3 线程池中线程的收缩策略
线程池中执行的任务,总有执行结束的时候。那么线程池当线程池中存在大量空闲线程时,也会有一定的收缩策略,来回收线程池中多余的线程。
线程池中线程的收缩策略,和以下几个参数相关:
- corePoolSize:核心线程数;
- maximumPoolSize:线程池的最大线程数;
- keepAliveTime:核心线程数之外的线程,空闲存活的时长;
- unit:keepAliveTime 的时间单位;
corePoolSize 和 maximumPoolSize 我们比较熟悉了,另外能够控制它的就是 keepAliveTime 空闲存活时长,以及这个时长的单位。
当线程池中的线程数,超过核心线程数时。此时如果任务量下降,肯定会出现有一些线程处于无任务执行的空闲状态。那么如果这个线程的空闲时间超过了 keepAliveTime&unit 配置的时长后,就会被回收。
需要注意的是,对于线程池来说,它只负责管理线程,对于创建的线程是不区分所谓的「核心线程」和「非核心线程」的,它只对线程池中的线程总数进行管理,当回收的线程数达到 corePoolSize 时,回收的过程就会停止。
对于线程池的核心线程数中的线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime&unit 配置的时间,也将其回收掉。
- public void allowCoreThreadTimeOut(boolean value) {
- if (value && keepAliveTime <= 0)
- throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
- if (value != allowCoreThreadTimeOut) {
- allowCoreThreadTimeOut = value;
- if (value)
- interruptIdleWorkers();
- }
- }
allowCoreThreadTimeOut() 能被设置的前提是 keepAliveTime 不能为 0。
2.3 查缺补漏
1. 等待队列还会影响拒绝策略
等待队列如果配置成了无界队列,不光影响线程数量从核心线程数向最大线程数的增长,还会导致配置的拒绝策略永远得不到执行。
因为只有在线程池中的工作线程数量已经达到核心线程数,并且此时等待队列也满了的情况下,拒绝策略才能生效。
2. 核心线程数可以被「预热」
前面提到默认的情况下,线程池中的线程是根据任务来增长的。但如果有需要,我们也可以提前准备好线程池的核心线程,来应对突然的高并发任务,例如在抢购系统中就经常有这样的需要。
此时就可以利用 prestartCoreThread() 或者 prestartAllCoreThreads() 来提前创建核心线程,这种方式被我们称为「预热」。
3. 对于需要无界队列的场景,怎么办?
需求是多变的,我们肯定会碰到需要使用无界队列的场景,那么这种场景下配置的 maximumPoolSize 就是无效的。
此时就可以参考 Executors 中 newFixedThreadPool() 创建线程池的过程,将 corePoolSize 和 maximumPoolSize 保持一致即可。
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(
- nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
此时核心线程数就是最大线程数,只有增长到这个数量才会将任务放入等待队列,来保证我们配置的线程数量都得到了使用。
4. 线程池是公平的吗?
所谓的公平,就是先到的任务会被先执行。这在线程池中,显然是不公平的。
不提线程池中线程执行任务是通过系统去调度的,这一点就决定了任务的执行顺序是无法保证的,这就是是非公平的。另外只从线程池本身的角度来看,我们只看提交的任务顺序来看,它也是非公平的。
首先前面到的任务,如果线程池的核心线程已经分配出去了,此时这个任务就会进入任务队列,那么如果任务队列满了之后,新到的任务会直接由线程池新创建线程去处理,直到线程数达到最大线程数。
那么此时,任务队列中的任务,虽然先添加进线程池等待处理,但是这些任务的处理时机,是晚于后续新创建线程去处理的任务的,所以说仅从任务的角度,依然是非公平的。
三、小结时刻
本文我们聊到了线程池中,对于线程数量的增长和收缩策略。
在这里我们简单总结一下:
1. 增长策略。默认情况下,线程池是根据任务先创建足够核心线程数的线程去执行任务,当核心线程满了时将任务放入等待队列。待队列满了的时候,继续创建新线程执行任务直到到达最大线程数停止。再有新任务的话,那就只能执行拒绝策略或是抛出异常。
2. 收缩策略。当线程池线程数量大于核心线程数 && 当前有空闲线程 && 空闲线程的空闲时间大于 keepAliveTime 时,会对该空闲线程进行回收,直到线程数量等于核心线程数为止。
总之要谨记,慎用无界队列。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
裁员3万,美国运营商经历了什么?
根据公开数据,美国运营商AT&T和Verizon的裁员规模在2019年达到了七年来的最高水平,去年一年,这两家巨头公司共裁员近30000人,占2018年员工总数的7%以上。 运营商发布的季度数据显示,AT&T裁员20420人,占员工总数的近8%,Verizon裁员9500人,占2018年底总员工总数的7%左右。 几周前,两家运营商都分别发布了2019年的收益报告,相关裁员信息也随之浮出水面,数据显示,与欧洲运营商相比,美国运营商正在推行更为激进的裁员计划。近日,欧洲运营商也公布了自己的裁员数据。 德国电信(Deutsche Telekom)上周透露,该公司去年裁员约5000人,约占其2018年底员工总数的2%。西班牙Telefónica的裁员4500人,低于其2018年总数的4%。 随着自动化和新数字技术的不断发展,企业加速了裁员计划。英国沃达丰最近裁员数百名,并将此归因于网络运营中心(NOC)的自动化,并表示未来会进一步裁员。沃达丰首席技术官斯科特·佩蒂(Scott Petty)表示:“从现在起的五到七年内,运营NOC基础设施的人将越来越少。我们运行网络的方式正在发生...
- 下一篇
同期下降15%,新型冠状肺炎影响一季度服务器/存储销售
【51CTO.com原创稿件】受到新型冠状肺炎的影响,国内的服务器生产企业都采用了各种管理防控措施,这导致很多工厂生产线还没有开工,很多服务器与存储企业的中国工厂都处于关闭状态。受此影响,2020年第一季度中国IT硬件市场将暂时遭受较大的影响。虽然这一影响还不确定是否会冲击国外市场,但确实有可能扩散到全球的其他市场。 一季度服务器/存储市场销售额持续下滑 根据IDC预测,2020年服务器总增长率将从最初的12.4%降低到7.4%,第一季度销售额将比去年同期下降15%,而不是原来的16.5%的预测。 存储2020年的增长率将从12.5%降低至7.3%,第一季度将下降20%,而不是原来预计的2019年第一季度增长16.6%。 2020年网络增长率从原来的6.2%降低至3.0%,降幅也十分明显。 根据IDC企业研究小组高级副总裁Matt Eastwood的说法,中国目前占全球IT基础设施市场份额的20%左右 ,虽然目前仍然没有对全球市场进行汇总,但中国市场的影响并不是微不足道的。 IDC认为,新型冠状肺炎带来的影响将仅限于今年第一季度,并在第二季度逐渐消失。相比较服务器、存储和网络等企业级I...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Mario游戏-低调大师作品
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8