1.序
位于 java.util.concurrent 下的 ThreadPoolExecutor
本文围绕线程池的开启、工作、关闭三个方向进行探究,去追溯源码的实现。
2.线程池怎么开启的
ThreadPoolExecutor 有四个构造方法,如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
实际上前三个构造方法最终都是调用最后一个方法,然后前三个方法中没有的构造参数,在调用时候给了一个默认值。
可通过如下代码开启一个线程器并提交一个任务到池中:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
threadPoolExecutor.execute(()->{
// todo
});
那么问题来了,调用 execute() 方法后,线程池是怎么添加线程的呢?
execute() 的源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 当前线程数是否大于核心线程数
if (addWorker(command, true)) // 开启线程
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {// 如果线程池处于running并且添加任务到等待队列成功
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command)) // 如果线程池处于非running并且移除任务成功,那么驳回任务
reject(command);
else if (workerCountOf(recheck) == 0) // 如果工作线程数为零,那么添加一个null的任务,以防新的任务进来却没有线程可以去工作
addWorker(null, false);
} else if (!addWorker(command, false)) // 如果添加非核心线程时失败,那么驳回任务
reject(command);
}
execute() 方法的工作是先判断线程数是否大于核心线程数,小于就调用 addWorker() 方法添加核心线程,否则就添加到任务队列等待。在添加任务到队列采用 offer() 方法,如果返回 false ,那么就立即再次调用 addWorker() 方法添加一个非核心线程。
这时再来看 addWorker() 方法,它是怎么添加一个线程的呢?
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
// 检验线程池...
}
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 {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
workers.add(w); // 把线程添加到HashSet中保存
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); // 开启线程
workerStarted = true;
}
}
} finally {
// ...
}
return workerStarted;
}
查看 addWorker() 方法,不难发现线程池的是把咱们的线程类封装成了一个任务,放到自己的 Worker 类中执行。
至此一个线程就开启成功了。
3.线程池怎么工作的
线程池是如何工作的呢?
那么再去查看 Worker 这个类的 run() ,Worker 是线程池的任务执行类,实现了 Runnable 接口。
Worker 类的 run 方法调用的是 runWorker() 这个方法。
final void runWorker(Worker w) {
// ...
Runnable task = w.firstTask; // 获取咱们提交的任务
// ...
try {
while (task != null || (task = getTask()) != null) { // 获取任务去执行
// ...
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 {
// ...
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
runWorker() 先拿到当前线程的任务,调用 task.run() 来执行我们在业务侧提交的任务。处理完毕后,再次去等待队列中提取任务进行处理,如果没有任务那么终止这个 while 死循环,最后该线程结束。
可问题来了,线程池的核心线程数是永久活跃的,那么这个 while() 循环就不能退出,否则该核心线程就结束了。
这时带着这个问题去看下循环条件中的 getTask() 这个方法,
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
// ...
int wc = workerCountOf(c); // 工作线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // true-工作线程数>核心线程或者允许超时
// ...
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // 如果允许超时,那么在 keepAliveTime 内没有 poll 获取数据则返回 null ,否则使用 take 方法会一直阻塞,直到获取新的数据。
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
关键看 allowCoreThreadTimeOut 这个参数,源码中的注释:
如果为false(默认值),核心线程即使在空闲时也保持活动。
如果为真,核心线程使用keepAliveTime超时等待工作。
在默认情况下,allowCoreThreadTimeOut 是 false ,即不允许超时。再结合从队列获取任务的代码,对线程是怎么一直保持活跃的就十分清晰了。如果工作线程数大于核心线程数,那么在 keepAliveTime 中没有拉取到任务的话,就会返回一个 null ,这个 null 会使 runWorker() 的while循环终止,那么该非核心线程就运行完毕结束了。而如果工作线程数小于核心线程数,那么会使用 take() 方法一直阻塞的去拉取任务,直到新的任务添加进来,然后返回 runWorker() 去处理任务。
4.线程是怎么关闭的
ThreadPoolExecutor 为我们提供两种方式去关闭线程池:
shutdown() | 启动一个先前提交的有序关闭执行任务,但不接受新的任务。如果已经关闭,则调用没有额外的效果。 |
shutdownNow() | 试图停止所有正在执行的任务,将停止处理等待中的任务,并返回任务列表等待处决的人这些任务被耗尽(删除)从此方法返回时从任务队列中返回。 |
shutdown() 调用后不再接受新的任务,并在当前任务执行完成后关闭线程池。
shutdownNow() 调用后不再接受新的任务,并将当前等待队列中的任务返回给调用者,然后试图停止所有正在执行的任务,对于那种会抛出 InterruptedException 异常的方法会立即抛出异常。
如果需要检测线程池是否真正关闭,使用 awaitTermination()
两个方法都是通过
t.interrupt();
使线程中断,不过的是 shutdown 会在调用去获取该线程的锁(任务执行任务时持有锁),而 shutdownNow 却不会。
t.interrupt(); 让可以抛出 InterruptedException 异常的线程立即抛出异常,如 sleep() ,take() 方法。
看线程池调用停止方法后,工作线程如何处理的:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
//...
/**
* 把线程数减一,并且返回null,null会是上一层方法的while循环结束,从而结束线程
* 进入该 if 的条件如下:
* 1. 线程状态为 STOP
* 2. 线程状态为 SHUTDOWN 且任务队列没有任务了
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//...
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // take() 方法会在线程调用 interrupt() 方法是抛出异常,即该核心线程不在此阻塞等待任务。
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
两个方法的区别:
1.shutdown() 设置线程池的状态为 SHUTDOWN,shutdownNow() 设置线程池的状态为 STOP
2.shutdown() 什么都不返回。shutdownNow() 会把所有任务队列中的任务取出来,返回一个任务列表。
3.shutdown() 会执行完等待队列中的任务,shutdownNow() 不会。
5.总结
1.业务方提交线程
2.线程池收到任务,判断当前工作线程是否小于核心线程?是-创建新的线程,否-添加任务到等待队列
3.任务队列是否已满?是-创建新的线程去处理任务,否-添加任务到等待队列
4.当前工作线程是否大于最大线程数?是-调用线程池拒绝策略拒绝该任务,否-创建新的线程处理任务
5.核心线程处理完任务,调用 take() 方法阻塞拉取任务。非核心线程在 leepAliveTime 内拉取不到任务即结束工作。
6.任务全部处理完毕,关闭线程池。
6.常见问题
1.线程池一创建就自动创建相应的核心线程数吗?
答:不会。只有在提交线程的时候才会去创建。
2.为什么核心线程5个,最大线程10个,可提交6个线程却只有5个线程在工作?
答:只有当等待队列满了,才会去创建额外的线程去工作。
3.调用了 shutDownNow() 方法,为什么线程池却迟迟不关闭?
答:调用该方法后,仅仅是把线程池的状态设置为 STOP,并为所有线程执行 interrupt() 方法,如果该线程处理的任务中没有抛出 InterruptedException 异常的方法或者手动捕获了 InterruptedException 异常,那么该线程会继续运行直到结束。