1. 基础用法
任务类:
java
public class TestTask implements Runnable {
int i;
TestTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程正在执行任务:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用线程池:
java
public static void main(String[] args) throws InterruptedException {
// 声明一个线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,5,
TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(40));
for (int i = 0;i < 50;i++){
System.out.println("start task::"+i);
executor.execute(new TestTask(i)); // 将任务扔到线程池
}
System.out.println(executor.getTaskCount());
}
输出:
java
start task::0
start task::1
start task::2
start task::3
start task::4
start task::5
start task::6
start task::7
start task::8
start task::9
10
pool-1-thread-1线程正在执行任务:0
pool-1-thread-2线程正在执行任务:1
pool-1-thread-1线程正在执行任务:2
pool-1-thread-2线程正在执行任务:3
pool-1-thread-1线程正在执行任务:4
pool-1-thread-2线程正在执行任务:5
pool-1-thread-1线程正在执行任务:6
pool-1-thread-2线程正在执行任务:7
pool-1-thread-1线程正在执行任务:9
pool-1-thread-2线程正在执行任务:8
2. 开始源码探索
上面展示了一个简单线程池的例子,入口就是ThreadPoolExecutor.execute()
。但我们的源码旅程要从ThreadPoolExecutor类的构造函数开始,先了解几个控制线程池的参数:
java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
构造参数含义:
参数 | 含义 |
---|---|
corePoolSize | 工作线程数 |
maximumPoolSize | 最大工作线程数:当corePoolSize用完后,额外生成新的工作线程的额度,但处理完任务后,这部分工作线程会被释放 |
keepAliveTime | 空闲线程存活时间,即超出corePoolSize数量的工作线程,在没有处理任务时,存活的时间 |
workQueue | 任务队列:当没有空闲的工作线程来处理任务时,就会先把任务放到这个队列中 |
threadFactory | 创建工作线程的工厂对象,一般使用默认即可Executors.defaultThreadFactory() |
RejectedExecutionHandler | 拒绝策略:如果当前没有空闲的工作线程,工作线程数也达maximumPoolSize个数了,workQueue也塞不下任务了。这个时候线程池会拒绝这个任务,怎么拒绝?就看这个对象:1.AbortPolicy:直接抛出异常(默认) 2.CallerRunsPolicy:用调用者所在的线程来执行任务 3.DiscardOldestPolicy:丢弃队列中最靠前的任务,来执行当前任务 4.DiscardPolicy:直接丢弃当前任务 5.实现RejectedExecutionHandler接口的自定义handler |
在了解线程池的构造参数后(没记住也没事,看到时再返回来看一下,多来几遍就就记住了),我们再看下线程池两个重要的概念参数:线程池状态和工作线程数:
java
// 控制位,32位字节码。前四位作为线程池的状态码。其他位作为工作线程的计数器
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 线程池的状态码
// -1 左移后前四位:1110 线程池能处理新提交的任务,并且也能处理阻塞队列中的任务
private static final int RUNNING = -1 << COUNT_BITS;
// 0 左移后前四位:0000 线程池不再接受新提交的任务,但是能够处理存量任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 左移后前四位:0010 线程池不再接受新提交的任务,也不处理存量任务
private static final int STOP = 1 << COUNT_BITS;
// 左移后前四位:0100 线程池中所有任务已经终止
private static final int TIDYING = 2 << COUNT_BITS;
// 左移后前四位:0110 线程池terminated()方法执行后,进入该状态
private static final int TERMINATED = 3 << COUNT_BITS;
线程池就是通过下面两个方法,拿ctl变量作位移、与、取反操作,来获取线程池的状态和工作线程数,可以自己拿笔,写写01,实操一下,有点绕。二进制的位移、与、或操作不熟也没事,知道这两方法的作用就行。
java
// 线程池运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取工作线程数
private static int workerCountOf(int c) { return c & CAPACITY; }
接下来我们就要开始线程池"处理逻辑"的源码之旅了。我们会按照先整体,后局部的思路来解读代码逻辑。
- 执行任务的入口:
java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1.如果当前工作线程小于 corePoolSize。
if (workerCountOf(c) < corePoolSize) {
// 尝试新增一个工作线程,来处理任务,如果成功直接返回
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.新增工作线程失败,尝试将task新增到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
/**
* 任务入队列成功,再次检查线程池状态,若为非running状态,
* 则将task从队列中移除,并拒绝该任务
*/
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0) // 线程池状态正常,但工作线程数为0
// 再尝试新增一个工作线程,这里只要工作线程数不超过设置的"最大线程数"(maximumPoolSize),就会再次新增工作线程
addWorker(null, false);
}
// 3.task入队列失败,再次尝试新增工作线程数。这里只要工作线程数不超过设置的"最大线程数"(maximumPoolSize),就会再次新增工作线程
else if (!addWorker(command, false))
reject(command); // 最后一次新增工作线程也失败了,根据设置的拒绝策略,拒绝task
}
上述代码整体分为三步:
- 正在运行的工作线程数如果少于配置的corePoolSize,则试图创建一个新的线程来执行任务。调用addWorker()方法,会自动检查线程池的运行状态和工作线程数,当新增线程失败时,该方法会返回false,尝试第二步流程
- 尝试新增工作线程失败但线程池状态是正常的,则向
workQueue
队列中添加任务。如果task正常被添加到了队列,我们仍然需要再次检测线程池的状态及工作线程的数量,判断是否需要再新增一个工作线程(存在上一个工作线程die的可能)或者可能在task被添加到队列的时候,线程池已经被关闭了。所以我们需要再次检查线程池的状态,在必要的时候回滚队列或者新增Thread - 如果task入队列失败,我们会再次尝试新增一个线程,如果新增失败,线程池可能被关闭了或者队列饱和了,我们需要根据设定的策略拒绝这个任务
整体流程看下来,只有addWorker()
方法需要我们仔细研究,其他逻辑都是进行一些简单的条件判断,我们接下来仔细研究一下新增工作线程的方法。
2.1 新增工作线程逻辑
新增工作线程的主要分为两部分:
- 第一部分,状态的判断
- 第二部分,真正新增工作线程
- look源码:
java
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 第一部分,状态判断
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 线程池已经处于不接收task状态(不处于running状态了),直接返回false
if (rs >= SHUTDOWN &&
// 若线程池处于SHUTDOWN状态,但是队列中的任务还未完成,会新增工作线程来处理task
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize)) // 判断工作线程数已经超过限制,直接返回false
return false;
// 尝试新增一个工作线程(这里只是计数器+1,原子性操作),成功则直接结束循环。
if (compareAndIncrementWorkerCount(c))
break retry;
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 {
w = new Worker(firstTask); // new 一个工作线程对象。worker对象持有了一个Thread对象
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());
// 再次检查线程池状态
// 线程池处于running状态
if (rs < SHUTDOWN ||
// 线程池处于SHUTDOWN状态,但传入的task是null,单纯是为了增加工作线程,处理队列中的剩余任务
(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) {
// 执行真正的任务逻辑,其实是执行worker里的run方法,worker自己也实现了Runnable接口
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 若工作线程添加失败,一般就是线程池异常(自己手动调用停止线程池了),移除刚添加的工作线程、将工作线程计数器-1,尝试修改线程池状态
addWorkerFailed(w);
}
return workerStarted;
}
针对代码我都添加了注释,如果在博客上阅读不方便,可以把代码拷贝到文本编辑器中阅读或者直接阅读源码,对照着看。
大部分的逻辑都是条件判断,可以自己研究,理解。这里我们主要看一下,下面的逻辑:
java
w = new Worker(firstTask); // new 一个工作线程对象。worker对象持有了一个Thread对象
final Thread t = w.thread;
...省略了大部分代码...
if (workerAdded) {
// 执行真正的任务逻辑,其实是执行worker里的run方法,Worker对象自己也实现了Runnable接口
t.start();
workerStarted = true;
}
...省略了大部分代码...
这里将我们传进来的task任务,封装成了一个Worker对象,然后从worker对象中,获取一个Thread对象,然后就是我们熟悉的t.start()
启动线程方法。首先我们看一下Work的构造函数:
java
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable{
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
// 我们的真正、实际要执行的任务逻辑类
this.firstTask = firstTask;
// new 了一个Thread类
this.thread = getThreadFactory().newThread(this);
}
}
this.thread = getThreadFactory().newThread(this)
看到这行代码有没有熟悉的感觉?不就是我们平时Thread t = new Thread(0)
么?再一看Worker类实现了Runnable,是熟悉的代码。所以t.start()
代码就是异步 执行我们Worker类中的run()
方法,然后我们再来看run()
java
public void run() {
runWorker(this);
}
再看runWorker(this)
方法:
java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true; // 意外中断标识
try {
// 获取任务。只要还能获取到task任务,就一直死循环执行task任务
while (task != null || (task = getTask()) != null) {
w.lock(); // 上锁
// 如果线程池是stoping状态,要确保工作线程也要终止
// 这里就需要我们进行双重检测。以免在清理线程"中断状态"的同时,又shutdown线程池了
if ((runStateAtLeast(ctl.get(), STOP) || // 线程池状态已经处于STOP或者TIDYING、TERMINATED状态了
(Thread.interrupted() && // 线程的中断状态为true
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
// 线程池的状态是非running状态 且 工作线程的中断状态为false。 就尝试中断线程
wt.interrupt();
try {
// 空方法,方便扩展的。可以自己覆盖方法,实现自己的内容
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行你真正的任务,注意这里调用的是run(),不是start()。这里是同步执行的
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; // 将task置空
w.completedTasks++;
w.unlock(); // 释放锁
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly); // 结束工作线程
}
}
runWorker()
方法,我们主要看以下几点:
- while循环的判断条件,只要task不为空,就会一直死循环执行任务。这也是我们复用线程资源的点。那么
getTask()
如何获取任务就很关键了,我们后面再仔细看该方法。 task.run()
代码,就是调用我们自己的任务逻辑的地方了,注意这里是同步方法调用,别被迷惑了。
我们接着看getTask()
方法:
java
private Runnable getTask() {
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.
// 线程池状态为SHUTDOWN且队列中没有task任务了,释放工作线程
// 或者线程池状态为STOP、TIDYING、TERMINATED,释放工作线程
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 工作线程数大于设定的最大阈值 或者 工作线程数大于corePoolSize且从队列中获取任务超时(即队列中没有任务了)
if ((wc > maximumPoolSize || (timed && timedOut))
// 至少有一个以上的工作线程 或者 队列中没有任务才释放线程
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // 工作线程计数器-1
return null; // 返回null,释放当前工作线程资源
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 获取任务失败,有这种情况1.工作线程数大于corePoolSize且从队列中获取任务超时(即队列中没有任务了)。
// 2.设置了allowCoreThreadTimeOut参数为true,但从队列中获取任务超时(即队列中没有任务了)
// allowCoreThreadTimeOut 设置为ture时,会把corePoolSize数目内的线程也释放调。
// 使每次超过空闲时间后,执行任务的线程都是新的线程。感兴趣的可以看看allowCoreThreadTimeOut()方法。
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
在getTask()
方法中,主要就是将存放队列中的task取出来,交给工作线程。我们重点看一下几个在线程池构造函数中设置的参数值对逻辑的控制:
- 线程数:通过maximumPoolSize、corePoolSize来控制工作线程数
java
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 工作线程数大于设定的最大阈值
// 或者 工作线程数大于corePoolSize且从队列中获取任务超时(即队列中没有任务了)
if ((wc > maximumPoolSize || (timed && timedOut))
// 至少有一个以上的工作线程 或者 队列中没有任务才释放线程
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) // 工作线程计数器-1
return null; // 返回null,释放当前工作线程资源
continue;
}
- 超时时间:通过设置队列的获取时间来控制。
java
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
3. 总结
- 极简流程图:
- 以上就是线程池的正向逻辑的全部源码了。其他还有线程池的异常处理、状态控制的细节、如何中止线程池,就留给读者自己研究,加深理解了。