目录
- 一、为什么引入线程池技术?
- 二、Executor框架
- 2.1 Runnable、Callable与Future接口
- 2.2 Executor接口
- 2.2.1 Executor
- 2.2.2 ExecutorService
- 三、Java中线程池的工作原理
- 3.1 ThreadPoolExecutor中核心的变量及常量
- 3.2 线程池的任务调度逻辑
- 3.2.1 addWorker方法
- 3.2.1.1 状态及容量检查
- 3.2.1.2 添加工作线程
- 3.2.2 线程的启动
- 3.2.3 线程的复用
- 3.2.4 线程池的拒绝策略
- 3.2.5 总结
- 3.3 线程池的关闭流程
- 3.3.1 ThreadPoolExecutor#shutdown
- 3.3.2 ThreadPoolExecutor#shutdownNow
- 3.3.3 ThreadPoolExecutor#processWorkerExit
- 3.3.4 ThreadPoolExecutor#awaitTermination
- 四、线程池的使用
- 4.1 FixedThreadPool
- 4.2 SingleThreadExecutor
- 4.3 CachedThreadPool
- 4.4 ScheduledThreadPoolExecutor
- 五.线程池的几种BlockingQueue
- 5.1 LinkedBlockingQueue
- 5.2 SynchronousQueue
- 5.3 DelayedWorkQueue
- 5.3.1 RunnableScheduledFuture
- 5.4.2 DelayedWorkQueue的数据结构
一、为什么引入线程池技术?
在并发编程的过程中,创建线程是十分消耗资源的,甚至可能会引发内存溢出,线程池技术应运而生。在Java中,几乎所有需要异步或并发执行任务的程序都需要利用线程池进行调度,合理设计的线程池能够带来以下优势:
降低资源消耗。通过对线程进行重复利用,降低线程创建和销毁的性能损耗。
提高响应速度。当任务到达时,省去线程创建的步骤,线程池直接调度线程执行任务。
实现线程的统一管理。线程是稀缺资源,利用线程池可以对其进行统一分配、调优和监控。
Java中内置了一套完备的线程池框架,想要在开发过程中根据业务场景合理、高效地使用线程,必须全面了解其内部的工作原理。
二、Executor框架
一个基于线程池模型的多线程程序在并发处理多个任务时,通常对应着两级调度结构:
在上层应用程序中,用户级的调度器将这些任务映射为固定数量的线程
在底层,操作系统内核将这些线程映射到CPU上
在HotSpot VM的线程模型中,Java线程(Thread对象)被一对一映射为本地操作系统的线程。一个Java
线程的启动和终止,对应着一个本地操作系统线程的创建和回收。
Java的线程池调度模型中,以Executor接口为核心的Executor框架充当了用户级调度器的角色,其包含的主要的类与接口如下:
Executor框架主要由三部分组成:
- 任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
- 任务的执行。包括任务执行机制的核心接口Executor,继承自Executor的ExecutorService接口,抽象类AbstractExecutorService以及其重要的子类ThreadPoolExecutor。
- 异步计算的结果。包括Future接口,继承自Future的RunnableFuture以及其重要的实现类FutureTask。
2.1 Runnable、Callable与Future接口
Runnable、Callable、Future是三个与线程池框架关联十分紧密的接口:
-
Runnable、Callable都是函数式接口,两者都是为类实例可能由另一个线程执行的场景(例如将Runnable或Callable任务提交到线程池中)设计的,区别在于Callable接口可以返回计算结果
-
Future接口用于表示异步任务的执行结果。 其提供了检查任务是否完成、等待任务完成以及获取任务结果的能力。
本节侧重介绍Future接口在JDK线程池框架中扮演的角色,关于其具体的实现原理可以参考笔者的另一篇原创文章:Future接口的实现原理 。
JUC.ExecutorService中定义了向线程池提交Runnable、Callable任务的抽象方法,其具体实现在抽象类JUC.AbstractExecutorService中:
/**
* 提交Runnable任务,并返回一个代表该任务的Future。
* Future的get()方法将在任务执行成功后返回null。
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
//将Runnable任务封装成RunnableFuture
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask); //将任务交给线程池执行
return ftask;
}
/**
* 提交有返回值的任务,并返回Future
* Future的get()方法在任务执行成功后返回任务的计算结果。
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
//将Callable任务封装成RunnableFuture
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
- 在线程池的框架中,Runnable和Callable任务都会被封装成RunnableFuture
- RunnableFuture继承自Runnable接口和Future接口,因此能够作为任务提交到线程池中执行,也能通过其获取到任务的执行结果
- RunnableFuture的具体实现为FutureTask,FutureTask中聚合了Callable类型的实例变量用于存放具体的任务,并且支持Runnable任务到Callable任务的转换,可以说FutureTask实现了Callable和Runnable任务的统一管理
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable; //Callable任务直接赋值
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
//Runnable任务需要转换成Callable再赋值
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
Runnable到Callable的转换其实很简单,适配器模式的经典应用:
FutureTask作为内置在JDK中的Future接口实现类,在线程池的框架中起到了举足轻重的作用:
将Runnable和Callable任务进行了统一的封装,使线程池无需关心上层任务的具体类型,只需负责调度线程执行任务即可
作为任务提交之后的返回值,用于获取任务的执行结果
2.2 Executor接口
在Java的线程池框架中,Executor接口是任务执行机制的核心,Executor以及其子接口ExecutorService提供了任务执行涉及到的通用能力,包括任务的提交、调度运行和终止等等。
2.2.1 Executor
Executor接口只定义一个excute方法,用于执行提交的Runnable任务
Executor接口的作用是将任务提交与每个任务将如何运行的机制(括线程使用、调度等的细节)解耦
内存一致性保证:线程中将Runnable任务提交给Executor之前的操作happens-before任务开始执行(可能由另一个线程执行)
/**
* 在之后的某个时间执行提交的任务。
* 该任务可以在新线程、线程池或调用线程中执行,取决于Executor的具体实现。
*
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
*/
void execute(Runnable command);
2.2.2 ExecutorService
ExecutorService继承自Executor接口,在Executor接口已有的任务执行能力的基础上:
- 提供了关闭执行器以及终止任务执行的能力
- 任务提交后,能够生成 Future对象以跟踪任务的处理进度
内存一致性保证:线程中将任务提交给ExecutorService之前的操作happens-before对该任务采取的任何操作,而这些操作又happens-before通过Future.get()获取任务执行结果
/**
* 提交有返回值的任务,并返回Future,Future的get()方法在任务执行成功后返回任务的执行结果。
*
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
*/
<T> Future<T> submit(Callable<T> task);
/**
* 提交Runnable任务,并返回一个代表该任务的Future。
* Future的get()方法将在任务执行成功后返回null。
*
* @throws RejectedExecutionException if the task cannot be
* scheduled for execution
*/
Future<?> submit(Runnable task);
/**
* 启动有序的关闭程序,关闭过程中仍会执行已提交的任务(正在执行和队列中的),但不会接收新任务
* 该方法不会等待已提交的任务执行完成,如果需要,使用#awaitTermination方法
*/
void shutdown();
/**
* 启动有序的关闭程序,不再接收新任务
* 尝试停止所有正在执行的任务(不是一定能够停止,典型的实现通过Thread#interrupt取消,这意味着未能响应中断的任务不会终止执行),停止等待任务的处理,并返回等待执行的任务列表。
* 此方法不会等待正在执行的任务终止。 如果需要,使用#awaitTermination方法
*/
List<Runnable> shutdownNow();
/**
* 在关闭请求后阻塞,直到所有任务都完成执行,或者发生超时,或者当前线程被中断,以先发生者为准。
*
* @return {@code true}: executor终止
* {@code false}:在终止前超时
* @throws InterruptedException if interrupted while waiting
*/
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
三、Java中线程池的工作原理
Executor框架中,线程池有两个原生的实现类,分别为ThreadPoolExecutor和ScheduledThreadPoolExecutor
- ThreadPoolExecutor是Executor框架最核心的类,其内部实现了线程池中任务的执行、线程的调度以及任务的终止等细节
- ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,其依托线程池模型实现了任务延迟和定期执行的能力
本章将着重分析ThreadPoolExecutor的内部实现,以剖析线程池的工作原理。
3.1 ThreadPoolExecutor中核心的变量及常量
ThreadPoolExecutor中通过位运算表征线程池的运行状态
private static final int COUNT_BITS = Integer.SIZE - 3; //29
//00011111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//11100000000000000000000000000000
private static final int RUNNING = -1 << COUNT_BITS;
//00000000000000000000000000000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
//00100000000000000000000000000000
private static final int STOP = 1 << COUNT_BITS;
//01000000000000000000000000000000
private static final int TIDYING = 2 << COUNT_BITS;
//01100000000000000000000000000000
private static final int TERMINATED = 3 << COUNT_BITS;
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; }
- ctl属性:AtomicInteger类型,高3位存储线程池状态,低29位存储线程数量
//初始值线程池状态为RUNNING、线程数为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- workQueue属性:阻塞队列,用于存放待执行的任务
private final BlockingQueue<Runnable> workQueue;
- workers属性:用于存放工作线程,非线程安全,操作该属性需要获取全局锁
private final HashSet<Worker> workers = new HashSet<Worker>();
- 线程池的线程容量:
corePoolSize:核心线程数,由volatile修饰
maximumPoolSize:最大线程数,由volatile修饰,最大值受限于CAPACITY
- 线程池的状态说明:
RUNNING:线程池可以接收新的任务和执行已入队的任务
SHUTDOWN:线程池处不接收新任务,但不影响正在执行的任务,且能处理已入队的任务
STOP:线程池处不接收新任务,移除已经入队的任务且不处理,同时会中断正在执行的任务
TIDYING:线程池中所有的任务已终止,线程数为0;线程池变为TIDYING状态时,会执行钩子函数terminated(),默认实现为空
TERMINATED:钩子函数terminated()被执行完成
- 线程池的状态转换:
线程池一旦被创建(比如调用Executors.newFixedThreadPool()方法),就处于RUNNING状态,并且线程池中的线程数为0
RUNNING —> SHUTDOWN:调用线程池的shutdown()方法
( RUNNING or SHUTDOWN ) —> STOP:调用线程池的shutdownNow()方法
SHUTDOWN —> TIDYING:任务队列为空并且线程池中不存在工作线程
STOP —> TIDYING:线程池中不存在工作线程
TIDYING —> TERMINATED:钩子函数terminated()被执行完成
3.2 线程池的任务调度逻辑
当ThreadPoolExecutor线程池接收到一个待处理的任务之后,其具体的调度逻辑是在execute()方法中实现的:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//分支1:当前工作线程数 < 核心线程数
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) //添加一个核心线程执行当前任务
return;
c = ctl.get(); //若因并发问题导致添加失败,需要重新获取ctl(保证后续逻辑clt的准确性)
}
//分支2:在线程池状态为RUNNING的前提下,将任务入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) //double check线程池状态,非RUNNING则移除command并走拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0) //任务入队成功后,若不存在工作线程需要手动创建一个非核心线程
addWorker(null, false);
}
//分支3:任务队列已满导致入队失败,需要创建一个非核心线程执行当前任务,若创建失败则走拒绝策略
else if (!addWorker(command, false))
reject(command);
结合execute()方法的源码,总结线程池中任务的主要调度流程如下:
下面我们将更深入地剖析源码,揭秘线程池中线程的创建、任务的真正执行、线程复用以及拒绝策略等细节。
3.2.1 addWorker方法
该方法的主要作用就是在线程池中添加工作线程并执行任务,两个参数:
- Runnable firstTask:即新添加的线程要执行的第一个任务(从这个命名就可以看出,这个线程的生命周期并不只有这一个任务要执行,线程的复用会在后文详细分析)
- boolean core:新添加的线程是否为核心线程
该方法的调用场景分3种情况:
- firstTask非空,core为true:创建一个核心线程执行当前任务
- firstTask非空,core为false:创建一个非核心线程执行当前任务
- firstTask为null,core为false:创建一个非核心线程执行任务队列中的遗留任务
该方法只有在线程添加完成且成功启动的情况下才会返回true
我们将该方法分成两个部分剖析,因为实在是太长了(再次感叹Doug Lea的脑回路)!!!!
3.2.1.1 状态及容量检查
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//1.状态检查
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize)) //2.容量检查
return false;
if (compareAndIncrementWorkerCount(c)) //3.cas更新工作线程数量
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
}
}
...
-
线程池状态检查时,满足下列两种场景之一,才继续后续逻辑,否则直接返回false
- 线程池处于RUNNING状态
- 线程池处于SHUTDOWN状态,并且firstTask为null,并且任务队列不为空
这里要结合 execute()方法的分支2去分析,当任务入列后,double check时发现队列已处于SHUTDOWN状态,此时如果尝试移除任务失败,且线程池中已经不存在工作线程,那么就会添加一个非核心线程去处理已经入队的任务(因为SHUTDOWN状态下的线程池仍然需要处理任务队列中的任务)
-
容量检查失败则直接返回false
- 如果是添加核心线程,需要判断核心线程数是否已满(corePoolSize)
- 如果是添加非核心线程,需要判断是否到达最大线程数(maximumPoolSize)
-
容量检查通过后,会尝试通过CAS更新工作线程数(ctl属性的低29位)
- CAS成功,跳出最外层循环,继续后续逻辑
- CAS失败,且线程池状态未发生改变,则继续内层循环重新进行容量检查并尝试CAS
- CAS失败,且线程池状态发生改变,则需要重新执行外层循环进行状态检查、容量检查并尝试CAS
3.2.1.2 添加工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
...
...
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock; //1.加全局锁
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
workers.add(w); //2.添加工作线程
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock(); //3.释放全局锁
}
if (workerAdded) {
t.start(); //4.启动线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
- 向线程池中添加新生线程的过程是需要获取全局锁的
- 线程池中的线程被封装成了Worker对象
- Worker实现了Runnable,并且其继承了AbstractQueuedSynchronizer(具备AQS的同步管理能力)。
- Worker对象创建时,将新生的线程记录在thread属性中,并通过firstTask属性记录任务
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); //这里我们终于看到了线程产生之处(工厂模式创建一个新的线程)
}
- 这里有个隐蔽的细节,Worker对象本身也是个Runnable任务,线程创建时通过.newThread(this)将当前Worker对象与之关联,因此addWorker()中通过t.start()启动线程时,其实是调用了Worker中实现的run()方法
- execute()方法中,任务入队成功后,若不存在工作线程需要手动创建一个非核心线程,此处如果创建一个核心线程会有什么问题?
在高并发的场景下,很有可能由于核心线程数达到上限导致其他Worker线程添加失败,使得一些本可以直接执行的任务需要排队执行
3.2.2 线程的启动
Worker类中run()方法的逻辑如下(剔除掉了一些非核心逻辑):
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread(); //任务执行线程
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock(); //1.设置独占锁
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted()) //2.中断的处理
wt.interrupt();
try {
Throwable thrown = null;
try {
task.run(); //3.执行任务
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
}
} finally {
task = null;
w.completedTasks++;
w.unlock(); //4.释放锁
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly); //5.工作线程退出
}
}
执行上述逻辑的线程,就是Worker对象对应的工作线程
工作线程真正执行任务逻辑前,会先设置独占锁,这样根据独占锁的状态可以判断Worker对应的线程是否在处理任务(这个状态的消费后文会解析)
线程中断逻辑:
- 当前线程池状态大于等于STOP且当前线程未设置中断状态,则主动设置当前线程的中断标识
- 当前线程池状态小于STOP,且线程未带中断标识,则不需要处理,直接执行后续的任务逻辑
- 当前线程池状态小于STOP,且线程带有中断标识(注意:Thread.interrupted()返回中断状态同时也会清除中断标识),此时会double check线程池状态
- 如果线程池状态大于等于STOP(瞬间的并发导致的两次状态不一致),则需要保证线程设置了中断标识
- 当前线程池状态小于STOP,则需要保证线程的中断标识被清除
总结一下这段代码的整体逻辑:
- 如果线程池状态大于等于STOP,则要保证当前线程的中断标识为true
- 如果线程池状态小于STOP,则要保证当前线程的中断标识为false
3.2.3 线程的复用
工作线程进入runWorker()的任务执行逻辑后,并不是执行执行完当前任务就结束其使命了,即线程池中的线程是能够复用的,而其中关窍正是在runWorker()的循环判断条件中:
- 当task为空时:
当前线程会通过getTask()从阻塞队列里取任务,如果返回null,意味着当前工作线程退出runWorker()方法,JVM 会自动回收该线程
下面分析getTask()方法的内部细节:
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {//1.线程池状态检查
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) { //2.处理超时
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try { //3.取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
- 当线程池不需要再处理队列中的任务时,getTask()将返回null,调用getTask()方法的工作线程执行结束,工作线程数-1。这个分支对应两种场景:
- 线程池状态为SHUTDOWN,且任务队列为空
- 线程池状态大于等于STOP
- 局部变量timed表征从阻塞任务取队列时是否有超时时间:
- allowCoreThreadTimeOut为true(即核心线程取任务时有超时时间,创建线程池时可以设置该属性,默认为false)或者核心线程数已满,则timed为true,线程会在取任务时设置超时时间workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
- timed为false,线程在取任务时会无限期阻塞,直到取到任务
- 如果取任务超时,且满足以下条件之一,getTask()将返回null,调用getTask()方法的工作线程执行结束,工作线程数-1:
- 工作线程数大于1
- 任务队列为空
梳理getTask()的整体逻辑可以看出,如果核心线程数已满或者allowCoreThreadTimeOut为true,线程在获取任务超时之后,就会退出任务的执行逻辑,因为线程池认为此时池中的工作线程数量供大于求,这体现了线程池动态调整其工作线程数的策略和思想。
3.2.4 线程池的拒绝策略
参考executer()方法,可以看出线程池在以下两种场景会执行拒绝策略:
任务入队后,double check线程池状态,如果非RUNNING状态则移除command并执行拒绝策略
任务队列已满导致入队失败,需要创建一个非核心线程执行当前任务,若已经达到最大线程数则执行拒绝策略
private volatile RejectedExecutionHandler handler;
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
handler即为拒绝策略的处理器,是线程池的一个实例变量,通常在线程池创建时传入,JDK中有四种实现:
AbortPolicy:默认的拒绝策略,任务无法处理时直接抛出RejectedExecutionException异常,适合主调线程不可阻塞且FailFast场景
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
DiscardPolicy/DiscardOldestPolicy:适合主调线程不可阻塞且FailSafe场景
//do nothing 直接丢弃任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}
//在线程池未关闭的情况下,抛弃任务队列中最旧的任务也就是最先加入队列的,再将新任务尝试添加进去,若线程池关闭则任务r被丢弃
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
CallerRunsPolicy:caller线程直接运行任务,可能会block caller线程,适合不能接受任务丢失、可阻塞主调线程的场景使用
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
3.2.5 总结
ThreadPoolExecutor执行execute方法分下面 4 种情况。
- 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务,这一步骤需要获取全局锁。
- 如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue。
- 如果队列已满,则创建新的线程来处理任务 ,这一步骤需要获取全局锁。
- 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被拒绝,并执行拒绝策略。
ThreadPoolExecutor的总体设计思路,是为了在执行 execute()方法时,
尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于
corePoolSize),几乎所有的execute()方法调用都是执行步骤2(入队),不需要获取全局锁。
3.3 线程池的关闭流程
3.3.1 ThreadPoolExecutor#shutdown
调用线程池的shutdown()方法后,线程池将变成SHUTDOWN状态,不再接收新任务,但会处理完正在运行的和在阻塞队列中等待的任务。shutdown()方法的主要逻辑如下:(剔除了部分非核心代码)
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //全局锁,可重入
try {
//1.CAS将线程池状态设置为SHUTDOWN
advanceRunState(SHUTDOWN);
//2.中断所有空闲线程
interruptIdleWorkers();
} finally {
mainLock.unlock();
}
//3.尝试终止线程池
tryTerminate();
}
interruptIdleWorkers(false):中断所有空闲的Worker线程
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //全局锁,可重入
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
结合上文中runWorker方法中的逻辑,线程池中的工作线程(Worker)可以划分为两类:
- 空闲Worker:当队列中没有任务时,这部分空闲线程阻塞在从队列中获取任务的逻辑里,未上Worker锁。
- 运行中Worker:正在task.run()执行任务的Worker,上了Worker锁
参考Worker类中的tryAcquire()方法可以看出,Worker锁不可重入,因此只有空闲Worker其tryLock才会返回true,也即该方法仅会将空闲Worker的中断标识置为true
中断信号发出后,对应的阻塞在getTask()获取任务的空闲Worker退出阻塞状态,getTask()将return null,并执行对应Worker线程的退出逻辑
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
...
...
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {//2.线程池状态检查返回null
decrementWorkerCount();
return null;
}
...
...
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) { //1.中断信号发出,退出阻塞态
timedOut = false;
}
}
}
运行中的Worker不会收到中断信号,运行结束后,会从队列中阻塞获取任务,如果队列为空,又因为线程池处于SHUTDOWN状态,不会接收新任务,所以会一直阻塞在任务获取逻辑中,导致线程无法终止!!!这就需要在shutdown()后,仍可以发出中断信号。
final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//走到这里有两种情况:
//1.STOP状态
//2.SHUTDOWN状态且workQueue为空
if (workerCountOf(c) != 0) {
//中断一个正在等任务的空闲worker
//收到信号后再次判断线程池状态,会return null,执行线程的退出逻辑
interruptIdleWorkers(ONLY_ONE);
return;
}
//走到这里,开始执行terminate逻辑:
//1.SHUTDOWN状态,且workQueue为空,且worker数为0
//2.STOP状态,且worker数为0
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//CAS将状态变成TIDYING
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();//调用钩子函数,需要子类实现
} finally {
ctl.set(ctlOf(TERMINATED, 0));//CAS将状态变成TERMINATED
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
//CAS失败重新进入循环
}
}
Doug Lea大神巧妙地在所有可能导致线程池产终止的地方安插了tryTerminated()尝试线程池终止的逻辑
该方法的整体逻辑分两个部分:
- 如果处于关闭流程中的线程池已经没有任务存在,且仍存在工作线程,则需要发出中断信号,至于为什么在该方法中只是中断一个正在等
- 任务的空闲Worker,是因为每个工作线程执行其退出逻辑processWorkerExit()时,还会再调用tryTerminate(),这个后面会分析
如果处于处于关闭流程中的线程池已经没有任务存在,且已经没有任何工作线程,则执行terminate逻辑
总结:
- 调用线程池的shutdown()方法后,线程池将变成SHUTDOWN状态,线程池无法再接收新的任务
- shutdown()方法不会干预正在执行的以及队列中已经存在的任务,这些任务仍能正常执行,但shutdown()方法不会等待这些任务执行完成
- shutdown()方法会中断所有的空闲线程,收到中断信号的线程会最终从线程池中移除
- shutdown()方法退出之前,会调用tryTerminated()尝试终止线程池
3.3.2 ThreadPoolExecutor#shutdownNow
调用线程池的shutdownNow()方法后,线程池将变成STOP状态。STOP状态下,线程池处不接收新任务,中断正在执行的任务,移除已经入队的任务不再处理,并返回已经入队的任务列表。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //全局锁,可重入
try {
//CAS设置线程池状态为STOP
advanceRunState(STOP);
//中断所有worker,包括正在执行任务的
interruptWorkers();
//将workQueue中的元素移除并放入一个List并返回
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate(); //尝试终止线程池
return tasks;
}
interruptWorkers() :对所有Worker调用interruptIfStarted(),其中会判断Worker的AQS state是否大于等于0,再设置线程的中断标识
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
至于为什么要加上state的判断,我们看下Doug Lea的注释一目了然:
需要注意的是,对于运行中的线程调用Thread.interrupt()并不能保证线程被终止,task.run()内部可能捕获了InterruptException,没有上抛,导致线程正在执行的任务无法立即终止
总结:
- 调用线程池的shutdownNow()方法后,线程池将变成STOP状态,线程池无法再接收新的任务
- shutdownNow()会将线程池中所有的Worker线程设置中断标识,收到中断信号的线程最终会由于getTask()返回null而从线程池中移除
- shutdownNow()会移除已经入队的任务不再处理,并返回队列中的任务列表。
- shutdownNow()方法退出之前,会调用tryTerminated()尝试终止线程池
3.3.3 ThreadPoolExecutor#processWorkerExit
清理一个即将销毁的Worker,只有线程池中Worker对应的线程才会调用该方法:(剔除了部分非核心逻辑)
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly)
//只有异常退出的需要在这里做--操作,其他情况都是在getTask()方法里做的
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //全局锁
try {
workers.remove(w); //销毁Worker
} finally {
mainLock.unlock();
}
tryTerminate(); //尝试终止线程池,这个逻辑解释了为什么tryTerminate()中只是中断一个空闲线程
int c = ctl.get();
if (runStateLessThan(c, STOP)) { //仅 < STOP状态(任务队列中可能存在任务)时进入
if (!completedAbruptly) { //非异常退出的场景,如果核心线程允许超时且队列中还有任务,则最小线程数为1,如果核心线程不允许超时,则最小线程数为corePoolSize
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false); //任务异常退出或者线程池中线程数不够,需要补齐新增一个线程
}
}
3.3.4 ThreadPoolExecutor#awaitTermination
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
private final Condition termination = mainLock.newCondition();
termination.awaitNanos() 是通过 LockSupport.parkNanos(this, nanosTimeout)实现的阻塞等待,阻塞等待过程中发生以下具体情况会解除阻塞:
-
termination.signalAll(),只有在 tryTerminated()尝试终止线程池成功,将线程池更新为TERMINATED状态后才会signalAll(),awaitTermination()再次判断状态会return true退出
-
达到了超时时间,此时nano==0,再次循环判断return false
-
当前线程被中断,termination.awaitNanos()会上抛InterruptException,awaitTermination()继续上抛给调用线程
四、线程池的使用
通过 Executor 框架的工具类 Executors,可以创建 3 种类型的 ThreadPoolExecutor。
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
4.1 FixedThreadPool
FixedThreadPool中 corePoolSize
和 maximumPoolSize
都被设置为创建时传入的参数 nThreads
如果线程池中的工作线程数量小于corePoolSize
,则创建新的核心线程来执行任务。
在线程池完成预热之后(工作线程数等于corePoolSize),新的任务将加入 LinkedBlockingQueue
。
工作线程执行完 firstTask后,会在循环中反复从LinkedBlockingQueue
获取任务来执行。
FixedThreadPool使用无界阻塞队列LinkedBlockingQueue
作为线程池的工作队列(队列的容量为 Integer.MAX_VALUE)。
由于不存在阻塞队列已满的场景,maximumPoolSize
和keepAliveTime
都是无效参数,且处于RUNNIG状态的线程池不会拒绝任务
4.2 SingleThreadExecutor
SingleThreadExecutor是使用单个工作线程的线程池。
SingleThreadExecutor的corePoolSize
和maximumPoolSize
被设置为 1。其余参数与FixedThreadPool
一致,两个线程池整体的调度逻辑也没有太大区别
SingleThreadExecutor当第一个工作线程创建后即完成预热,之后新的任务将加入 LinkedBlockingQueue
。
4.3 CachedThreadPool
CachedThreadPool中corePoolSize
为0,maximumPoolSize
为 Integer.MAX_VALUE,即maximumPool是无界的。keepAliveTime为60L,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间为 60 秒,超过60秒后该线程将从线程池中移出。
CachedThreadPool使用SynchronousQueue
作为线程池的工作队列,SynchronousQueue
是一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。
如果主线程提交任务的速度非常高,CachedThreadPool会不断创建新线程(maximumPool是无界的),极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
4.4 ScheduledThreadPoolExecutor
下面章节详解。
注意:线程池的参数配置和具体选型是和业务场景紧密相关的,好多公司都是基于业务场景,在JDK原生的ThreadPoolExecutor封装定制化的线程池。
五.线程池的几种BlockingQueue
线程池 | 队列类型 |
---|---|
CachedThreadPool | SynchronousQueue |
SingleThreadExecutor | LinkedBlockingQueue |
FixedThreadPool | LinkedBlockingQueue |
ScheduledThreadPool | DelayedWorkQueue |
向线程池添加任务的部分代码如下:
5.1 LinkedBlockingQueue
这个队列是链表形式的阻塞队列,长度无限制,这个队列用于SingleThreadExecutor
和FixedThreadPool
中,single是fix的特殊情况,core和max设置为1即可。那么可以知道FixedThreadPool线程池可以一直向其中提交任务,线程数量大于core数量的话,任务会被加入到LinkedBlockingQueue中,由于没有长度限制,也就不会出现RejectedExecutionHandler
拒绝策略的触发时机。
5.2 SynchronousQueue
CachedThreadPool
使用的该队列,该队列是个阻塞队列,主要使用的两个方法是offer
和take
,一个是插入数据,一个是取出数据。队列有如下性质:
- 队列有公平和非公平之分(公平队列存储FIFO,非公平栈存储FILO);
- offer和take操作会阻塞当前线程,两个线程只有配对进行操作,才能同时唤起;
- offer和take可以设置超时时长,超过时长操作失败。
我们可以无限向CachedThreadPool
线程池中添加任务,因为最大线程数是integer,MAX;SynchronousQueue是插入和取出是需要配对出现的,有两种情况会发生:
- 当我们直接插入任务到队列的时候,这个时候恰好有worker从队列中take取出数据的话,那么插入数据就成功。
- 由于插入数据调用的是workQueue,offer方法,因此没有设置超时时间,当所有worker一直处于工作的情况下,就不会take取出数据,插入失败,这个情况下,会出触发下面的
addworker(command,false)
,创建非核心线程。
我们可以看到,在某些情况下CachedThreadPool
会不断窗口新的非核心线程,又没有最大线程数量的限制,所以这个线程池不推荐使用,大量的资源浪费。
SynchronousQueue源码分析
5.3 DelayedWorkQueue
ScheduledThreadPool
使用的是DelayedWorkQueue
,我们知道ScheduledThreadPool
线程池具有timer的类似的功能,可以做定时任务,定时任务的排序,存取操作是由DelayedWorkQueue
实现的,
ScheduledThreadPool是继承自ThreadPoolExecutor,所以大部分函数接口都是相同的
多了一些有定时任务的接口如下:
随便看一个接口:
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
5.3.1 RunnableScheduledFuture
首先将runable包装成RunnableScheduledFuture,它继承关系:
- 定时运行时间节点接口
该接口返回一个时间节点,线程池在取任务的时候,会根据任务的时间节点来执行任务,如果头部第一个任务时间还没到,那么等待一定的延迟时间再继续取任务执行。
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
-
定时任务
boolean isPeriodic();
方法返回是否是需要重复运行的任务。 -
compareTo(T o);
用于队列数据进行排序操作的
5.4.2 DelayedWorkQueue的数据结构
DelayedWorkQueue的数据结构是一个连续空间的一维数组,空间不够会自动增长50%,内部的数据采用小堆形式存储;
private static final int INITIAL_CAPACITY = 16;
private RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
/**
* Resizes the heap array. Call only when holding lock.
*/
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
- 添加任务到堆中,按照RunnableScheduledFuture的
compareTo
函数比较大小进行插入操作。
函数调用链:
offer(Runnable x) -> siftUp(int k, RunnableScheduledFuture<?> key) -> setIndex(RunnableScheduledFuture<?> f, int idx)
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
grow();
size = i + 1;
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
} else {
siftUp(i, e);
}
if (queue[0] == e) {
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
首先判断添加之后数据是否需要扩容,需要扩容增加容量50%,然后将数据添加到堆的最后位置,然后从底部向上排序。
- 取出任务,从堆的顶部取出一个任务,将末尾任务放到堆顶部,进行小堆的重新排序。
函数调用链:
take() -> finishPoll(RunnableScheduledFuture<?> f) -> siftDown(int k, RunnableScheduledFuture<?> key)
-> setIndex(RunnableScheduledFuture<?> f, int idx)
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
首先判断首个元素是不是空,是空的话说明没用元素,需要线程挂起,等待offer操作唤起线程。如果取出首个元素存在,那么需要判断当前时间节点是否到了任务要执行的时间了,如果到了那么poll操作,否则需要当前线程等待一定的时间间隔重复这个过程。