ThreadPoolExecutor类图
从类图上来看 ThreadPoolExecutor 是Java中一个重要的基础的线程池实现类。
它下面可以从功能上细分几个类型线程池,FixedThreadPoolExecutor SingleThreadPoolExecutor CachedThreadPoolExecutor,
都是由 Executors 创建的。从下图来看基本都是不同的参数构造出来的 ThreadPoolExecutor,所以学习完这个类,便可理解大部分线程池工作原理。
通过读这源码,应该要理解:
- 1、线程池如何执行用户提交的任务。
- 2、线程池创建线程的工作流程。
- 3、线程池如何复用线程。
- 4、线程池如何保证空闲线程生存指定的时间后被销毁。
- 5、线程池销毁的工作流程。
- 6、动态修改corePoolSize
源码重要变量
ctl 是一个很重要的变量,它代表了线程池的状态:
RUNNING 运行(接收提交的新任务并处理工作队列的任务),
SHUTDOWN 停止(不接受新任务,终止空闲线程,但让正在工作的线程继续执行并处理完工作队列的任务),
STOP(shutdownNow) 立刻停止(不接受新任务,也不处理工作队列的任务,终止所有线程哪怕正在执行任务)
TIDYING 正在清洁(停止后,中断线程,释放资源的状态),
TERMINATED 终止。
ctl 可能是用不同的位代表不同的状态,因为有位运算,但是具体没去细看,后面可能会补充这方面。
RUNNING 状态 -1 在内存中是 1111 1111 左移29位后,第32、31、30位 111 ,后面29位都是0,所以RUNNING此时是一个很小的负数。
SHUTDOWN 是0的值。STOP是一个很大的正数,1<<29。TIDYING是一个更大的正数相当于 1 << 30。
TERMINATED 也是一个很大正数,相当于 0000 0011 << 29 = 0110 0000 ...... 00。
总结 TERMINATED > TIDYDING > STOP > SHUTDOWN > RUNNING,并且这些状态的标志位都是在32、31、30的高位上,低于30位的都是线程数量的标识位,因为每增加一个工作线程会用CAS给 ctl变量加一。所以在计算当前线程池工作线程数量时用 capacity & ctl = 0001 1111 ... 1111 & xxx0 000 ... 1000 (假设现在有8个线程,那么第4位就是1) xxx代表状态的标志位,所以capacity最高3位都是0,30位以下都是1,这样就能取到具体的工作线程数。
Worker是一个重要的内部类,是线程池工作线程的代表,内部持有一个线程对象.可以看到构造函数,传入一个Runnable对象作为这个工作线程的第一个任务,并且调用工厂创建一个线程。
这里还需要澄清2个概念:Thread是线程类,是工作单元。Runnable接口是工作任务,run()函数中封装了被线程执行的代码指令。当这个Worker被开启后,就由内部的这个Thread去执行 runWorker(this) 函数。
还有重要的变量如:工作队列、锁(用于添加线程时保证并发安全)、工作线程的容器、锁的等待队列。
largestPoolSize 是记录线程池从创建以来历史最大线程数。 0 < largestPoolSize <= maximumPoolSize,completedTaskCount 是记录总共完成的任务数。
keepAliveTime 是空闲线程的生存时间,在源码中体现是线程从工作队列获取元素时阻塞这么多的时间,当从阻塞中返回要么拿到任务去执行,没拿到就会销毁线程。
allowCoreThreadTimeOut 是否允许淘汰核心线程,ture的话,核心线程也会走上面的生存时间的逻辑。
corePoolSize 核心线程数,maximumPoolSize 最大线程数
以上变量都用 volatile 修饰,保证在代码执行时可获取到最新值
1、线程池如何执行用户提交的任务
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 拿到线程池当前的状态
int c = ctl.get();
// 从中提取工作线程数,判断是否达到用户设置的核心线程数量
if (workerCountOf(c) < corePoolSize) {
// 核心线程数未满的话,直接尝试添加一个线程去执行当前的 command 任务,成功添加后返回
if (addWorker(command true))
return;
// 添加线程不成功,可能是核心线程数已经满了,或线程池状态已经改变,需要从新获取状态
c = ctl.get();
}
// 走到这里代表要么线程池被 shutdown,要么核心线程数满,
// 如果线程池还在运行,尝试将任务加入工作队列
if (isRunning(c) && workQueue.offer(command)) {
// 就像作者在注释说的,加入工作队列后,再检查线程池工作状态,shutdown的话直接删除任务,
// 然后调用拒绝策略
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 线程池在 running,判断有没有存活的工作线程,没有的话,添加线程
// 这里加判断是因为防止进入方法后,有线程消亡,及时补充
else if (workerCountOf(recheck) == 0)
addWorker(null false);
}
// 工作队列满了,或者线程池shutdown了,
// 尝试添加线程,addWorker() 中会再次判断线程池状态。shutdown及以上的状态直接return false
// 如果状态是running且工作线程数量 < 预设的最大线程数,就会成功。
else if (!addWorker(command false))
// 失败后,调用拒绝策略handler
reject(command);
}
2、线程池创建线程的工作流程
上面介绍了 execute() 用户提交任务的函数,其中很重要的地方就是 addWorker() 函数,这个函数就是创建线程的工作流程。
boolean core 是指此次创建的线程是否核心线程。
private boolean addWorker(Runnable firstTask boolean core) {
// 定一个死循环,不断尝试增加工作线程的数量
retry:
for (;;) {
// 拿到工作状态
int c = ctl.get();
int rs = runStateOf(c);
// 每一次循环都检查运行状态,如果是shutdown及以上的状态(stop terminated)且firstTask不为空会返回false
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 拿到工作线程数量
int wc = workerCountOf(c);
// 如果工作线程数量 > 限制数量 返回失败
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 尝试 CAS 增加工作线程数量,成功则跳出死循环
// 这里不加锁,不怕并发的原因是,每个关键变量是volatile修饰的,并且只是做判断
// 而写操作用 CAS 保证原子性,这里就像乐观锁思想在操作。
if (compareAndIncrementWorkerCount(c))
break retry;
// CAS失败,获取工作状态
c = ctl.get(); // Re-read ctl
// 最新的工作状态和开始的不一样,从头开始判断
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 经历上面的状态判断,目前状态是合法的,并且工作线程数也增加了
// 下面要开始新增线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 这里就直接 new 了新线程,具体看 Worker 构造函数
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());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 如果加入成功后,就会开启线程了。线程就会开始执行用户提交的任务,或者拉取工作队列的任务。
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 最后如果线程没有开启,大概率是因为线程池shutdown了
if (! workerStarted)
// 尝试remove worker,减少工作线程数
addWorkerFailed(w);
}
return workerStarted;
}
这里补充线程是如何与worker关联起来的,构造函数拿到用户提交的工作任务,用工厂创建线程,
将 this 对象 即 worker(worker本身也是一个工作任务,因为实现了 runnable 接口)传进去。
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
查看 Thread 类本身构造函数,接收一个工作任务。
接下来是 Thread 本身的 run() ,内部直接调用 Runnable 的 run() 函数执行。回想一个类实现了 Runnable 复写 run() 后,也是需要一个Thread包装它,才能执行。所以更加明确了 Thread 和 Runnable Callable 的区别。一个是被cpu驱动的工作单元,一个是工作任务。
3、线程池如何复用线程
讲到复用线程就要看
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 不是很明白这里为什么要 unlock
w.unlock(); // allow interrupts
// 标志线程执行过程中是否被打断
boolean completedAbruptly = true;
try {
// 工作线程的余生将一直会在此循环中,初次会拿到task直接往下执行,往后每次getTask()执行
// 当工作队列返回空时,线程执行完 run() 就会消亡。
while (task != null || (task = getTask()) != null) {
// 这里上锁的原因是防止线程正在执行用户代码时被其它线程中断
// 因为shutdown()中有个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
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;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 做一些销毁线程的操作
processWorkerExit(w completedAbruptly);
}
}
4、线程池如何保证空闲线程生存指定的时间后被销毁
上面讲解了 runWorker() 函数,其中 getTask() 很重要,是线程执行完任务阻塞的主要地方。
总的来说,keepAliveTime 是靠工作队列的阻塞方法实现的。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 死循环尝试获取任务
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 检查线程池状态,如果被停止了,就减线程数,并返回空,消亡线程
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// 线程是否需要被淘汰,根据核心线程淘汰机制 或 当前线程数是否大于 核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
出现以下逻辑线程将被淘汰:
1、超过最大线程数限制
2、该线程超时等待了 且 (当前线程总数大于1 或 工作队列是空)
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果线程需要被淘汰就会阻塞指定时间获取任务,时间到后有任务就会进入到下一轮生存中
// 否则不超时淘汰就会 take() 一直阻塞到有任务来
Runnable r = timed ?
workQueue.poll(keepAliveTime TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 如果走到这里必定是超时了,下一轮循环会动态判断是否需要被淘汰
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
5、线程池销毁的工作流程
当一个线程从 runWorker() 的循环逻辑中走出来,最终执行这个方法的时候会做一些销毁流程。
根据线程消亡的原因不同,会有不同操作,如线程可能是因为用户的代码异常所消亡的,也可能是超时淘汰的。
private void processWorkerExit(Worker w boolean completedAbruptly) {
// 意外中断,减工作线程数
if (completedAbruptly) // If abrupt then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 统计这个线程完成的工作数,把它从容器中移除
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
// 中断一个空闲的线程,如果是 shutdown() 会中断所有空闲线程
// 尝试修改线程池状态,
// 从 shutdown 且 工作队列为空 -----> tidying
// 从 stop -----> tidying
// 将状态 cas 设置为 tidying 后,执行 terminated(),这个函数是空的,留给用户自己定义子类实现
// 执行完 terminated() 后,将状态 tidying ----> terminated
tryTerminate();
int c = ctl.get();
// 如果状态是 小于 stop,表明现在是 running 或 shutdown
if (runStateLessThan(c STOP)) {
// 意外中断的,可能超时等待的,如果没线程了或小于核心数量,就添加一个新的线程
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null false);
}
}
6、线程池的扩展,在每次线程工作前做点事情和工作完毕后做点后置处理
上面说了runWorker()中每次执行任务时会执行 beforeExecute(wt, task),执行完毕后会执行 afterExecute(task, thrown),
这是线程池留下的2个回调函数给用户子类自定义逻辑。有需要的自定义一个子类继承ThreadPoolExecutor 复写这2个方法就行。
7、动态修改corePoolSize和maximumPoolSize会有什么影响?
看源码和注释可知,如果新值比当前值要小,也就是现在不需要那么多核心线程,立即尝试中断所有空闲线程,
工作中的线程是lock状态,中断只是interrupt(),多余的线程将会执行完任务后被淘汰。
如果新值比当前值要大,当工作队列中还有任务就会添加线程直到达到新值的上限。
改maximumPoolSize,如果当前工作线程数超过了max数,立即尝试中断空闲线程,并且超出核心线程数的部分线程也会执行完当前任务后销毁。
PS:修改核心线程数和最大线程数都不影响当前正在执行的任务和线程。
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
// 尝试中断所有空闲的线程,工作中的线程是lock状态。这里的中断只是interrupt()标志位
interruptIdleWorkers();
else if (delta > 0) {
// We don't really know how many new threads are "needed".
// As a heuristic prestart enough new workers (up to new
// core size) to handle the current number of tasks in
// queue but stop if queue becomes empty while doing so.
// 如果工作队列还有任务就立即添加工作线程直到核心线程上线或工作任务数为0
int k = Math.min(delta workQueue.size());
while (k-- > 0 && addWorker(null true)) {
if (workQueue.isEmpty())
break;
}
}
}
public void setMaximumPoolSize(int maximumPoolSize) {
if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
throw new IllegalArgumentException();
this.maximumPoolSize = maximumPoolSize;
if (workerCountOf(ctl.get()) > maximumPoolSize)
// 如果当前工作线程数超过了max数,立即尝试中断空闲线程,并且超出核心线程数的部分线程也会执行完当前任务后销毁
interruptIdleWorkers();
}
补充知识:
线程 I/O 时间与 CPU 时间
至此我们又得到一个线程池个数的计算公式,假设服务器是单核的:
线程池大小 = (线程 I/O 阻塞时间 + 线程 CPU 时间 )/ 线程 CPU 时间
其中:线程 I/O 阻塞时间 + 线程 CPU 时间 = 平均请求处理时间。
比如:一个请求平均耗时 2000ms,其中cpu计算时间约为10ms,则 2000/10 = 200个线程数
如果是多核,则在单核计算结果 * CPU核心数
如何设计线程池大小,使得可以在1s内处理完20个Transaction?
计算过程很简单,先算出每个线程的tps,假设每个线程的处理能力为0.25TPS,那么要达到20TPS,显然需要20/0.25=80个线程。
这个理论上成立的,但是实际情况中,一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。