文章目录
使用场景
在日常编码下,有些情况我们可以使用线程池来提高我们的吞吐量和缩短处理时间。
原理是,在现在我们的处理器一般都是多核,可以通过将任务分配到不同的核上执行达到提高请求的吞吐量和缩短整体的访问时间。
例如:我们在一些请求中,可能有些字段需要访问A,B,C服务来获取,最后组装完毕返回到前端。
这里我们就可以使用线程池来缩短整体的时间
源码实现
线程池
线程池中主要分为三大块:
- 第一当然是自身状态的管理
- 第二就是执行线程的管理
- 第三对于用户提交的任务的管理
首先我们需要来看下线程池自身状态或者说是生命周期的一个变化,不管是在生活中或者工作中,我们只有在对应状态下才能做对应的事情。
比如 饭店只有在开店状态才能进行下单,当饭店在歇业状态就不能下单,如果在歇业状态还能下单那就会引发一系列不利于社会稳定的问题
生命周期
// 状态控制值
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//规定任务数量的上限位数
private static final int COUNT_BITS = Integer.SIZE - 3;
//可以快速获得线程数
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
/**
* COUNT_MASK 00011111111111111111111111111111
*
* RUNNING[-536870912]: 11100000000000000000000000000000
* SHUTDOWN[0]: 00000000000000000000000000000000
* STOP[536870912]: 00100000000000000000000000000000
* TIDYING[1073741824]: 01000000000000000000000000000000
* TERMINATED[1610612736]: 01100000000000000000000000000000
*
**/
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;
每个状态值得大小比较,需要各位同学稍微记一下,后续很多源码都是通过大小于的比较来判断的,大小关系搞清楚了就会更容易理解:
TERMINATED > TIDYING > STOP > SHUTDOWN[0] > RUNNING
终点最大,运行最小,正数都为停止相关的状态(包括0),运行都是负数
在ThreadPoolExecutor中,使用ctl
这个变量表达线程池当前的状态和执行线程的数量,int
总共是32位(4字节),其中最高的3位被用来表示当前的状态,剩下的29位用来表示执行线程的数量。
问:为什么会使用一个字段来存储两种数据?
答:因为有些情况我们不只是要判断当前状态,可能还需要判断执行线程的数量,在一个字段中就有了一次比较两种情况的可能。 例如 小于零的ctl
处于RUNNING状态并且可能有线程数
例如:
RUNNING[-536870912]: 11100000000000000000000000000000 处于RUNNING状态无执行线程
RUNNING[-536870913]: 11100000000000000000000000000001 处于RUNNING状态有1个执行线程
接下来看看这些状态都有什么用,先按照我们自己的分析,自己尝试定义其状态,那么线程池至少有3种状态:
- 运行中
- 停止中
- 终止
当容器运行时,我们需要有一个状态与之对应,因为我们需要知道什么时候可以往容器中新增任务。
停止中也需要,因为代表了容器正在逐步释放资源和处理收尾工作,并且不在接收新的任务(资源的释放和收尾都需要一定的时间,所以需要有状态来表示)
同理终止状态也需要,因为我们需要知道容器是否完全关闭了好做后续处理(代表容器使用得资源全部释放)
RUNNING
代表运行中,可以提交任务
实例创建时默认就是RUNNING
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctlof(int rs, int wc)
帮助使用者快速构建ctl
值,rs
表示状态 wc
表示当前执行线程数
SHUTDOWN
表示停止中,容器中任务将会在执行完,不能提交任务。
当我们调用shutdown()
方法时
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查是否有访问权限
checkShutdownAccess();
//更改状态值为SHUTDOWN
advanceRunState(SHUTDOWN);
//尝试终止空闲的执行线程,并不意味着执行线程必须停止
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//尝试变更状态为 TERMINATED
tryTerminate();
}
private void advanceRunState(int targetState) {
// assert targetState == SHUTDOWN || targetState == STOP;
for (; ; ) {
int c = ctl.get();
// 如果当前状态>= SHUTDOWN 状态 (意味着已经在后续状态了,则break)
// 或者 状态更新为SHUTDOWN 状态,并设置现在的执行线程数成功
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
当调用shutdown()
方法时会将状态变更为SHUTDOWN
TERMINATED
代表已终结,已经走到了终点。
final void tryTerminate() {
for (; ; ) {
int c = ctl.get();
/**
* RUNNING 无法终结
* 当前状态>= TIDYING无法终结
* 当前状态< STOP并且任务没处理完无法终结
*
* STOP并且执行线程为0可以终结,TERMINATED,任务队列为空并且执行线程为0可以终结
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateLessThan(c, STOP) && !workQueue.isEmpty()))
return;
//工作线程数不为0,尝试进行空闲任务的终止
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//尝试将状态变更为TIDYING,并把工作线程数设置为0
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
//将状态强制设置为TERMINATED,并把工作线程数设置为0
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
ok,我们先自己来梳理下流程,形成一个大体的印象。
问:首先
shutdown()
对于执行时任务队列中如果存在Task待处理是否能中断线程?
答:分情况,有些业务场景需要抛弃掉等待或正在执行的任务,那么就需要直接中断所有线程。
而有些情况,我们需要将已经进入线程池的任务给执行完。
所以提供了两个方法,shutdown()
该方法就会尝试打断空闲的线程。shutdownNow()
该方法会直接打断所有执行线程,将会导致任务丢失。
而针对与这两种情况,一种停止中,依旧有线程在运行。一种停止,所有线程都被打断了,就分配了两个状态来与之对应 SHUTDOWN 和 STOP。
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;
}
接下来调整下刚才我们理解的流程
到此为止我们已经理清楚了5种状态种的4种,还差最后一个状态TIDYING
就讲整个状态存在的比较性给理解的差不多了。
TIDYING
表示处理工作已完成等待收尾,该处理的任务和工作线程已经处理完毕
问:那为什么不能直接变成TERMINATED?
答:因为有个hook,也就是terminated()方法,允许继承者进行自定义,并且只能执行一次。
为了保证其只能执行一次,那么就需要TIDYING来表示,该方法已经执行过了。
final void tryTerminate() {
...
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//尝试将状态变更为TIDYING,并把工作线程数设置为0
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//允许继承者自己实现
terminated();
} finally {
//将状态强制设置为TERMINATED,并把工作线程数设置为0,防止扩展点terminated()人为的变更容器状态等错误操作。
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
根据源码可以知道,当变更为TIDYING就代表着已经收尾了,就等hook执行完毕以后关闭容器。
如果没有TIDYING状态,那么外层就不清楚现在的状态。
如果是STOP可能正在等所有线程都中断完毕,也可能是都终止完了正在执行hook方法。
如果是SHUTDOWN可能正在队列中的任务都处理完毕,也可能是正在执行hook方法。
那么hook的调用次数就没法确定,所以需要TIDYING代表hook已执行。
呼~,最后在来调整一次我们理解的流程
大功告成,整个ThreadPoolExecutor容器的状态定义以及为什么存在我们已经理解完了,接下来就是深入任务创建和执行的相关源码了,继续前进。
线程池配置
首先我们先看下一个线程池需要关心的是那些参数
//拒绝策略
private volatile RejectedExecutionHandler handler;
//最长存活时间
private volatile long keepAliveTime;
//是否运行核心线程有超时时长,true:会关闭核心线程
private volatile boolean allowCoreThreadTimeOut;
//核心线程数量
private volatile int corePoolSize;
//最大线程数量
private volatile int maximumPoolSize;
//任务队列
private final BlockingQueue<Runnable> workQueue;
这些参数将在RUNNING状态下,对于不同的设置产生不同的效果,接下来我们就带着这些参数来看下它的效果是什么。
任务提交
execute(Runnable command)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//corePoolSize,如果没达到核心线程数则添加核心执行线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//运行中,并且workQueue队列没满,则进入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//doubleCheck,执行到该处,其他线程将容器状态为STOP或其它停止状态,
//并且将所有工作线程都停止了,就会导致该任务提交成功在队列中无执行线程去执行的问题。
if (!isRunning(recheck) && remove(command))
//拒绝
reject(command);
//如果remove(command)没有移除成功,并且现在线程都没关闭了,那么需要创建一个
else if (workerCountOf(recheck) == 0)
//任务设置为null,让worker从任务队列里面去获取,如果这里直接设置command,会导致任务执行两次
addWorker(null, false);
}
//添加普通执行线程
else if (!addWorker(command, false))
//添加失败则拒绝
reject(command);
}
在来梳理下流程
提问时间到 ^ _ ^
问:任务是否遵循FIFO(先进先执行)?
答:第一取决于workQueue的实现类,第二当核心线程和队列都满的情况下,并且maximumPoolSize>当前线程数>corePoolSize 时,后续任务会比队列中先执行。
简单理解addWorker(Runnable firstTask, boolean core)
方法 第一个参数firstTask不为null则是立即执行
例如:addWorker(command, ...)
这种就是立即执行,addWorker(null, ...)
这种就是获取任务队列中的任务来进行执行
好了,可以回过头去看看代码里面哪里是null,哪里是非null
问:判断核心线程数是否满足的地方并没有判断容器状态,不会出问题吗?
答:容器当前状态的判断在addWorker()
方法中,并不是调用了addWorker()
方法就一定会创建线程
接下来我么就来看看addWorker()
方法的相关源码
addWorker(Runnable firstTask, boolean core)
因为addWorker()
源码比较长,导致页面也变得很长,这里我将它拆分为两部分。
第一部分:状态的判断与修改
第二部分:任务的投放与执行
- 状态的判断与修改
private boolean addWorker(Runnable firstTask, boolean core) {
//第一部分:状态的判断与修改
retry:
for (int c = ctl.get(); ; ) {
// Check if queue empty only if necessary.
//当前状态>=SHUTDOWN 并且 (当前状态>=STOP,或者任务立即执行,或者任务队列为空) 那么就不执行任务
//说人话,如果是SHUTDOWN,那么新增任务拒绝执行,队列里面的任务继续执行
//如果是STOP,那么不管是哪的都不能执行,执行线程必须里面关闭
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (; ; ) {
//如果是创建核心线程,并且超过了corePoolSize,那么就不能继续创建
//如果是普通线程,并且超过了maximumPoolSize,那么就不能继续创建
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
//创建线程后,将ctl中的线程数加1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//当前状态>=SHUTDOWN,则说明有其他线程在添加,则重新在检查一次
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
...
}
前面主要是判断状态与修改状态,我就写在代码注释里面了,就不过多描述了。
2. 任务的投放与执行
继续按照注释来
private boolean addWorker(Runnable firstTask, boolean core) {
...
//第二部分 任务的投放
//表示任务是否已启动
boolean workerStarted = false;
//表示任务是否已添加
boolean workerAdded = false;
Worker w = null;
try {
//包装成Worker线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
//容器RUNNING 或者 当前状态<STOP并且不是直接执行的任务而是从任务队列中获取的任务
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
//如果线程被中断 那么就抛出异常(可以在重新看下SHUTDOWN和STOP状态的流程)
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
//将工作线程添加到执行中的线程列表里面
workers.add(w);
workerAdded = true;
int s = workers.size();
//设置从启动开始时,最大的历史线程数
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//启动线程
t.start();
workerStarted = true;
}
}
} finally {
//如果任务没启动成功,那么就需要把线程数和任务移除,并检查是否实容器关闭了
if (!workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
到此,容器的任务添加部分代码就完毕了,后面就是看worker线程
是如何执行任务的。
等等,差点忘记了问题时间 ^ _ ^
问:corePoolSize,maximumPoolSize我们设置了,那么我们如何来区分某个worker是核心线程还是非核心线程?
答:区分不了,在整个容器中,只能保证线程核心数,但是不能保证某个线程就一直是核心线程
例如:corePoolSize=10,maximumPoolSize=20,那么前10个启动的线程不一定不会被关闭,整个容器会保证当核心线程数超过10个后,会按照配置来让核心线程保持在10,但这10个可能是最开始创建的也可能是后面创建的,动态变更而不是静态的。
简单理解,当前线程=15,随机抽取5个幸运线程关闭掉。
问:keepAliveTime ,allowCoreThreadTimeOut参数在哪产生作用的,怎么控制什么时候停止普通线程
答:后续从任务队列中获取任务的时候就会涉及到
接下来我们就深入上面所提的问题的源码来看一看,继续出发!
执行线程
在Worker
线程中,运行起来的run方法就是runWorker()
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();
//(当前状态>=STOP 或者 线程被中断并且当前状态>=STOP) && (当前线程没被中断)
//那就中断自己,保证线程比被中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//before hook
beforeExecute(wt, task);
try {
task.run();
//after hook
afterExecute(task, null);
} catch (Throwable ex) {
//after exception hook
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
//当前线程任务执行总数
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
//如果没提交任务并且任务队列中也获取不到任务,那么就移除当前的线程
processWorkerExit(w, completedAbruptly);
}
}
问:最后移除线程的时候没有怎么知道是不是核心线程啊?万一把核心线程移除了呢?
答:上上个问题已经说了,线程不区分核不核心,只保证数量是corePoolSize设置的数量
finally {
//如果没提交任务并且任务队列中也获取不到任务,那么就移除当前的线程
processWorkerExit(w, completedAbruptly);
}
接下来就是最后一块内容了,坚持就是胜利。 ヾ( ̄ー ̄)X(▽)ゞ
任务队列
getTask()
方法,去队列中申请任务。
简单理解:该方法中返回null
则该worker会被移除,并且会进行容器关闭检测(因为任务获取不到会对两个地方产生影响,第一个就是核心线程数,第二个就是容器处于SHUTDOWN状态)
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (; ; ) {
int c = ctl.get();
// Check if queue empty only if necessary.
//当前状态>=SHUTDOWN 并且 (当前状态>=STOP or 工作队列为空)
//返回null 移除线程
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
//是否允许核心线程关闭 或者 当前线程数大于了核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//(线程数大于了最大线程数 或者 已超时)并且 (线程数不为0 或者 任务队列为空)
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//任务数量减一
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//是否有过期设置,如果没有一直take到有任务为止,如果有则poll到keepAliveTime时间
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
到此ThreadPoolExecutor的源码就分析完毕了,其底层是通过实现AQS来实现Worker是执行还是空闲。