JAVA 笔记 ThreadPoolExecutor 源码剖析
Posted LaterEqualsNever
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA 笔记 ThreadPoolExecutor 源码剖析相关的知识,希望对你有一定的参考价值。
基本概念
Thread t = new Thread();
t.start();
上面的代码我们再熟悉不过了,因为我们通常在需要开启一个线程的时候都会这样做。
但使用这样的方式,有时候也会照成困扰。例如如果程序中存在大量的并发线程,这样做会带来什么缺陷?
答案很明显,会造成编写工作繁杂,降低系统效率,线程难以管理等等问题。
在这种情况下,有没有一种方式能够让我们避免这些困扰呢?有,也就是我们这里研究的线程池(ThreadPoolExecutor)。
线程池的思想理解起来其实也很简单,我们可以看做是我们创建了一个池,从此再需要开启线程的时候:
我们就只负责向这个池中添加线程任务,而线程的执行,生命周期的管理等等工作,就都由池帮我们管理。
实际上,如果仅仅是想要使用ThreadPoolExecutor是很容易的。
因为JAVA API里的Executor类已经为我们提供了四种工厂方法,获取四种最常用的各具特点的线程池对象:
ExecutorService es = null;
// 1.
es = Executors.newCachedThreadPool();
// 2.
es = Executors.newFixedThreadPool(5);
// 3.
es = Executors.newSingleThreadExecutor();
// 4.
es = Executors.newSingleThreadScheduledExecutor();
我们来看API说明文档里对于ThreadPoolExecutor,有如下这样一段说明:
为了便于跨大量上下文使用,此类提供了很多可调整的参数和扩展钩子 (hook)。但是,强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和 Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。
其实从以上的说明我们不难发现,虽然Executors类为我们提供了足够的工厂方法以获取线程池对象。
但是通过方法命名及API说明,我们都不难发现,事实上Executors只是针对在不同场景的使用特点,对线程池的属性进行了特定的设置,以简化我们的使用。但其底部终究应该仍是离不开ThreadPoolExecutor。
通过源码,我们能够最好的验证我们的猜想。那我们就不妨去看一看前面说到的工厂方法的源码实现:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L,
TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService(
new ScheduledThreadPoolExecutor(1));
}
由此我们知道了,Executors提供的获取线程池的工厂方法,底部正是通过构建ThreadPoolExecutor对象完成的。
ScheduledExecutorService例外,是通过构建ScheduledThreadPoolExecutor完成,但该类型继承自ThreadPoolExecutor。
所以,如果我们不仅仅满足于简单的使用线程池,还想要了解其实现原理。那么,研究ThreadPoolExecutor的源码自然就是最好的方式。
构造器
按照我们的老套路,研究源码自然先看看其构造器的定义,因为使用对象首先需要构建对象。
而且我们前面说到的Executors提供的工厂方法也是通过设置不同的构造器参数,而返回不同使用特点的线程池对象。
// 核心池大小
private volatile int corePoolSize;
// 池最大大小
private volatile int maximumPoolSize;
// 任务队列
private final BlockingQueue<Runnable> workQueue;
// 超过核心池大小的空闲线程的存活时间
private volatile long keepAliveTime;
// 线程工厂
private volatile ThreadFactory threadFactory;
// 拒绝任务时的处理策略
private volatile RejectedExecutionHandler handler;
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
/*
* 满足以下任一条件,则报告非法参数异常:
* 1.corePoolSize(核心池大小)小于0
* 2.maximumPoolSize(池最大容量)小于或等于0
* 3.池最大容量小于核心池大小
* 4.keepAliveTime(空闲线程存活时间)小于0
*/
if (corePoolSize < 0 || maximumPoolSize <= 0
|| maximumPoolSize < corePoolSize || keepAliveTime < 0)
throw new IllegalArgumentException();
/*
* 满足以下任一条件,则报告空指针异常:
* 1.workQueue(任务阻塞队列)为null
* 2.threadFactory(线程创建工厂)为null
* 3.handler(拒绝任务处理策略对象)为null
*/
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
// 赋值操作
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
对于ThreadPoolExecutor类来说,构造器的实现非常一目了然。我们从源码中看到,虽然提供了四个类型的构造器,但最终都将回到第四种构造器的调用。
而我们需要关注的,自然就是这个构造器内,7个参数代表的含义分别是什么了。这可以为我们之后读源码打下一个基础。
- corePoolSize:指线程池的核心大小。每当向线程池内提交新的任务,就会检查当前池内运行的线程的数量。如果小于该值,就会创建新的线程;如果大于该值,则会将任务暂时安排进阻塞队列。
- maximumPoolSize:指线程池的最大容量。设置该值的意义在于:当向池内添加新的任务,如果当前池内运行的线程数大于corePoolSize且小于maximumPoolSize;并且阻塞队列已经排满,则创建新的线程。
- keepAliveTime:指的是空闲线程的存活时间。但一定注意该存活时间只针对于那些超过corePoolSize的线程生效。也就是说,如果池内当前即使存在空闲的线程,但如果数量在corePoolSize范围之内。该值则不会生效。
- unit:很简单,就是设置keepAliveTime的时间单位(如:天、小时、分、秒等)
- workQueue:阻塞队列,用来存储等待执行的任务。一般来说,这里的阻塞队列有以下几种选择:1.ArrayBlockingQueue; 2.LinkedBlockingQueue; 3.SynchronousQueue;
- threadFactory:线程创建工厂,顾名思义,主要负责线程的创建工作。
- RejectedExecutionHandler:拒绝任务时的处理策略。拒绝任务是指:向池内新添加任务时,池内运行的线程数量已经大于或等于maximumPoolSize(即最大容量),那么当前池则会拒绝此次添加的任务。
添加并执行任务
任何时候,当我们创建完对象,最关心的自然就是如何去使用该对象。而对象线程池来说,我们最关心的莫过于向池中添加任务并执行了。
这样的操作实际上完成起来很简单,就像我们下面这个例子中做的:
public class Demo {
static int i;
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Runnable() {
@Override
public void run() {
while (i < 10) {
System.out.println(++i);
}
}
});
}
}
就像我们在这个例子中看到的一样,通常我们希望线程池为我们执行任务。就是通过调用execute方法,并传入Runnable对象完成的。
那么,既然我们的目的是了解线程池的工作原理。毫无疑问的,研究该方法的源码实现就是我们必不可少的一个重点:
public void execute(Runnable command) {
// 如果传入任务为null,报告空指针异常
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
} else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
我们来分析一下上面的代码:
1.首先,判断传入的任务是否为空。如果判断结果为真,则报告空指针异常。
2.接着进行的是一个“短路或”的逻辑判断。首先判断池内当前线程数(poolSize)是否大于核心池大小。
3.如果判断结果为真,则会造成短路,就可以直接结束当前判断进入if代码块中继续执行了。
4.否则则会继续进行判断,从而就会调用到addIfUnderCorePoolSize。顾名思义,就是小于核心池大小添加任务。
5.如果调用addIfUnderCorePoolSize的结果返回为false,代表添加任务失败。则同样会进入if代码块中继续执行了。
如果返回结果为true,则代表添加任务成功,execute方法就可以执行结束了。
6.接下来就是当“短路或”的判断运算通过,即当满足以下两个条件中的任一 一个时,就会继续进行下一轮的判断。
这两个条件即是:“poolSize >= corePoolSize”和“addIfUnderCorePoolSize(command)返回false”。
7.这时,首先是判断线程池状态是否为running状态。那么说到这里,我们有必要了解一下,关于线程池的状态的知识。
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;//当创建线程池后,初始时,线程池处于RUNNING状态;
static final int RUNNING = 0;//如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
static final int SHUTDOWN = 1;//如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
static final int STOP = 2;//当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
static final int TERMINATED = 3;
8.另一个运算条件是“workQueue.offer(command)”,这个运算的用意也很明显,就是将任务加入阻塞队列。
需要注意的就是这里执行的是“短路与”运算,所以如果线程池的运行状态只要不为runnning,就不会执行加入阻塞队列的操作了。
9.当线程池状态为runnning,并且将任务加入阻塞队列成功,就会执行下一轮判断“runState != RUNNING || poolSize == 0”。
否则就将进入else if判断,执行到addIfUnderMaximumPoolSize方法,同样的,这个方法的意义就是在小于池最大容量的时候添加任务。
10.接下来我们先分析“短路与”运算判断通过的情况,我们说了,这时会继续执行判断“if (runState != RUNNING || poolSize == 0)”。思考一下,为什么我们已经将任务加入阻塞队列了,还要执行这一次判断呢?
其实从判断的条件就不难想到,这是为了如果出现在任务加入阻塞队列的同时,其它线程调用了线程池的shutdown或者shutdownNow方法的情况,能够得到处理。
如果判断通过即代表出现了前面所说的“紧急情况”,执行的代码是“ensureQueuedTaskHandled(command);”
就这个方法的命名,我们就可以知道,如果出现了这样的情况,执行的方法就是为了保证阻塞队列的任务能够被处理。
private void ensureQueuedTaskHandled(Runnable command) {
// 获取锁为代码加上同步
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
// 拒绝任务判断标示
boolean reject = false;
Thread t = null;
try {
// 获取线程池运行状态
int state = runState;
// 如果运行状态不为RUNNING,并且将任务从阻塞队列中移除
if (state != RUNNING && workQueue.remove(command))
// 设置拒绝标示
reject = true;
// 如果满足运行状态小于STOP;当前线程数小于核心大小;阻塞队列不为空
else if (state < STOP &&
poolSize < Math.max(corePoolSize, 1) &&
!workQueue.isEmpty())
// 添加一个runnable任务为null的线程
t = addThread(null);
} finally {
mainLock.unlock();
}
// 判断拒绝标示
if (reject)
// 拒绝任务
reject(command);
else if (t != null)
// 执行任务
t.start();
}
11.接着就是我们说的另一种情况,即else if的addIfUnderMaximumPoolSize判断。
如果该方法的返回结果为false,也就是说线程池已经到达了最大容量,添加任务失败。那么,就会调用reject方法,进行拒绝添加任务的策略处理、
void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
我想在execute方法当中,有两个方法的源码我们也很有兴趣一看。就是“addIfUnderCorePoolSize”和“addIfUnderMaximumPoolSize”。
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
// 同步
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 当前线程数小于核心池数目 并且 线程池运行状态为RUNNING
if (poolSize < corePoolSize && runState == RUNNING)
// 创建线程
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
Thread t = null;
// 同步
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 当前线程数小于池最大容量 并且 线程池运行状态为RUNNING
if (poolSize < maximumPoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
从源码我们可以看到,这两个方法的逻辑实际上很简单。但我们注意到一个关键的方法调用“addThread”。
实际上我们在ensureQueuedTaskHandled中也已经看到过这个方法的调用,那我们肯定会关心该方法的实现:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w); //创建一个线程,执行任务
if (t != null) {
w.thread = t; //将创建的线程的引用赋值为w的成员变量
workers.add(w);
int nt = ++poolSize; //当前线程数加1
if (nt > largestPoolSize)
largestPoolSize = nt; // 记录线程池内出现过的线程的最大数量
}
return t;
}
该方法的实现逻辑同样清晰,需要引起我们注意的自然是方法开头就出现的一个类型:“Worker”。
Worker是定义在ThreadPoolExecutor当中的内部类,实际上是对我们添加进线程池的Runnable任务进行了封装。
接下来,我们就来研究一下该内部类的源码实现。
内部类Worker
private final class Worker implements Runnable {
// 同步锁对象
private final ReentrantLock runLock = new ReentrantLock();
// 初始任务
private Runnable firstTask;
// 执行完成的任务数
volatile long completedTasks;
Thread thread;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
// 任务是否处在活动状态
boolean isActive() {
return runLock.isLocked();
}
// 封装线程中断操作
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
if (runLock.tryLock()) {
try {
if (thread != Thread.currentThread())
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
void interruptNow() {
thread.interrupt();
}
// 任务执行
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
if (runState < STOP && Thread.interrupted() && runState >= STOP)
thread.interrupt();
boolean ran = false;
/*
*beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
*自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等。
*/
beforeExecute(thread, task);
try {
task.run();
ran = true;
afterExecute(task, null);
// 增加完成执行的任务数量
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
// 循环执行任务
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
}
我们看到Worker自身实现了Runnable接口,所以实际上我们在addThread方法中看到的以下代码:
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
实际上其效果基本上就等同于:
Thread t = new Thread(w);
所以我们不难想到,对于Runnable接口的实现,最重点的自然就是对于run方法的覆写。
而我们从Worker当中的run方法的实现可以看出,它首先执行的是通过构造器传进来的任务firstTask。
在调用runTask()执行完firstTask之后,在while循环里面不断通过getTask()去取新的任务来执行。
那么去哪里取呢?自然是从任务缓存队列“workQuene”里面去取。接下来我们就来看一看这个方法的源码实现:
Runnable getTask() {
// 等同于一个while循环
for (;;) {
try {
// 获取运行状态
int state = runState;
// 如果是STOP或者TERMINATED状态
if (state > SHUTDOWN)
return null;
Runnable r;
// 如果是SHUTDOWN状态
if (state == SHUTDOWN)
r = workQueue.poll(); // 从队列中获取任务
//如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
//则通过poll取任务,若等待一定的时间取不到任务,则返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
//否则通过take取任务(其实也就是处于RUNNING状态时)
r = workQueue.take();
if (r != null)
return r; // 如果取到的任务不为null,则返回任务。
if (workerCanExit()) { // 如果取到的任务为空,即r==null,则判断当前的worker是否可以退出
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers(); // 中断处于空闲状态的线程
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
}
以上的代码我们已经做了较为详细的注释,但额外再次强调的一段代码是:
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
//则通过poll取任务,若等待一定的时间取不到任务,则返回null
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
之所以再把这两行代码额外提出来解释,是因为这就是我们所说的线程超时的现象的原因所在。
这里是做了一个“短路或”运算,首先判断当前池内的线程数是否大于核心池数量。
如果判断结果为真,则直接进行后面的代码。如果为假,就要判断allowCoreThreadTimeOut。
我们之前说到“keepAliveTime”被用于指定空闲线程的存活时间。但是该线程只针对于超过corePoolSize的线程有效。
实际上这正是因为allowCoreThreadTimeOut默认为false。在上面的代码中如果allowCoreThreadTimeOut为true,那么也将执行之后的poll方法。
事实上,”workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)”与无参的poll方法相比,其不同的地方就在于:
该方法有一个等待时间,如果超过等待时间,仍然没有获取到队列的头元素,那么就将返回null。
那么也就是说,如果我们调用“allowsCoreThreadTimeOut()”方法设置该值为true。那么keepAliveTime就会对corePoolSize以内的线程也生效。
除此之外需要额外观察的可能就是“workerCanExit”与“interruptIdleWorkers”。
我们首先来看“workerCanExit”的源码实现:
private boolean workerCanExit() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
boolean canExit;
try {
canExit = runState >= STOP ||
workQueue.isEmpty() ||
(allowCoreThreadTimeOut &&
poolSize > Math.max(1, corePoolSize));
} finally {
mainLock.unlock();
}
return canExit;
}
这个方法通过一个boolean型的返回结果来判断worker是否可以退出。那么什么时候该方法返回结果为true呢?
从上面的代码我们不难看到,需要满足以下三种条件的任一一种:
- 1.runState >= STOP,即当前线程池已经处于STOP或者TERMINATED状态。
- 2.workQueue.isEmpty(),也就是说阻塞任务队列中的任务已经被执行完毕。
- 3.allowCoreThreadTimeOut &&poolSize > Math.max(1, corePoolSize)。如果设置了allowCoreThreadTimeOut为true,并且池内线程数大于1.
OK,如果的确满足我们上面所说的三种情况中的任一一种,那么就会执行“interruptIdleWorkers”:
void interruptIdleWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfIdle();
} finally {
mainLock.unlock();
}
}
我们发现实际上唤醒中断线程的操作,最终是通过定义在Worker类当中的interruptIfIdle方法完成的。它的源码如下:
void interruptIfIdle() {
final ReentrantLock runLock = this.runLock;
// 获取锁
if (runLock.tryLock()) {
try {
// 如果work的成员thread不是系统的当前线程
if (thread != Thread.currentThread())
// 中断线程状态
thread.interrupt();
} finally {
runLock.unlock();
}
}
}
这个方法中值得注意的就是:我们通过调用tryLock()来获取锁。那么如果当前worker正在执行任务。
这说明锁当前并不是自由的,所以通过tryLock是无法获取到锁的。那么对应的,如果成功获取了锁,说明当前worker处于空闲状态。
线程池的工作原理
实际上到了,这里我们就对整个线程池的工作原理有了一个了解了。我们在这里再次进行一次回顾和总结,能够加深我们的理解。
- 如果我们想要使用线程池,自然首先需要构建一个线程池对象。
我们可以根据自己的需要调用不同的构造器;也可以通过现有的Executors类中的工厂方法获取。- 有了线程池对象后,我们可以通过调用execute方法,向池内添加需要执行的线程任务。
- 线程池接受到任务的过程中,如果当前池的运行状态不为RUNNING,那么添加的任务将直接被拒绝。
如果池内当前存在的线程数小于核心池大小,则会创建新的线程并执行任务。反之则会将任务加入阻塞队列。
如果阻塞队列已经无法再添加新的任务,且当前线程数小于池的最大容量,则会创建新的额外线程去执行任务。
默认情况下,超过核心池数量的线程,空闲时间一旦超过keepAliveTime就会被中断销毁。- 接收到任务后,创建线程执行的工作是由addThread方法完成。该方法会将接受到的任务封装成一个Worker对象。
- Worker实现了runnable接口,在其覆写的run()方法当中,首先会执行firstTask,也就是初次接收到的任务。
- 当firstTask执行完毕,就会调用getTask方法循环的从阻塞队列”workQuene”当中取出阻塞的任务去执行。从而也就实现了线程的复用,节约了消耗。
- 当从workQuene中获取到的任务对象返回为null,就代表阻塞队列中的任务已经被取出执行完毕。
这时就会调用workerCanExit来判断worker是否可以被退出了,如果判断结果为真:
紧接着就会通过调用interruptIdleWorkers()方法,将空闲的线程全部中断,从而进行空闲线程的清理工作。- 在Worker的run方法中,循环执行完所有的task后。finally语句块中执行了workerDone方法。
workerDone方法会将当前worker对象从Workers集合中移除,并且将poolSize执行自减运算。
一旦poolSize为0了,就会执行tryTerminate()试图关闭当前线程池。
由此,经过我们的总结整理,我们发现ThreadPoolExecutor的整个工作流程实际上算是十分清晰的。
以前在另一篇文章中看到过一个对于ThreadPoolExecutor工作原理的比喻,我觉得十分形象,大致意思就是:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
那么只要有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
这个时候,如果有工人做完了当前手中的任务。那么就从等待的任务清单中取出新的任务做。
但是如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招2个临时工人进来;
然后就将任务也分配给这2个临时工人做;如果即使加上临时工,工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这12个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉2个临时工,只保持原来的10个工人了,毕竟请额外的工人是要花钱的。
到了这个时候,我们就对ThreadPoolExecutor有了很不错的了解了。当然该类当中还有很多其他的方法,
但实际上这些方法的实现和使用有了我们以上的基础,理解起来都很容易了,有兴趣我们可以自己查看源码和查阅API文档来了解。
而值得一提的,还有以下的几个东西,我们分别来看一下:
任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue< Runnable >,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
任务拒绝策略
任务拒绝策略就是指我们之前说到的,当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,还有任务进入时的处理策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出常异常
RejectedExecutionException:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
以上是关于JAVA 笔记 ThreadPoolExecutor 源码剖析的主要内容,如果未能解决你的问题,请参考以下文章