死磕 java线程系列之线程池深入解析——生命周期
(手机横屏看源码更方便)
注:java源码分析部分如无特殊说明均基于 java8 版本。
注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类。
简介
上一章我们一起重温了下线程的生命周期(六种状态还记得不?),但是你知不知道其实线程池也是有生命周期的呢?!
问题
(1)线程池的状态有哪些?
(2)各种状态下对于任务队列中的任务有何影响?
先上源码
其实,在我们讲线程池体系结构的时候,讲了一些方法,比如shutDown()/shutDownNow(),它们都是与线程池的生命周期相关联的。
我们先来看一下线程池ThreadPoolExecutor中定义的生命周期中的状态及相关方法:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // =29 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // =000 11111... // runState is stored in the high-order bits private static final int RUNNING = -1 << COUNT_BITS; // 111 00000... private static final int SHUTDOWN = 0 << COUNT_BITS; // 000 00000... private static final int STOP = 1 << COUNT_BITS; // 001 00000... private static final int TIDYING = 2 << COUNT_BITS; // 010 00000... private static final int TERMINATED = 3 << COUNT_BITS; // 011 00000... // 线程池的状态 private static int runStateOf(int c) { return c & ~CAPACITY; } // 线程池中工作线程的数量 private static int workerCountOf(int c) { return c & CAPACITY; } // 计算ctl的值,等于运行状态“加上”线程数量 private static int ctlOf(int rs, int wc) { return rs | wc; }
从上面这段代码,我们可以得出:
(1)线程池的状态和工作线程的数量共同保存在控制变量ctl中,类似于AQS中的state变量,不过这里是直接使用的AtomicInteger,这里换成unsafe+volatile也是可以的;
(2)ctl的高三位保存运行状态,低29位保存工作线程的数量,也就是说线程的数量最多只能有(2^29-1)个,也就是上面的CAPACITY;
(3)线程池的状态一共有五种,分别是RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED;
(4)RUNNING,表示可接受新任务,且可执行队列中的任务;
(5)SHUTDOWN,表示不接受新任务,但可执行队列中的任务;
(6)STOP,表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务;
(7)TIDYING,所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法;
(8)TERMINATED,中止状态,已经执行完terminated()钩子方法;
流程图
下面我们再来看看这些状态之间是怎么流转的:
(1)新建线程池时,它的初始状态为RUNNING,这个在上面定义ctl的时候可以看到;
(2)RUNNING->SHUTDOWN,执行shutdown()方法时;
(3)RUNNING->STOP,执行shutdownNow()方法时;
(4)SHUTDOWN->STOP,执行shutdownNow()方法时【本文由公从号“彤哥读源码”原创】;
(5)STOP->TIDYING,执行了shutdown()或者shutdownNow()后,所有任务已中止,且工作线程数量为0时,此时会执行terminated()方法;
(6)TIDYING->TERMINATED,执行完terminated()方法后;
源码分析
你以为贴个状态的源码,画个图就结束了嘛?那肯定不能啊,下面让我们一起来看看源码中是怎么控制的。
(1)RUNNING
RUNNING,比较简单,创建线程池的时候就会初始化ctl,而ctl初始化为RUNNING状态,所以线程池的初始状态就为RUNNING状态。
// 初始状态为RUNNING private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
(2)SHUTDOWN
执行shutdown()方法时把状态修改为SHUTDOWN,这里肯定会成功,因为advanceRunState()方法中是个自旋,不成功不会退出。
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改状态为SHUTDOWN advanceRunState(SHUTDOWN); // 标记空闲线程为中断状态 interruptIdleWorkers(); onShutdown(); } finally { mainLock.unlock(); } tryTerminate(); } private void advanceRunState(int targetState) { for (;;) { int c = ctl.get(); // 如果状态大于SHUTDOWN,或者修改为SHUTDOWN成功了,才会break跳出自旋 if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) break; } }
(3)STOP
执行shutdownNow()方法时,会把线程池状态修改为STOP状态,同时标记所有线程为中断状态。
public List<runnable> shutdownNow() { List<runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改为STOP状态 advanceRunState(STOP); // 标记所有线程为中断状态 interruptWorkers(); tasks = drainQueue(); } finally { // 【本文由公从号“彤哥读源码”原创】 mainLock.unlock(); } tryTerminate(); return tasks; }
至于线程是否响应中断其实是在队列的take()或poll()方法中响应的,最后会到AQS中,它们检测到线程中断了会抛出一个InterruptedException异常,然后getTask()中捕获这个异常,并且在下一次的自旋时退出当前线程并减少工作线程的数量。
private Runnable getTask() { boolean timedOut = false; // Did the last poll() time out? for (;;) { int c = ctl.get(); int rs = runStateOf(c); // 如果状态为STOP了,这里会直接退出循环,且减少工作线程数量 // 退出循环了也就相当于这个线程的生命周期结束了 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); // Are workers subject to culling? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 真正响应中断是在poll()方法或者take()方法中 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { // 这里捕获中断异常 timedOut = false; } } }
这里有一个问题,就是已经通过getTask()取出来且返回的任务怎么办?
实际上它们会正常执行完毕,有兴趣的同学可以自己看看runWorker()这个方法,我们下一节会分析这个方法。
(4)TIDYING
当执行shutdown()或shutdownNow()之后,如果所有任务已中止,且工作线程数量为0,就会进入这个状态。
final void tryTerminate() { for (;;) { int c = ctl.get(); // 下面几种情况不会执行后续代码 // 1. 运行中 // 2. 状态的值比TIDYING还大,也就是TERMINATED // 3. SHUTDOWN状态且任务队列不为空 if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; // 工作线程数量不为0,也不会执行后续代码 if (workerCountOf(c) != 0) { // 尝试中断空闲的线程 interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // CAS修改状态为TIDYING状态 if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { // 更新成功,执行terminated钩子方法 terminated(); } finally { // 强制更新状态为TERMINATED,这里不需要CAS了 ctl.set(ctlOf(TERMINATED, 0)); termination.signalAll(); } return; } } finally { mainLock.unlock(); } // else retry on failed CAS } }
实际更新状态为TIDYING和TERMINATED状态的代码都在tryTerminate()方法中,实际上tryTerminated()方法在很多地方都有调用,比如shutdown()、shutdownNow()、线程退出时,所以说几乎每个线程最后消亡的时候都会调用tryTerminate()方法,但最后只会有一个线程真正执行到修改状态为TIDYING的地方。
修改状态为TIDYING后执行terminated()方法,最后修改状态为TERMINATED,标志着线程池真正消亡了。
(5)TERMINATED
见TIDYING中分析。
彩蛋
本章我们一起从状态定义、流程图、源码分析等多个角度一起学习了线程池的生命周期,你掌握的怎么样呢?
下一章我们将开始学习线程池执行任务的主流程,对这一块内容感到恐惧的同学可以先看看彤哥之前写的“手写线程池”的两篇文章,对接下来学习线程池的主要流程非常有好处。
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
</runnable></runnable>
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
kiftd 1.0.26 发布,青阳网络文件传输系统
kiftd简介: kiftd是一款专门面向个人、团队和小型组织的私有网盘系统。开源、便捷、小巧。无论是在笔记本上、家庭、学校还是办公室,均可以随时随地使用它。它不但是替代U盘进行文件传输的不二之选,同时也是一款具备视频/音乐在线播放、文档预览、图片查看、文件夹访问控制、拖拽上传、移动端访问等多种功能的个人云存储应用。它无任何的使用限制(无论是非商业的还是商业的),即开即用,即使是刚刚学会点击鼠标的小白也能够在3分钟内快速开始。 想要了解更多内容?欢迎访问项目官网:https://kohgylw.gitee.io/index.html 常规更新v1.0.25 *本次更新为维护性的更新,修复一些已经发现的问题并优化使用体验,推荐所有用户升级。 优化了账户配置文件的载入和读取机制 本次修改主要为了避免当多个用户同时进行修改密码或注册新账户等需要更新账户配置文件的操作时,程序可能会发生写入冲突的问题。新的账户配置更新机制中加入了线程同步,从而确保其写入和读取时的安全性。 以上即为本次更新的全部内容,欢迎各位用户下载并更新体验。如果您在使用过程中遇到问题或是希望提出建议,欢迎随时至信kohgyl...
- 下一篇
Gear-Lib 1.1.8 发布,适合 IOT 开发的 C 库
Gear Lib 是一组通用的 C 基础库(由原libraries更名),适用于 IOT 开发,支持嵌入式,以及网络服务开发等场景 新版本 1.1.8 更新日志如下: 新增 homekit 协议 Gear Lib 功能列表: 数据结构 libdict: key-value 库 libhash: linux 内核原生哈希库 libringbuffer: 循环缓冲 libqueue: 数据队列 librbtree: linux 内核 rbtree libsort: libvector: 容器库 libmacro: 通用宏定义 网络库 librtsp: RTSP 协议,适合 IPCamera 和 NVR 开发 librtmp: RTMP 协议,适合推流直播 libskt: Socket 封装 librpc: 远程过程调用库 libipc: 进程间通信 libp2p: p2p 穿透传输 libhomekit: 苹果 homekit 协议库 异步 libgevent: 事件驱动 libthread: 线程 libworkq: 工作队列 I/O libbase64: Base64/32 编解码 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- 设置Eclipse缩进为4个空格,增强代码规范
- Mario游戏-低调大师作品
- MySQL8.0.19开启GTID主从同步CentOS8
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16