前面的文章已经详细分析了线程池的工作原理及其基本应用,接下来本文将从底层源码分析一下线程池的执行过程。在看源码的时候,首先带着以下两个问题去仔细阅读。一是线程池如何保证核心线程数不会被销毁,空闲线程数会被销毁的呢?二是核心线程和空闲线程的区别到底是什么?
首先,我们先来看一下以下两个示例,从代码示例走入底层源码,真正做到了如指掌。
1、示例分析
package cn.lspj.threadpool;
import java.text.SimpleDateFormat;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 从示例到底层源码分析 ThreadPoolExecutor 的执行过程
* <p>
* 1、核心线程和空闲线程的区别?
* 2、线程池如何保证核心线程不会销毁,空闲线程会被销毁呢?
* <p>
* 接下来带着这两个问题来仔细分析一下
*/
public class UseThreadPoolExecutor {
private static AtomicInteger i = new AtomicInteger();
private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
private static String getSystemTime() {
return sdf.format(System.currentTimeMillis());
}
/**
* 定义核心线程数为1,最大线程数为2,空闲线程存活3秒,队列长度为5
* 主线程提交6个任务,因为队列能够存放得下提交的所有任务,所以不会启用空闲线程来执行提交的任务,确切的来说这里永远只会有一个线程在工作。
* 因为队列能够存放得下提交的任务,所以不会启用空闲线程。
*/
private static void m1() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,
2,
3,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
(r) -> {
return new Thread(r, "t" + i.incrementAndGet());
});
for (int i = 0; i < 6; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSystemTime() + " 线程 " + Thread.currentThread().getName() + " 执行任务 " + finalI);
});
}
}
/**
* 一个很有意思的例子,定义核心线程数为0,最大线程数为10000,空闲线程存活5秒,队列长度为10000
* 主线程提交5个任务,讲道理队列能够存放得下提交的所有任务,不会启用空闲线程,线程池也不会执行任务。
* <p>
* 事实真的会如此吗???那肯定不会,如果不会执行那还扯个蛋呢。
*
* 那执行这个任务的线程是什么线程呢?并不是核心线程(因为核心线程数为0),线程池会启动一个空闲线程来执行任务。
*/
private static void m2() {
/**
* 核心线程和空闲线程的区别?
* 1、空闲线程可能会销毁,空闲时间达到 keepAliveTime,线程将被立刻销毁。
* 2、核心线程不会销毁,没有任务执行的时候会一直阻塞,除非调用线程池的 shutdown() 方法。
*
* 线程池是如何保证核心线程不会被销毁?空闲线程数为什么会销毁呢?接下来我们深入分析一下线程池源码底层实现。
*
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,
10000,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10000),
(r) -> {
return new Thread(r, "t" + i.incrementAndGet());
});
for (int i = 0; i < 5; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSystemTime() + " 线程 " + Thread.currentThread().getName() + " 执行任务 " + finalI);
});
}
}
public static void main(String[] args) {
//m1();
m2();
}
}
方法m1():
定义核心线程数为1,最大线程数为2,空闲线程存活3秒,队列长度为5。主线程提交6个任务,因为队列能够存放得下提交的所有任务,所以不会启用空闲线程来执行提交的任务,确切的来说这里永远只会有一个线程在工作。 因为队列能够存放得下提交的任务,所以不会启用空闲线程。
执行结果如下:
方法m2():
一个很有意思的例子,定义核心线程数为0,最大线程数为10000,空闲线程存活10秒,队列长度为10000。主线程提交5个任务,讲道理队列能够存放得下提交的所有任务,不会启用空闲线程,线程池也不会执行任务。事实真的会如此吗???那肯定不会,如果不会执行那还扯个蛋呢。那执行这个任务的线程是什么线程呢?并不是核心线程(因为核心线程数为0),线程池会启动一个空闲线程来执行任务。从执行结果来看,任务执行结束10秒后线程池就关闭了。
执行结果如下:
2、源码分析
线程池是如何保证核心线程不会被销毁?空闲线程数为什么会销毁的呢?接下来我们深入分析一下线程池源码底层实现。
2.1 execute方法
public void execute(Runnable command) {
// 判断command任务是否为空
if (command == null)
throw new NullPointerException();
// 前面的文章已经讲解过ctl此变量的含义了,高3位表示线程池的状态,低29位表示线程池的工作线程数
int c = ctl.get();
// 判断线程池中当前工作线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 如果线程池中当前工作线程数是否小于核心线程数,则执行addWorker()方法创建新线程执行command任务,注意第二个参数为true
if (addWorker(command, true))
return; // 如果addWorker()成功则直接返回,结束execute
// 如果addWorker()失败,则再次获取c的值
c = ctl.get();
}
// 代码执行到这此处,说明任务要么放到阻塞队列中,要么启用一个空闲线程来执行任务或者执行拒绝策略
// 如果线程池处于RUNNING状态,把提交的任务成功放入阻塞队列中
if (isRunning(c) && workQueue.offer(command)) {
// 任务加入阻塞队列成功,recheck 双重检查线程池状态
int recheck = ctl.get();
// 如果此时线程池已经处于非 RUNNING状态,则将任务remove掉
if (! isRunning(recheck) && remove(command))
// 成功从阻塞队列中remove掉任务,执行拒绝策略
reject(command);
//线程池处于RUNNING状态,但是工作线程数为0,则创建新线程
//示例中m2方法会执行到此行代码
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 注意第二个参数为false
}
// 如果不能将任务放入到阻塞队列中,那么我们尝试添加一个新线程(此处是空闲线程),如果创建线程失败,则执行拒绝策略
else if (!addWorker(command, false)) // 注意第二个参数为false
reject(command);
}
为什么需要双重检查线程池的状态呢?
因为在多线程环境下,线程池的状态可能会随时变化,ctl.get()方法不是原子操作,很有可能刚获取线程池状态后线程池的状态就被改变了,因此再次检查线程池状态是否跟加入到阻塞队列workQueue之前一致。如果没有双重检查线程池的状态,在多线程环境下万一线程池的状态为非RUNNING状态,那么任务永远不会被执行。
2.2 addWorker方法
addWorker方法一上来就来两个for (;; )死循环,是不是很懵逼,别着急,接下来我会带大家一起详细分析以下方法的具体实现逻辑,其实也就那么回事。
private boolean addWorker(Runnable firstTask, boolean core) {
retry: // 标签retry:,break 和 continue 的语法,用于跳出多重循环,后续会单独写一篇文章来分析这个标签的含义,大家拭目以待吧。。。
for (;;) {
// 变量c不在赘述
int c = ctl.get();
// 获取线程池的运行状态
int rs = runStateOf(c);
// 下面这个if判断看起来相当的绕啊
// 第一个判断条件,如果rs >= SHUTDOWN成功则执行后面的逻辑运算,否则不会返回false,也就是说如果线程池处于运行态,则可以添加任务,执行后续的for (;;)循环
// 第一个判断条件返回true,此时线程池处于非运行状态,具体看后面的判断
// 如果 rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty() 返回false,则return,不会添加任务
// 具体不再详细分析了,相信大家都能看懂,主要是围绕线程池的状态、任务及队列是否为空来判断是否需要添加新任务
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 此处也是使用了一个死循环
for (;;) {
// wc 变量,表示线程池中正在运行的线程个数
int wc = workerCountOf(c);
// 第一个判断 wc >= CAPACITY,运行的线程数大于等于线程池的最大容纳个,则return false。
// 如果wc < CAPACITY,进入第二个判断,根据传入的core值判断wc是与核心线程数corePoolSize或者是最大线程数maximumPoolSize比较
// 如果core为true 且 wc >=corePoolSize,则return false
// 如果core为false 且 wc >=maximumPoolSize,则return false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用CAS操作,给工作线程数加1,如果成功,则跳出外层死循环
if (compareAndIncrementWorkerCount(c))
break retry; // 跳出外层死循环
// 使用CAS操作,给工作线程数加1,如果失败,有可能是其他线程做了自增操作。需要继续更新变量c的值,并继续执行内层死循环做ctl自增操作。
c = ctl.get(); // Re-read ctl
// 在继续执行内层死循环之前,需要判断线程池的状态是否发生了变化
if (runStateOf(c) != rs)
continue retry; // 如果线程池状态从RUNNING变成了STOP,则不能继续执行内层死循环,而是执行外层死循环,重新做状态判断
// else CAS failed due to workerCount change; retry inner loop
}
}
// 如果外层死循环执行结束,并且没有return false,那说明ctl自增操作成功,接下来的逻辑就是要开始添加新任务了
boolean workerStarted = false; // 表示线程是否启动
boolean workerAdded = false; // 表示线程是否添加到线程池中
Worker w = null;
try {
// 变量w中包括任务、线程等等
w = new Worker(firstTask);
// 获取线程t
final Thread t = w.thread;
// 判断线程t是否为空
if (t != null) {
// 如果线程t不为null,获取锁lock
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)) {
// 判断线程是否一块启动,启动则抛出异常IllegalThreadStateException
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 将w添加到wokers中,set集合
workers.add(w);
int s = workers.size();
// 判断是否超过了最大线程数,largestPoolSize表示记录出现过的最大线程数
if (s > largestPoolSize)
largestPoolSize = s;
// 已经创建了消息称并添加到线程池中了
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 启动线程
t.start();
// 线程已经启动成功了
workerStarted = true;
}
}
} finally {
// 如果线程没有启动成功,执行addWorkerFailed方法回滚操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
2.3 run方法
Worker实例添加成功之后,线程池把Worker当作线程来处理,Worker实现了Runnable接口,接下来我们继续分析一下Worker中的run()方法。
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
// 获取当前线程
Thread wt = Thread.currentThread();
// 获取提交的任务
Runnable task = w.firstTask;
w.firstTask = null; // 设置Worker中的fistTask为空
// 解锁,这行代码与Worker中的构造函数的setState(-1)对应
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 当提交的任务不为空,则处理提交的任务;或者当提交的任务为空,则通过getTask()方法从阻塞队里中获取任务
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
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 线程池的扩展,在执行任务之前处理的逻辑,在之前的线程池的基本应用文章中我们有详细讲过如何使用线程池的扩展
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 真正执行的run方法
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);
}
}
2.4 getTask方法
接下来分析一下getTask()方法,从阻塞队列BlockingQueue中获取任务,调用队列的take()方法阻塞式获取任务或者poll(long timeout, TimeUnit unit)方法超时获取任务。
private Runnable getTask() {
// 定义变量timeOut,上一次poll()是否超时
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// 获取线程池的运行状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 如果线程池状态处于非运行状态,且rs >= STOP 或者队列为空,则工作线程数减1,返回空任务
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
// 获取线程池的工作线程数
int wc = workerCountOf(c);
// Are workers subject to culling?
// 定义变量 timed ,是否允许超时获取任务;
// 1、allowCoreThreadTimeOut表示是否允许核心线程数超时获取任务,默认false。
// 2、工作线程数是否大于核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 如果工作线程数大于最大线程数 或者 timed && timedOut 为true(timed为true也就是超时获取任务,timedOut为true表示上一次已经poll超时).
// 且工作线程数大于1或者阻塞队列为空,那么执行compareAndDecrementWorkerCount(c)方法
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // 工作线程数减1成功,return null
return null;
continue;
}
try {
// 线程池如何保证核心线程不会销毁,空闲线程会被销毁呢?源码就在此,仔细品味吧
// 如果timed为true,则使用poll方法超时获取任务;如果timed为false,则使用take方法阻塞式获取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null) // 如果获取的任务不为空,则返回任务r
return r;
timedOut = true; //poll 超时,进入下一次死循环
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
源码分析就到此为止了,其实还有一些方法没有去分析,相信读者有了上面的源码了解,其他的方法应该都能看明白。通过对以上源码的了解,回过头来再看一下文章一开始抛出来的两个问题是不是已经得到解决。
一是线程池如何保证核心线程数不会被销毁,空闲线程数会被销毁的呢?
1)、核心线程在获取任务的时候调用的是take()方法阻塞式获取
2)、空闲线程在获取任务的时候调用的是poll(long timeout, TimeUnit unit)方法超时获取
因此,线程池的核心线程数不会被销毁,空闲线程数会被销毁。
二是核心线程和空闲线程的区别到底是什么?
1)、空闲线程可能会销毁,空闲时间达到 keepAliveTime,线程将被立刻销毁。
2)、核心线程不会销毁,没有任务执行的时候会一直阻塞,除非调用线程池的 shutdown() 或者shutdownNow()方法。
备注:博主微信公众号,不定期更新文章,欢迎扫码关注。