开篇-为什么要使用线程池?
Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3 个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
1. 线程池的任务执行步骤
当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?处理流程图如图 1-1所示。
从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。
1.线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2.线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3.线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor
执行 execute()
方法的示意图,如图 1-2 所示。
-
如果当前运行的线程少于
corePoolSize
,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。 -
如果运行的线程等于或多于
corePoolSize
,则将任务加入BlockingQueue
。 -
如果无法将任务加入
BlockingQueue
(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。 -
如果创建新线程将使当前运行的线程超出
maximumPoolSize
,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()
方法。ThreadPoolExecutor
采取上述步骤的总体设计思路,是为了在执行 execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor
完成预热之后(当前运行的线程数大于等于corePoolSize
),几乎所有的 execute()方法调用都是执行步骤 2,而步骤 2 不需要获取全局锁。
2. 线程池的使用
2.1 线程池的创建
通过ThreadPoolExecutor
可以创建一个线程池。
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建一个线程池时需要输入几个参数,如下。
-
corePoolSize
(线程池的基本大小,也可以称之为核心线程数大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()
方法,线程池会提前创建并启动所有基本线程。 -
maximumPoolSize
(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列,这个参数就没有什么效果了。 -
keepAliveTime
(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。默认情况下该参数针对的是非核心线程,如果将参数allowCoreThreadTimeOut
设置为true,那么核心线程也会受这个参数影响。 -
TimeUnit
(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。 -
workQueue
(任务队列):用于保存等待执行的任务的阻塞队列。具体可以参考Java阻塞队列。 -
threadFactory
:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 -
RejectedExecutionHandler
(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在 JDK 1.5 中 Java 线程池框架提供了以下 4 种策略。•
AbortPolicy
:直接抛出异常。•
CallerRunsPolicy
:只用调用者所在线程来运行任务。•
DiscardOldestPolicy
:丢弃队列里最近的一个任务,并执行当前任务。•
DiscardPolicy
:不处理,丢弃掉。当然,也可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。如记录日志或持久化存储不能处理的任务。
2.2 向线程池中提交任务
可以使用两个方法向线程池提交任务,分别为 execute()和 submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知 execute()方法输入的任务是一个 Runnable 类的实例。
executorService.execute(()-> Thread.currentThread().getName());
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Future<String> submit = executorService.submit(() -> Thread.currentThread().getName());
System.out.println(submit.get());
2.3 关闭线程池
可以通过调用线程池的 shutdown
或 shutdownNow
方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt
方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow
首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown
只是将线程池的状态设置成 SHUTDOWN
状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown
方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed
方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow
方法。
2.4 合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
• 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。
• 任务的优先级:高、中和低。
• 任务的执行时间:长、中和短。
• 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU 密集型任务应配置尽可能小的线程,如配置 N+1(其中N是CPU合适)个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2*N(其中N是CPU合适)。混合型的任务,如果可以拆分,将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime().availableProcessors()
方法获得当前设备的 CPU 个数。
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue
来处理。它可以让优先级高的任务先执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,等待的时间越长,则 CPU 空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行 SQL 变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。
2.5 线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。
• taskCount
:线程池需要执行的任务数量。
• completedTaskCount
:线程池在运行过程中已完成的任务数量,小于或等于taskCount
。
• largestPoolSize
:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
• getPoolSize
:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
• getActiveCount
:获取活动的线程数。
通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute
、afterExecute
和 terminated
方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。
3.线程池的生命周期
线程池的生命周期包括以下几个状态:
-
初始状态(
NEW
):线程池被创建后处于初始状态。此时线程池没有包含任何线程,也没有开始执行任务。 -
运行状态(
RUNNING
):通过调用线程池的execute()
或submit()
方法,线程池开始接受任务并创建线程执行。线程池可以动态地调整线程数量来适应任务的需求。 -
关闭状态(
SHUTDOWN
):当调用线程池的shutdown()
方法后,线程池进入关闭状态。此时线程池不会再接受新的任务提交,但会继续处理已经提交的任务直到完成。处于关闭状态的线程池仍然可以调用execute()
方法来提交任务,但会抛出RejectedExecutionException
。 -
停止状态(
STOP
):通过调用线程池的shutdownNow()
方法可以使线程池进入停止状态。此时线程池会立即停止,取消所有正在执行的任务,并且丢弃所有等待执行的任务。 -
整理状态(
TIDYING
):当线程池处于STOP
状态或者SHUTDOWN
后,并且所有任务都已经完成,线程池会进入整理状态。在整理状态中,线程池会清理已终止的工作线程。当线程池变为TIDYING状态时,会执行钩子函数terminated()。 -
终止状态(
TERMINATED
):当线程池完成整理操作后,最终进入终止状态。此时线程池彻底终止,不再接受任务和执行任务。
注意,线程池的状态可以通过isShutdown()
和 isTerminated()
方法进行查询,以确定线程池当前所处的状态。
4.代码分析线程池的运行原理
4.1 线程池控制状态ctl
ctl
是线程池源码中常常用到的一个变量,它的主要作用是记录线程池的生命周期状态和当前工作的线程数。它是一个原子整型变量。ctl
是一个32位的整数,高3位用于表示线程池的运行状态,低29位用于表示线程池中的线程数量。具体的结构如下所示:
31-29 | 线程池运行状态(用来保存线程池的状态 RUNNING,SHUTDOWN,STOP,STOP,STOP)
28-0 | 线程池中线程数量
ctl
在ThreadPoolExecutor
中的声明和初始化
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
-
ctl
(线程池控制状态)是原子整型的,这意味这对它进行的操作具有原子性。 - 如此一来,作为
ctl
组成部分的runState
(线程池生命周期状态)和workerCount
(工作线程数) 也将同时具有原子性。 -
ThreadPoolExecutor
使用ctlOf
方法来将runState
和workerCount
两个变量(都是整型)打包成一个ctl
变量。
4.1.1 工具人常量COUNT_BITS 和 CAPACITY
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
-
Integer.SIZE
为整型的位数32位 ,COUNT_BITS
位32 - 3 = 29。 - (1 << COUNT_BITS) - 1 的计算过程如下:
int 类型的1用二进制表示为:00000000 00000000 00000000 00000001
1 << COUNT_BITS,其中COUNT_BITS为29,1 << 29,代表将数字1向左移动29位。结果为:00100000 00000000 00000000 00000000
将上一步计算出的结果减1,最终CAPACITY用二进制表示为:00011111 11111111 11111111 11111111
- 因此在接下来的代码中, COUNT_BITS 就用来表示分隔
runState
和workerCount
的位数。 - 而CAPACITY 则作为取变量
runState
和workerCount
的工具。
4.1.2 线程池的生命周期常量
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
代码中的五个变量分别对应着线程池中的五个生命周期,他们的二进制表示如下:
RUNNING 二进制为 11100000 00000000 00000000 00000000
SHUTDOWN 二进制为 00000000 00000000 00000000 00000000
STOP 二进制为 00100000 00000000 00000000 00000000
TIDYING 二进制为 01000000 00000000 00000000 00000000
TERMINATED 二进制为 01100000 00000000 00000000 00000000
通过这里可以看出为什么将COUNT_BITS 设置为32 - 3,而不是将4或者其它的数。目的是将32位的高三位用来表示线程池的状态,后29位用来表示线程池的最大容量。
4.1.3 方法runStateOf
,workerCountOf
,ctlOf
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
-
runStateOf
方法用来获取当前线程池的状态,~CAPACITY就是11100000 00000000 00000000 00000000。再通过ctl
与其进行&运算就可以获取高三位的值,获取线程池的状态。 -
workerCountOf
方法通过将ctl
与CAPACITY&运算可以计算出线程池中的工作线程数。 -
ctlOf
方法runState
和workerCount
进行 | (按位或)操作来得到ctl
变量,就是因为runState
的高 3 位为有效信息,而workerCount
的低 29 位为有效信息,合起来正好得到一个含 32 位有效信息的整型变量。
4.2 源码分析线程池的执行流程
4.2.1 ThreadPoolExecutor.execute
方法
execute用于向线程池中提交任务
方法源码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
// 获取当前线程池的控制状态ctl。
int c = ctl.get();
// workerCountOf(c)获取工作线程数,判断工作线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 工作线程数小于核心线程数,调用addWorker(command, true) 添加核心线程并且执行任务,放回true直接结束方法。addWorker方法见4.2.2中的代码清单4-2
if (addWorker(command, true))
return;
// addWorker(command, true) 调用失败,更新ctl的临时变量c,重新获取线程池控制状态.
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
// 如果线程池是Running状态,并且往阻塞队列中添加任务完成。进入这里
// 考虑并发带来的影响,可能往同步队列中添加元素之前,线程池被调用了shutDown,或者shutDownNow等方法想关闭线程池。这里再一次的确定线程池的状态。如果还是Running,那代表线程池的状态没变,一直是Running。
int recheck = ctl.get();
// 线程池的状态变动了,调用remove方法从队列中移除任务。
if (! isRunning(recheck) && remove(command))
// 拒绝任务
reject(command);
// 走到这里,代表1.往同步队列中添加元素成功,并且2.线程池的状态一直是Running状态。
else if (workerCountOf(recheck) == 0)
// 到了这里,阻塞队列中有任务了,但是工作线程数为0,这种情况一般是临界点,举个例子,线程池中创建时,corePoolSize为 1,maximumPoolSize为 1,阻塞队列使用无界队列,再调用此excute方法之前,线程池一直是有个线程A在执行任务的,此时调用完workQueue.offer(command)往队列中添加元素之后,突然,执行任务的线程A突然遇到了一个空指针异常,线程A走终止逻辑。这是线程池中就需要一个新的线程来继续执行阻塞队列中的任务了。线程A终止的时候会调用processWorkerExit方法,该方法中也有addWorker(null, false);这个代码片段。
addWorker(null, false);
}
// 走到这里说明,线程池中的工作线程总数已经大于等于corePoolSize;线程池不是RUNNING状态,或者说线程池是RUNNING状态但无法继续往队列中添加元素了,这时候就需要创建非核心线程去执行任务了。
else if (!addWorker(command, false))
// 创建非核心任务执行
reject(command);
}
4.2.2 addWorker
方法
addWorker
被用于向线程池添加工作线程,该方法的用于将线程池的工作线程数的变量加1,然后创建一个线程并且运行。
private boolean addWorker(Runnable firstTask, boolean core) {
// for循环,这一层的功能用于将线程池的工作线程数变量workerCountOf加1
retry:
for (;;) {
// 获取线程池控制状态ctl
int c = ctl.get();
// 获取线程池的状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// rs >= SHUTDOWN 的状态有SHUTDOWN,STOP,TIDYING,TERMINATED 代表线程池不在接收新的任务了
// ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) 该方法要返回true,里面的3个判断条件至少有一个为false,rs == SHUTDOWN 判断线程状态为SHUTDOWN,firstTask == null 代表创建工作线程时指定的任务为null,这种情况是线程池主动调用addWorker方法才会出现的,比如代码清单4-1 execute方法 代码里面的代码片段addWorker(null, false),接着是 ! workQueue.isEmpty() 判断工作队列是否为空。如果上述条件判断中的任何条件为真,就表示不能满足添加工作线程的条件,会返回 false。这意味着在rs >= SHUTDOWN,只有当线程池状态为 SHUTDOWN、没有待执行的任务并且工作队列不为空时,才能添加工作线程,添加线程去执行同步队列中剩下的任务。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 获取当前线程池的工作线程数
int wc = workerCountOf(c);
// wc >= CAPACITY 返回true代表线程池中的线程已经>=2^29-1(CAPACITY 设定最大值为2^29-1,CAPACITY用来表示线程池工作线程的数量),wc >= (core ? corePoolSize : maximumPoolSize) 返回true代表当前要添加类型的线程数已经满了。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 将工作线程数加1
if (compareAndIncrementWorkerCount(c))
// 成功就跳出外层循环retry
break retry;
// CAS失败重新获取ctl的值
c = ctl.get(); // Re-read ctl
// 检查重新读取的runStateOf(c)是否与之前的状态rs不同,如果不同则继续外层循环retry
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 下面的代码用于往线程池中添加工作线程。
// workerStarted 工作线程启动状态位,true代表启动成功。
boolean workerStarted = false;
// workerAdded 工作线程添加标志位,true代表添加成功。
boolean workerAdded = false;
Worker w = null;
try {
// 创建一个Worker,详细见4.2.3 代码清单4-3
w = new Worker(firstTask);
// 获得工作线程
final Thread t = w.thread;
if (t != null) {
// 加锁,防止以下操作并发
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN 如果线程池的状态小于SHUTDOWN,这里可以取到的状态枚举有RUNNING,rs == SHUTDOWN && firstTask == null,这段返回true说明线程池的状态虽然是SHUTDOWN状态,但需要处理阻塞队列中剩下的任务。
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
// 如果线程t是活动的抛出异常。这边还没有启动线程t,线程t就已经活动了,说明有问题。
throw new IllegalThreadStateException();
// 将worker放入workers,workers是一个HashSet,用来存放所有的工作线程Worker。
workers.add(w);
int s = workers.size();
// largestPoolSize,曾经出现过的最大线程数,如果s > largestPoolSize,重新赋值,更新线程池峰值
if (s > largestPoolSize)
largestPoolSize = s;
// 工作线程添加成功。
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 开启工作线程。工作线程执行逻辑见代码4.2.3 代码清单4-3
t.start();
// 工作线程开启成功
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 工作线程启动失败,见如下的代码片段1
addWorkerFailed(w);
}
return workerStarted;
}
// 代码片段1 java.util.concurrent.ThreadPoolExecutor#addWorkerFailed
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 从工作线程w从工作线程集合中移除
if (w != null)
workers.remove(w);
// 线程工作workerCount数量减1
decrementWorkerCount();
// 基于状态判断尝试终结线程池,下面的小结会分析该方法
tryTerminate();
} finally {
mainLock.unlock();
}
}
4.2.3 runWorker
方法
addWorker
方法 完成了工作线程创建和启动。addWorker
方法中的t.start()
方法调用完成后会调用Worker的run方法,然后调用runWorker
方法来进行工作。
Worker(Runnable firstTask) {
// 设置同步状态为 -1,禁止线程中断,直到runWorker()方法执行,结合java.util.concurrent.ThreadPoolExecutor.Worker#interruptIfStarted 方法可以知道原因,interruptIfStarted放在代码清单 4-3。
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// Worker 实现了Runnable接口,将Worker自身传入到了newThread中,到时候线程启动时调用Worker.run()。
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
# java.util.concurrent.ThreadPoolExecutor#runWorker
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 获取当前工作线程要执行的任务
Runnable task = w.firstTask;
// 将firstTask置为null,表示任务被取出
w.firstTask = null;
// 因为初始化Worker的时候将线程标志设置为了-1,这里把state更新为0,允许线程中断
w.unlock(); // allow interrupts
// 记录线程是否因为用户异常终结,默认是true
boolean completedAbruptly = true;
try {
// 一个循环,当任务非空,或者通过getTask()方法可以从同步队列中获取任务时进入循环,如果while命中后半段当前线程会处于阻塞或者超时阻塞状态。关于getTask方法见4.2.4 代码清单4-4.
while (task != null || (task = getTask()) != null) {
// 可能会疑问这一步为什么要上锁,这块根本不存在并发啊!!!其实是线程池在执行shutdown等方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;获取不了锁说明线程正在执行任务,否则就是处于等待任务或者空闲状态,可以被中断。
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
// 这个条件判断工作线程是否需要中断。如果线程池处于STOP,TIDYING,TERMINATED状态,或者当前调用该方法的线程已经被中断并且线程池处于STOP,TIDYING,TERMINATED状态,且当前工作线程未被中断(如果isInterrupted返回true,代表已被中断,那就没必要再调用wt.interrupt()中断一次了),则中断当前工作线程,这里的中断仅仅是将中断标志位设为true了,具体的中断逻辑还需要用户实现。
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 钩子函数,执行任务前的准备工作,开发者可以去重写他的实现。
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 钩子函数,任务执行完毕时调用,开发者可以去重新他的实现
afterExecute(task, thrown);
}
} finally {
// 将临时表量task置为null
task = null;
// 累加Worker完成的任务数
w.completedTasks++;
// 解锁
w.unlock();
}
}
// 走到这里说明getTask()返回为null,线程正常退出,移除的话就直接调用下面的finally了
completedAbruptly = false;
} finally {
// 处理线程退出,completedAbruptly为true说明工作线程非正常退出,false表示正常退出。processWorkerExit见4.2.5 代码清单4-5
processWorkerExit(w, completedAbruptly);
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
4.2.4 getTask
方法
工作线程尝试从队列中获取任务
private Runnable getTask() {
// 上一次调用poll从队列中拉取任务是否超时,默认false,没超时。该状态用于配合线程池的超时退出机制使用。
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 判断:
// 1.rs >= SHUTDOWN,线程池状态至少是SHUTDOWN
// 2.在满足条件1的情况下,rs >= STOP 线程池的状态已经至少是stop了,stop状态不需要继续处理队列中的任务了;workQueue.isEmpty()说明队列已经空了,说明工作线程可以退出了。
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
// 参数allowCoreThreadTimeOut是用来控制核心线程是否需要超时退出的一个变量,默认情况下该变量为false(表示核心线程不需要超时退出),wc > corePoolSize 说明有非核心线程加入了线程池,这些线程是需要超时退出的。
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// wc > maximumPoolSize 当前的工作线程总数大于maximumPoolSize,说明了通过setMaximumPoolSize()方法减少了线程池容量
// timed && timedOut 返回true 说明了线程是需要超时退出的,并且上一轮循环通过poll()方法从任务队列中拉取任务为null
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// cas WorkerCount - 1,成功返回null,失败继续下一轮。
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果timed为true,通过poll()方法做超时拉取,keepAliveTime时间内没有等待到有效的任务,则返回null,如果timed为false,通过take()做阻塞拉取,会阻塞到有下一个有效的任务时候再返回(一般不会是null)
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // 注意,这里是能被中断唤醒的,中断唤醒会继续执行循环。
if (r != null)
return r;
// 代表调用 workQueue.poll() 超时返回null
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
4.2.5 processWorkerExit
方法
工作线程退出时走的方法
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 因为抛出用户异常导致线程终结,直接使工作线程数减1即可
// 如果没有任何异常抛出的情况下是通过getTask()返回null引导线程正常跳出runWorker()方法的while死循环从而正常终结,这种情况下,在getTask()中已经把线程数减1
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 线程退出时,加上他完成的任务
completedTaskCount += w.completedTasks;
// 将线程从workers集合中移除。
workers.remove(w);
} finally {
mainLock.unlock();
}
// 用于根据当前线程池的状态判断是否需要进行线程池terminate处理,见4.2.6 代码清单4-6
tryTerminate();
int c = ctl.get();
// 如果线程池的状态小于STOP,也就是处于RUNNING或者SHUTDOWN状态的前提下:
// 1.如果线程不是由于抛出用户异常终结,如果允许核心线程超时,则保持线程池中至少存在一个工作线程
// 2.如果线程由于抛出用户异常终结,或者当前工作线程数为0,那么直接添加一个新的非核心线程
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
// 理论上线程池中的最小线程数,允许线程池超时,最小为0,否则为corePoolSize
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果min == 0,队列中还有任务,那么将min == 1
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 工作线程数大于等于最小值,直接返回不新增线程
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
4.2.6 tryTerminate
方法
根据当前线程池的状态判断是否需要进行线程池terminate处理,在工作线程退出时,调用shutdown方法,
shutdownNow
方法方法时都会调用这个方法。
final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 判断线程池的状态,如果是下面三种情况下的任意一种则直接返回:
// 1.线程池处于RUNNING状态(RUNNING不需要结束线程池)
// 2.线程池至少为TIDYING状态,也就是TIDYING或者TERMINATED状态,意味着已经走到了下面的步骤,线程池即将终结
// 3.线程池状态为SHUTDOWN状态并且任务队列不为空(SHUTDOWN状态还需要处理完任务队列中的任务)
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 工作线程数不为0,则中断工作线程集合中的第一个空闲的工作线程
if (workerCountOf(c) != 0) { // Eligible to terminate
// 到了这一步,说明工作线程中还有工作线程,不允许结束线程池
// 调用interruptIdleWorkers方法中断工作线程,interruptIdleWorkers方法代码在下面,interruptIdleWorkers(ONLY_ONE)只会中断一个线程,被中断的线程会继续进入tryTerminate方法去判断workerCountOf是否为0,最终总会有一个线程会执行到下面的代码去。
interruptIdleWorkers(ONLY_ONE);
// 说明该线程不是最后一个线程,执行上面的interruptIdleWorkers去中断其它线程(有这么一种情况,非空闲的线程在执行某个任务,执行任务完毕之后,如果它刚好是核心线程,就会在下一轮循环阻塞在任务队列的take()方法,如果不做额外的干预,它甚至会在线程池关闭之后永久阻塞在任务队列的take()方法中。为了避免这种情况,每个工作线程退出的时候都会尝试中断工作线程集合中的某一个空闲的线程,确保所有空闲的线程都能够正常退出。)
return;
}
//注意-方法走到了这里,说明结束线程池中的工作线程为0了-
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// CAS设置线程池状态为TIDYING,如果设置成功则执行钩子方法terminated()
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
// 最后更新线程池状态为TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
// 唤醒阻塞在termination条件的所有线程,这个变量的await()方法在awaitTermination()中调用
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
// 中断空闲的工作线程,onlyOne为true的时候,只会中断工作线程集合中的某一个线程
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
// t.isInterrupted() 为 true表示已经被中断了,这里是去寻找第一个没有被中断的空闲线程。w.tryLock() 加锁成功表示线程是空闲的,否则表示线程在执行任务,不允许中断。
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
// 这里跳出循环,也就是只中断集合中第一个工作线程,集合中的第一个线程被中断
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
到这里,浅析了execute方法的执行流程,从工作线程的创建,工作线程的结束,阻塞队列在线程池中的作用等等,下面将介绍线程池的关闭方法shutdown和shutdownNow
4.3 线程池的关闭
关于
shutdown
和shutdownNow
方法的使用可参考本篇 2.3 关闭线程池
4.3.1 shutdown方法
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 权限校验,安全策略相关判断
checkShutdownAccess();
// 设置SHUTDOWN状态
advanceRunState(SHUTDOWN);
// 中断所有的空闲的工作线程
interruptIdleWorkers();
// 钩子方法
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 调用上面分析果敢的尝试terminate方法,使状态更变为TIDYING,执行钩子方法terminated()后,最终状态更新为TERMINATED
tryTerminate();
}
// 中断所有的空闲的工作线程
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
4.3.2 shutdownNow
方法
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();
}
// 调用上面分析果敢的尝试terminate方法,使状态更变为TIDYING,执行钩子方法terminated()后,最终状态更新为TERMINATED
tryTerminate();
return tasks;
}
// 遍历所有的工作线程,如果state > 0(启动状态)则进行中断
private void interruptWorkers() {
// assert mainLock.isHeldByCurrentThread();
for (Worker w : workers)
w.interruptIfStarted();
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
参考
书籍《Java并发编程的艺术》--Java中的线程池
详解Java线程池的ctl(线程池控制状态)【源码分析】文章来源:https://www.toymoban.com/news/detail-616493.html
线程池与线程的几种状态 文章来源地址https://www.toymoban.com/news/detail-616493.html
到了这里,关于Java中的线程池使用及原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!