线程池是什么
线程是 Java 程序执行的最小单位。线程池就是管理一系列线程的资源池,提供了管理线程并且基于资源池内的线程执行多任务的功能。
为什么用线程池
在多任务的场景下,为了提升执行速度,通常会采取多线程并行处理的方式。这种情况下,线程的数量可能会膨胀到很夸张的地步,又或者说出现线程频繁创建销毁的情况。
而系统所能提供的资源是有限的,特别是在移动端。线程的创建销毁和调度都会占用系统资源。使用线程池可以维护多个线程执行任务,并且对线程进行复用,从而降低资源消耗,提高响应速度。
常用的线程池类型
JDK 中提供了一些常用的线程池类型,我们可以根据不同的应用场景调用 Executors 的静态方法构建它们。以下所列的线程池类型都是通过 ThreadPoolExecutor 构建的,只是传入的参数不同。当然也有其他用不同类构建的线程池,这里暂时不展开介绍,先了解经典的 ThreadPoolExecutor。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()
);
}
没有核心线程,但可以拥有近似不限数量的非核心线程。任务到来时,如果没有可用线程,会创建新的线程执行;如果有可用线程,则重用该线程执行。线程在空闲一段时间后,会自动终止。
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
只拥有固定数量的核心线程。当所有核心线程都在工作的时候,新的任务会放到一个默认容量为 Integer.MAX_VALUE 的队列中等待执行。
SingleThreadExecutor
SingleThreadExecutor 跟 FixedThreadPool 类似,只拥有一个核心线程。
ThreadPoolExecutor
以上可见,常用的线程池都是通过 ThreadPoolExecutor 构造的,当然开发者也可以按需构造 ThreadPoolExecutor 。
其实线程池的实现类不止 ThreadPoolExecutor ,但由于其使用比较广泛,是应该优先学习的。接下来将会介绍 ThreadPoolExecutor ,让读者对其有一个概念。
ThreadPoolExecutor 的构造
以下是 ThreadPoolExecutor 比较有代表性的构造函数及其参数:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
) {
...
}
| 参数 | 含义 |
|---|---|
corePoolSize | 核心线程数量。默认配置下,核心线程空闲时不会因为超时而退出,核心线程的存在能提高线程池的响应速度。 |
maximumPoolSize | 最大线程数量,包括核心线程数的总线程数。核心线程数以外、最大线程数以内的线程并不是常驻的,这部分线程在空闲时有一个最大存活时间。 |
keepAliveTime | 核心线程数以外的空闲线程最大存活时间。默认配置下只作用于非核心线程;如果开启 allowCoreThreadTimeOut,核心线程也会受影响。 |
unit | keepAliveTime 的时间单位。 |
workQueue | 任务队列。当前线程数达到核心线程数后,任务会尝试加入这个队列。 |
threadFactory | 构建线程的工厂类对象。 |
handler | 线程池拒绝执行任务时触发的回调。 |
线程池的状态
可以理解成线程池的生命周期,不同的状态线程池会有不同的表现,读者可以先有一个概念:
| 状态 | 接受新任务 | 处理队列任务 | 说明 |
|---|---|---|---|
RUNNING | 是 | 是 | 正常运行状态。 |
SHUTDOWN | 否 | 是 | 调用 shutdown 后进入该状态,不接受新任务,但会继续处理队列中剩余的任务和正在执行的任务。 |
STOP | 否 | 否 | 调用 shutdownNow 后进入该状态,不继续处理队列任务,并且尝试中断正在执行的任务。 |
TIDYING | 否 | 否 | 线程池达到终止条件后进入该状态,会触发 terminated 方法,让用户做一些收尾工作。 |
TERMINATED | 否 | 否 | terminated 方法执行完成后进入该状态。 |
Worker
Worker 是线程池的一个内部类。线程在线程池中的形式就是 Worker。它有以下特点:
每个 Worker 都会持有一个线程对象
Worker 在构造时,会通过 ThreadFactory 创建线程对象并持有。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
Worker 线程的运行
通过 ThreadFactory 创建线程对象时,会将 this 传入到 ThreadFactory 的 newThread 中,以下是 ThreadFactory 的默认实现类 DefaultThreadFactory 的 newThread 方法。
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
可以看到传入的参数是一个 Runnable 对象,并且传入了 Thread 的构造,而 Worker 本身就是 Runnable ,所以可以推断,当 Worker 中的线程被开启时,会执行 Worker 的 run 方法。
再来看一下 Worker 的 run 方法:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable {
public void run() {
runWorker(this);
}
}
可以看到 run 方法执行了 runWorker 方法。
本章小结: 线程池开启线程最终执行的是线程池的 runWorker 方法。
Worker 与任务执行
execute:任务提交流程
execute 是线程池执行任务的入口,需要传入一个任务:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 1
int c = ctl.get();
// 2
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 3
if (isRunning(c) && workQueue.offer(command)) {
// 3.1
int recheck = ctl.get();
// 3.2
if (! isRunning(recheck) && remove(command))
reject(command);
// 3.3
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 4
else if (!addWorker(command, false))
reject(command);
}
流程说明:
1:获取一个 int 值 c。这个 c 包含了两种数据:第一种是 Worker 的数量,第二种是线程池的状态。在需要时,对 ctl 进行位运算就可以取出相应的数据。
2:获取 Worker 的数量。如果 Worker 数量小于核心线程数,则通过 addWorker 添加 Worker 去执行 command 所代表的任务。
addWorker 的第二个参数是限定 Worker 的数量边界,后面讲解 addWorker 的时候会介绍,可以理解为是否开启一个核心 Worker。addWorker 会返回代表添加成功的布尔值:如果返回 true,表示添加成功,流程结束;如果返回 false,表示添加失败,会重新获取 c。
3:从 c 获取状态。如果状态是运行中,则尝试将任务加入到任务队列。如果加入成功,则继续执行以下逻辑。
3.1:重新获取 ctl,因为此时线程池的状态可能发生了变化。
3.2:如果此时不在运行中,则从任务队列中移除这个任务。如果移除成功,则执行拒绝策略回调。
3.3:来到这里有两种情况:一是状态仍然为运行中,二是从任务队列移除失败。这两种情况都会判断 Worker 数是否为 0。如果为 0,则创建一个不携带任务的 Worker,让它从队列中获取任务执行。为什么是不携带任务,因为这个任务已经被加入到任务队列了却没有 Worker 处理,不管是刚刚列出的情况一还是情况二都需要一个 Worker 去处理这个任务,而 Worker 本身不需要携带任务,只需要从队列取出任务执行就好了。
4:来到这里有两种情况:一是状态不是运行中,二是加入任务队列失败。如果是第一种情况,addWorker 会返回 false,因为线程池不在运行中,不会接受新的任务。第二种情况一般是队列已经满了,所以会尝试开启一个核心线程数以外的 Worker 并携带这个任务去执行。
本章小结: execute 的流程可以概括为先补核心线程,再尝试入队,入队失败后再尝试增加线程执行任务。
addWorker:增加 Worker
execute 的主要任务就是执行 addWorker ,addWorker 顾名思义,就是新增一个 Worker。它有两个参数:
| 参数 | 含义 |
|---|---|
firstTask | 新创建的 Worker 首先要执行的任务。它可以是 execute 传入的新任务,也可以为空。 |
core | 是否以核心线程数作为边界条件。如果是 false,则以最大线程数作为边界条件。 |
private boolean addWorker(Runnable firstTask, boolean core) {
// 1
retry:
for (int c = ctl.get();;) {
// 1.1
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
// 1.2
for (;;) {
// 1.2.1
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
// 1.2.2
if (compareAndIncrementWorkerCount(c))
break retry;
// 1.2.3
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 2
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;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int c = ctl.get();
// 2.1
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
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;
}
流程说明:
1:首先进入一个双重 for 循环,主要作用是把控线程池状态与 Worker 数量的正确性,好执行下一步操作。线程池是支持异步的,ctl 随时都有可能变化,所以代码中有很多类似的判断逻辑。
1.1:想要通过这个 if return,首先线程池状态是 RUNNING 肯定能过。另外一种能过的情况是:线程池状态为 SHUTDOWN,并且 firstTask 为空,并且任务队列不为空。
为什么这种情况可以通过?因为 SHUTDOWN 状态可以继续执行任务队列里的任务,但是不能接受新的任务。所以当新创建的 Worker 没有携带任务,并且任务队列也有任务待执行时,这个 Worker 是被允许创建的。
1.2:内层 for 循环是为了确保可以顺利使 ctl 中的 Worker 数量增加 1。
1.2.1:第一道关卡,确保增加 Worker 后数量在边界条件以内。如果超出边界条件,返回 false,创建失败。
1.2.2:第二道关卡,compareAndIncrementWorkerCount 会对 ctl 中的 Worker 数量加 1,也就是会对 ctl 的值进行修改。如果修改成功,则跳出外层循环,走接下来的逻辑。
如果此时有其他线程也同时修改了 ctl,则 ctl 最新的值就不是 c 了,compareAndIncrementWorkerCount 会返回 false 并走到 1.2.3。
1.2.3:来到这里有个前提条件,那就是 ctl 发生了变化,但是并不确定是线程池状态发生了变化还是 Worker 数量发生了变化。如果有可能是前者,则 continue retry 重新进入外层循环重新检验线程池状态。为什么是有可能,因为 runStateAtLeast(c, SHUTDOWN) 返回 true 不一定就是状态变了,它有两种情况,第一种是从 RUNNING 切换到了 SHUTDOWN,第二种是一直在 SHUTDOWN,第一种情况是一定要重走外层循环的,那第二种情况既然线程池状态没变,那变得肯定是 Worker 数量,也就是说有可能其他线程给线程池增加了 Worker 并且处理了任务队列的任务,此时当前的 Worker 就没必要创建了,为了预防这种情况,也需要重新进入外层循环走一遍准入的逻辑。如果单纯只是 Worker 数量发生了变化,线程池状态一直是 RUNNING,那只需要重走内层的循环重新尝试增加 Worker 数量就行。
2:这里开始创建 Worker。先新建 Worker 对象并取出内部的 Thread,然后使用全局锁 mainLock 进行同步,再次获取最新的 ctl 并在 2.1 中校验状态。
状态没问题后,把新建的 Worker 加入集合,释放锁,之后调用 Worker 内部 Thread 的 start 方法开启线程。如果以上过程成功开启线程则返回 true,否则返回 false。
本章小结: addWorker 的流程可以概括为:在状态允许且在数量边界内创建 Worker 并且返回创建结果。
runWorker:Worker 的运行
Worker 中的线程运行后,最终会执行线程池的 runWorker 方法,参数是线程对应的 Worker。
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();
// 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
// 1
if (
(runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted()
) {
wt.interrupt();
}
try {
beforeExecute(wt, task);
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
runWorker 的逻辑比较直接:开启一个循环,先执行自身携带的任务,再通过 getTask 不断获取任务执行。要注意的是,如果通过 getTask 获取到的任务为空,则会终止循环,也同样意味着这个 Worker 已经结束运行。
执行过程中会以自身为锁进行同步。这个锁主要用于标记 Worker 是否是空闲中,以便在需要的时候中断线程。
重点看 1 处的条件判断。想要进入这个 if,其实会有两条分支。
分支 1:
runStateAtLeast(ctl.get(), STOP) && !wt.isInterrupted()
线程池状态至少为 STOP,并且线程还没被中断,则中断线程。这个很好理解,至少为 STOP 表示不接受新的任务与任务队列的任务,并中断正在执行的任务。
分支 2:
线程池状态小于 STOP
&& Thread.interrupted()
&& runStateAtLeast(ctl.get(), STOP)
&& !wt.isInterrupted()
这里通过 && 可以分割出 A、B、C、D 四个条件。
条件 A 不多做解释。
条件 B 是判断线程是否被中断过,这是有可能出现的,因为线程池并不完全掌控所有工作线程,这些线程是有可能被外部中断的。
条件 C 再次判断线程池状态是否为 STOP 以上。如果为 false,则分支 2 结束,因为状态小于 STOP,不应该中断线程。注意 Thread.interrupted() 会清除线程的中断状态,所以如果线程池状态没有到 STOP,相当于取消了线程的意外中断。
既然 C 为 true,则已经满足了中断线程的前置条件,条件 D 只是在 wt.interrupt() 之前再做一层保险,前面说了线程池并不完全掌控所有工作线程,如果此时线程已经被中断了,就没有必要再在这里中断一次了,重复中断甚至有可能抛异常。
分支 2 可以总结为:如果线程池的状态需要线程中断,则让线程进入或保持中断的状态,如果否,则将线程恢复到非中断的状态。
本章小结: runWorker 负责持续取任务、执行任务,并且在每次执行任务前处理线程的中断状态。
getTask:获取执行任务
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// 1
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 2
int wc = workerCountOf(c);
// 3
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 4
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
// 5
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
流程说明:
1:检查状态。这里有两种情况会返回 null 去结束 Worker。
- 线程池状态为
STOP。 - 线程池状态为
SHUTDOWN,并且任务队列为空。
2:获取当前的 Worker 数。
3:timed 的意思是当前 Worker 是否会等待任务超时。这里有两种情况会让 timed 为 true。
allowCoreThreadTimeOut为true,表示核心Worker会等待任务超时,同时也意味着线程池的所有Worker都是会超时的。- 核心线程数以内的
Worker不会等待任务超时,但是总的Worker数已经超过了核心线程数,意味着当前调用getTask的Worker会等待任务超时。
4:这里是决定当前 Worker 是否可关闭的核心判断。以下是判断条件:
| 条件 | 含义 | ||
|---|---|---|---|
| `wc > maximumPoolSize | (timed && timedOut)` | Worker 数量大于设定的最大 Worker 数量,或者当前 Worker 已经超时。 | |
| `wc > 1 | workQueue.isEmpty()` | 当前 Worker 数量大于 1,或者任务队列已经空了。 |
条件 A 满足后,当前 Worker 已经是可以考虑关闭了,但同时也要满足条件 B,因为如果当前 Worker 关闭了,任务队列又有任务,会出现任务无 Worker 处理的情况。
如果 A、B 都满足,则可以关闭当前 Worker。
此时会使用 CAS 将 ctl 中的 Worker 数量减一:
- 如果 CAS 成功,返回一个
null给调用处,结束Worker的运行。 - 如果 CAS 失败,有可能线程池状态或者 Worker 数量出现了变化,需要重新进入循环走状态判断。
5:从任务队列取任务。如果队列中有任务,会立刻返回;如果没有,则根据 timed 决定等待方式。
poll:等待任务一段时间,如果超过时间,则返回null。take:一直等待任务,直到队列中存在任务。
接下来如果得到的任务为空,表示已超时,将 timedOut 赋值为 true,并进入下一轮循环。注意 timedOut 为 true 不一定就马上结束当前 Worker,还要重新走以上逻辑,直到满足结束条件才会真正结束 Worker。
本章小结: getTask 决定了 Worker 是继续等待任务,还是因为线程池状态、Worker 数量、等待超时等原因退出。
总结
ThreadPoolExecutor 的核心流程可以总结为:
- 提交任务时,优先补足核心
Worker。 - 核心
Worker已满后,优先把任务放入任务队列。 - 如果任务队列无法加入,再尝试创建核心线程数以外的
Worker。 - 如果创建失败,则执行拒绝策略。
Worker运行后,会不断通过getTask从队列获取任务,直到线程池状态或超时条件要求它退出。