(手机横屏看源码更方便)


注: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中分析。

彩蛋

本章我们一起从状态定义、流程图、源码分析等多个角度一起学习了线程池的生命周期,你掌握的怎么样呢?

下一章我们将开始学习线程池执行任务的主流程,对这一块内容感到恐惧的同学可以先看看彤哥之前写的“手写线程池”的两篇文章,对接下来学习线程池的主要流程非常有好处。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java线程系列之线程池深入解析——生命周期的更多相关文章

  1. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  2. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  3. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

  4. 死磕 java同步系列之Semaphore源码解析

    问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...

  5. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  6. 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

    问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...

  7. 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

    问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...

  8. 死磕 java同步系列之CountDownLatch源码解析

  9. 死磕 java同步系列之zookeeper分布式锁

    问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...

随机推荐

  1. Spring boot 自定义banner的在线制作

    目前工作不是很忙,利用闲暇的时间,在给自己不断地充电,提升自己的技术实力. 目前在做一个基于Spring Boot2.x+webmagic+quartz的爬虫项目[hotDog]https://git ...

  2. Fliptile POJ-3279 DFS

    题目链接:Fliptile 题目大意 有一个01矩阵,每一次翻转(0->1或者1->0)一个元素,就会把与他相邻的四个元素也一起翻转.求翻转哪些元素能用最少的步骤,把矩阵变成0矩阵. 思路 ...

  3. VM虚拟机,如何放大虚拟机屏幕,如何导出虚拟机ovf

    放大屏幕:,第一打开虚拟机,第二在需要放大的虚拟机上安装VMware tools   第三步查看>自动调节大小>自适应客户机.这样就可以放大屏幕了. 没有放大的屏幕 找到安装VMware  ...

  4. 读《深入理解Elasticsearch》点滴-查询二次评分

    理解二次评分 二次评分是指重新计算查询返回文档中指定个数文档的得分,es会截取查询返回的前N个,并使用预定义的二次评分方法来重新计算他们的得分 小结 有时候,我们需要显示查询结果,并且使得页面上靠前文 ...

  5. 使用Espresso测试记录

    准备工作 建立测试项目 添加测试依赖 编写Espresso测试 运行测试并检查测试结果 建立测试项目 使用Android Studio建立测试项目,Activity模版使用 LoginActivity ...

  6. Android类似日历的翻转控件

    最近写了个翻转面板的控件拿出来与大家分享一下,类似日历的那种,写的比较简单有需要的可以直接拿去用.直接上效果图吧,代码我放在百度云了,有问题的话直接回复就好呢,大家一起交流下. http://pan. ...

  7. springboot启动后自动退出

    有时新建的springboot启动后自动退出运行,如图所示: 此种情况大都数是因为pom文件加入了tomcat的依赖,与springboot内嵌的tomcat冲突导致,所以只需将pom文件中的tomc ...

  8. ps 将图片四角变成圆角

    1.用PS打开一张图片,用矩形选框工具,选出你要保留的的那一部分,“选择→修改→平滑”.在弹出的选框里添入数值,值越大角就越圆. 2.选择“选择→反选”,再按delete删除就ok了.

  9. mysql初识笔记

    一.初始mysql mysql介绍: mysql版本: 版本号=3个数字+1个后缀 mysql-5.0.9-beta 5 0 9 Beta 主版本号 发行级别 发行稳定级别 发行系列 发行系列的版本号 ...

  10. passwd、shadow、group文件格式

    [root@bogon ~]# cat /etc/passwd root:x:0:0:root:/root:/bin/bash 登录名:密码占位符:UID:GID:注释:家目录:用户的默认shell ...