线程池中关键的ThreadPoolExecutor类原理分析
java.uitl.concurrent.ThreadPoolExecutor 类是 Executor 框架中最核心的类。
线程池简介
什么是线程池
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。
为什么要使用线程池
因为 Java 中创建一个线程,需要调用操作系统内核的 API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
使用线程池就能很好地避免频繁创建和销毁,所以有必要引入线程池。使用 线程池的好处 有以下几点:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。
线程池状态和工作线程数量
这本来是两个不同的概念,但是在ThreadPoolExecutor中我们使用一个变量ctl来存储线程池状态和工作线程数量,这样我们只需要维护这一个变量的并发问题,提高运行效率。
/**
* 记录线程池中Worker工作线程数量和线程池的状态
* int类型是32位,它的高3位,表示线程池的状态,低29位表示Worker的数量
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// COUNT_BITS 29位,
private static final int COUNT_BITS = Integer.SIZE - 3;
// 表示线程池中创建Worker工作线程数量的最大值。即 0b0001.....1(29位1)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
怎么使用一个变量ctl存储两个值呢?
就是利用int变量的高3位来储存线程池状态,用int变量的低29位来储存工作线程数量。
这样就有两个需要注意的地方:
- 工作线程数量最大值不能超过int类型29位的值CAPACITY 即0b0001.....1(29位1)
- 因为线程池状态都是高3位储存的,所以工作线程数量不会影响状态值大小关系。
线程池状态
// 高3位值是111
private static final int RUNNING = -1 << COUNT_BITS;
// 高3位值是000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 高3位值是001
private static final int STOP = 1 << COUNT_BITS;
// 高3位值是010
private static final int TIDYING = 2 << COUNT_BITS;
// 高3位值是011
private static final int TERMINATED = 3 << COUNT_BITS;
线程池状态分析:
① RUNNING:运行状态。接受新任务,并且也能处理阻塞队列中的任务。
② SHUTDOWN:关闭状态。不接受新任务,但可以处理阻塞队列中的任务。
- 在线程池处于 RUNNING 状态时,调用 shutdown 方法会使线程池进入到该状态。
- finalize 方法在执行过程中也会调用 shutdown 方法进入该状态。
③ STOP:停止状态。不接受新任务,也不处理队列中的任务。会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow 方法会使线程池进入到该状态。
④ TIDYING:整理状态。如果所有的任务都已终止了,workerCount (有效线程数) 为 0,线程池进入该状态后会调用 terminated 方法进入 TERMINATED 状态。
⑤ TERMINATED:已终止状态。在 terminated 方法执行完后进入该状态。默认 terminated 方法中什么也没有做。进入 TERMINATED 的条件如下:
- 线程池不是 RUNNING 状态;
- 线程池状态不是 TIDYING 状态或 TERMINATED 状态;
- 如果线程池状态是 SHUTDOWN 并且 workerQueue 为空;
- workerCount 为 0;
- 设置 TIDYING 状态成功。
操作ctl的方法
获取线程池的状态
/**
* 获取线程池的状态。因为线程池的状态是使用高3位储存,所以屏蔽低29位就行了。
* 所以就c与~CAPACITY(0b1110..0)进行&操作,屏蔽低29位的值了。
* 注意:这里是屏蔽低29位的值,而不是右移29位。
*/
private static int runStateOf(int c) { return c & ~CAPACITY; }
获取工作线程数量
/**
* 获取线程池中Worker工作线程的数量,
* 因为只使用低29位保存Worker的数量,只要屏蔽高3位的值就行了
* 所以就c与CAPACITY(0b0001...1)进行&操作,屏蔽高3位的值了。
*/
private static int workerCountOf(int c) { return c & CAPACITY; }
合并ctl的值
/**
* 得到ctl的值。
* 接受两个参数rs和wc。rs表示线程池的状态,wc表示Worker工作线程的数量。
* 对于rs来说我们只需要高3位的值,对于wc来说我们需要低29位的值。
* 所以我们将rs | wc就可以得到ctl的值了。
*/
private static int ctlOf(int rs, int wc) { return rs | wc; }
其他方法
// 因为RUNNING状态高三位是111,所以状态值rs与工作线程数量ws相与的结果值c一定是个负数,
// 而其他状态值都是大于等于0的数,所以c是负数,那么表示当前线程处于运行状态。
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
/**
* 使用CAS函数将ctl值自增
*/
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
/**
* 使用CAS函数将ctl值自减
*/
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
/**
* 使用CAS函数加循环方法这种乐观锁的方式,解决并发问题。
* 保证使ctl值减一
*/
private void decrementWorkerCount() {
do {} while (! compareAndDecrementWorkerCount(ctl.get()));
}
重要成员变量
// 记录线程池中Worker工作线程数量和线程池的状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 任务线程的阻塞队列,因为是阻塞队列,所以它是并发安全的
private final BlockingQueue<Runnable> workQueue;
// 独占锁,用来保证操作成员变量的并发安全问题
private final ReentrantLock mainLock = new ReentrantLock();
// 等待线程池完全终止的条件Condition,
private final Condition termination = mainLock.newCondition();
//----------------- 需要mainLock来保证并发安全-------------------------//
// 线程池中工作线程集合。Worker中持有线程thread变量
private final HashSet<Worker> workers = new HashSet<Worker>();
// 线程池中曾拥有过的最大工作线程个数
private int largestPoolSize;
// 线程池完成过任务的总个数
private long completedTaskCount;
//----------------- 需要mainLock来保证并发安全-------------------------//
// 创建线程的工厂类
private volatile ThreadFactory threadFactory;
// 当任务被拒绝时,用来处理这个被拒绝的任务
private volatile RejectedExecutionHandler handler;
// 工作线程空闲的超时时间keepAliveTime
private volatile long keepAliveTime;
// 是否允许核心池线程超时释放
private volatile boolean allowCoreThreadTimeOut;
// 线程池核心池线程个数
private volatile int corePoolSize;
// 线程池最大的线程个数
private volatile int maximumPoolSize;
成员变量说明:
- mainLock:使用mainLock来保证会发生变化成员变量的并发安全问题。会发生的成员变量有5个:ctl、workQueue、workers、largestPoolSize和completedTaskCount。但是其中ctl和workQueue的类型本身就是多线程安全的,所以不用mainLock锁保护。
- termination:等待线程池完全终止的条件,如果线程池没有完全终止,调用它的awaitNanos方法,让线程等待。当线程池完全终止后,调用它的signalAll方法,唤醒所有等待termination条件的线程。
- workers:记录所有的工作线程Worker
- workQueue:记录所有待执行的任务。使用阻塞队列BlockingQueue,可以在队列为空时,线程等待,队列有值时,唤醒等待的线程。
- largestPoolSize:线程池中曾拥有过的最大工作线程个数
- completedTaskCount:线程池完成过任务的总个数
- threadFactory:创建线程的工厂类
- handler:当任务被拒绝时,用来处理这个被拒绝的任务
- keepAliveTime:工作线程允许空闲的超时时间,一般都是针对超过核心池数量的工作线程。
- allowCoreThreadTimeOut: 是否允许核心池的工作线程超时释放。
- corePoolSize:线程池核心池线程个数。
- maximumPoolSize: 线程池最大的线程个数。
构造方法
ThreadPoolExecutor 有四个构造方法,前三个都是基于第四个实现。第四个构造方法定义如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
参数说明:
1、corePoolSize:核心线程数量。当有新任务通过 execute 方法提交时 ,线程池会执行以下判断:
- 如果运行的线程数少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
- 如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当 workQueue 满时才创建新的线程去处理任务;
- 如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建的线程池的大小是固定的。这时如果有新任务提交,若 workQueue 未满,则将请求放入 workQueue 中,等待有空闲的线程去从 workQueue 中取任务并处理;
- 如果运行的线程数量大于等于 maximumPoolSize,这时如果 workQueue 已经满了,则使用 handler 所指定的策略来处理任务;
所以,任务提交时,判断的顺序为 corePoolSize => workQueue => maximumPoolSize。
2、maximumPoolSize:最大线程数量。
- 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
值得注意的是:如果使用了无界的任务队列这个参数就没什么效果。
3、keepAliveTime:线程保持活动的时间(非核心线程的存活时间)。
当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime。
所以,如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
4、unit:keepAliveTime 的时间单位。有 7 种取值。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
5、workQueue - 等待执行的任务队列。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
(1)ArrayBlockingQueue - 有界阻塞队列。
此队列是基于数组的先进先出队列(FIFO)。
此队列创建时必须指定大小。
(2)LinkedBlockingQueue - 无界阻塞队列。
此队列是基于链表的先进先出队列(FIFO)。
如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE。
吞吐量通常要高于 ArrayBlockingQueue。
使用 LinkedBlockingQueue 意味着: maximumPoolSize 将不起作用,线程池能创建的最大线程数为 corePoolSize,因为任务等待队列是无界队列。
Executors.newFixedThreadPool 使用了这个队列。
(3)SynchronousQueue - 不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
吞吐量通常要高于 LinkedBlockingQueue。
Executors.newCachedThreadPool 使用了这个队列。
(4)PriorityBlockingQueue - 具有优先级的无界阻塞队列。
6、threadFactory - 线程工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
7、handler - 饱和策略。它是 RejectedExecutionHandler 类型的变量。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。线程池支持以下策略:
AbortPolicy - 丢弃任务并抛出异常。这也是默认策略。
DiscardPolicy - 丢弃任务,但不抛出异常。
DiscardOldestPolicy - 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
CallerRunsPolicy - 只用调用者所在的线程来运行任务。
如果以上策略都不能满足需要,也可以通过实现 RejectedExecutionHandler 接口来定制处理策略。如记录日志或持久化不能处理的任务。
常用方法
执行任务execute方法
在线程池中如何执行一个任务command,要分三种情况:
- 线程池中工作线程的数量没有达到核心池个数,那么线程池就应该开启新的工作线程来执行任务。
- 线程池中工作线程的数量达到核心池个数,那么就应该将任务添加到任务队列中,等待着工作线程去任务队列中获取任务并执行。
- 如果任务添加到任务队列失败,那么就要开启新的工作线程来执行任务。
public void execute(Runnable command) {
// 如果command为null,抛出异常
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 分为三个步骤:
* 1. 如果运行的工作线程数量少于核心池数量corePoolSize,
* 那么就调用addWorker方法开启一个新的工作线程,运行任务command。
* 2. 如果开启新的工作线程失败,就将任务添加到任务队列中。
* 3. 添加到任务队列失败,
* 那么仍然addWorker方法在最大池中开启一个新的工作线程,运行任务command。
*/
int c = ctl.get();
// 运行的工作线程数量少于核心池数量corePoolSize
if (workerCountOf(c) < corePoolSize) {
/**
* 开启一个新的工作线程,运行任务command。
* 返回true,表示开启工作线程成功,直接return。
* 返回false,表示没有开启新线程。那么任务command就没有运行,所以要执行下面代码。
*/
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程池处于运行状态,
// 且任务添加到任务阻塞队列workQueue中成功,即workQueue队列有剩余空间。
if (isRunning(c) && workQueue.offer(command)) {
// 再次检查线程池状态和工作线程数量
int recheck = ctl.get();
/**
* 如果线程池不在运行状态,那么就调用remove方法移除workQueue队列这个任务command,
* 如果移除成功,那么调用reject(command)方法,进行拒绝任务的处理。
* 如果移除失败,那么这个任务还是会被执行,那么就不用调用reject(command)方法
*/
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果工作线程数量为0,但是workQueue队列中我们添加过任务,
// 那么必须调用addWorker方法,开启一个新的工作线程。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 调用addWorker方法,开启一个新的工作线程,运行任务command。
// 如果还是失败,那么这个任务command就不会不可能执行了,
// 那么调用reject(command)方法拒绝这个任务
else if (!addWorker(command, false))
reject(command);
}
方法流程上面已经有标注,注意有以下几点:
- addWorker(Runnable firstTask, boolean core):表示开启一个新的工作线程执行任务firstTask。core是用来判断核心池还是最大池。返回false,表示开启新线程失败,即任务firstTask没有机会执行。
- isRunning(c)线程池处于RUNNING状态,只有处于RUNNING状态下,才能将任务添加到任务队列。
- reject(command) 当任务command不能在线程池中执行时,就会调用这个方法,告诉调用值,线程池拒绝执行这个任务。
添加工作线程addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
// 利用死循环和CAS函数,实现乐观锁,来实现多线程改变ctl值的并发问题
// 因为ctl值代表两个东西,工作线程数量和线程池状态。
// 这里就用了两个for循环,一个是线程池状态的for循环,一个是工作线程数量的for循环
retry:
for (;;) {
int c = ctl.get();
// 获取线程池运行状态rs,
int rs = runStateOf(c);
// 首先判断线程池状态和任务队列状态,
// 来判断能否创建新的工作线程
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 线程池中工作线程数量wc
int wc = workerCountOf(c);
// 当线程池工作线程数量wc大于线程上限CAPACITY,
// 或者用户规定核心池数量corePoolSize或用户规定最大线程池数量maximumPoolSize
// 表示不能创建工作线程了,所以返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用CAS函数,使工作线程数量wc加一
if (compareAndIncrementWorkerCount(c))
// 跳出retry循环
break retry;
// 来到这里表示CAS函数失败,那么就要循环重新判断
// 但是c还代表线程状态,如果线程状态改变,那么就必须跳转到retry循环
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 工作线程是否开始,即调用了线程的start方法
boolean workerStarted = false;
// 工作线程是否添加到工作线程队列workers中
boolean workerAdded = false;
Worker w = null;
try {
// 创建一个Worker对象
w = new Worker(firstTask);
// 得到Worker所拥有的线程thread
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
// 并发锁
mainLock.lock();
try {
// 获取线程池运行状态rs
int rs = runStateOf(ctl.get());
// 当线程池是运行状态,或者是SHUTDOWN状态但firstTask为null,
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果线程t已经被开启,就抛出异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 将w添加到工作线程集合workers中
workers.add(w);
// 获取工作线程集合workers的个数
int s = workers.size();
// 记录线程池历史最大的工作线程个数
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 如果已经添加到工作线程队列中,那么开启线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 如果开启工作线程失败,那么这个任务也就没有执行
// 因此移除这个任务w(如果队列中有),减少工作线程数量,因为这个数量在之前已经增加了
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
添加一个新的工作线程,就涉及到两个成员变量的改变,一个是工作线程数量ctl,一个是工作线程集合workers。而ctl的类型是AtomicInteger,所以它可以使用乐观锁解决并发问题,workers就只能使用mainLock互斥锁来保证并发安全问题。
更改工作线程数量ctl
因为ctl储存了两个值,工作线程数量和线程池状态。所以使用了两个for循环来监控多线程对这两个值的更改。
用线程池状态来判断是否允许添加新的工作线程:
// 是对addWorker中线程状态if判断的拆分
// 当线程池不是处于运行状态
if (rs >= SHUTDOWN) {
/**
* 线程池状态不是SHUTDOWN,或者firstTask不为null,或者任务队列为空,
* 都直接返回false,表示开启新工作线程失败。
* 只有当线程池状态是SHUTDOWN,firstTask为null,任务队列不为空时,
* 需要创建新的工作线程。
* 从execute(Runnable command)方法中分析,firstTask参数为空只有一种情况,
* 此时线程池中工作线程数量是0,而任务队列不为空,
* 那么就要开启一个新工作线程去执行任务队列中的任务,否则这些任务会被丢失。
*/
if (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()) {
return false;
}
}
由此可以得出,只有两种情形允许添加新的工作线程:
- 线程池处于RUNNING状态
- 线程池虽然处于SHUTDOWN状态,但是线程池工作线程个数是0(即这里的firstTask != null),且任务队列workQueue不为空,那么就要开启一个新工作线程去执行任务队列中的任务。
然后使用for循环和CAS函数方式,来给工作线程数量加一。注意此时工作线程还没有创建,并添加到线程集合workers中,所以如果线程添加失败,那么还要将工作线程数量减一。
添加工作线程集合workers
创建一个工作线程Worker,将它添加到线程集合workers中,然后开启这个工作线程,使用mainLock独占锁保证成员变量workers的并发安全问题。
内部类Worker
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
/** 该Worker所拥有的工作线程 */
final Thread thread;
/** Worker拥有的第一个任务,初始化的时候赋值 */
Runnable firstTask;
/** 该工作线程Worker完成任务的数量 */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
// 将state设置为-1,禁止发起中断请求,
// 直到调用过runWorker方法,即线程已经运行时。
setState(-1);
// 第一个任务
this.firstTask = firstTask;
// 创建一个thread线程对象,它的run方法就是本Worker的run方法
// 这个thread就是Worker真正执行任务的工作线程
this.thread = getThreadFactory().newThread(this);
}
/** 复写的是Runnable中的run方法,所以当工作线程开启运行后,会调用这个方法。 */
public void run() {
runWorker(this);
}
// 当前独占锁是否空闲
protected boolean isHeldExclusively() {
return getState() != 0;
}
// 尝试获取独占锁
protected boolean tryAcquire(int unused) {
// 如果通过CAS函数,可以将state值从0改变成1,那么表示获取独占锁成功。
// 否则独占锁被别的线程获取了。
if (compareAndSetState(0, 1)) {
// 设置拥有独占锁的线程是当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放独占锁
protected boolean tryRelease(int unused) {
// 设置拥有独占锁的线程为null
setExclusiveOwnerThread(null);
// 设置获取独占锁的次数是0,表示锁是空闲状态
setState(0);
return true;
}
// 获取独占锁,如果锁被别的获取,就一直等待。
public void lock() { acquire(1); }
// 尝试获取独占锁,如果锁被别的获取,就直接返回false,表示获取失败。
public boolean tryLock() { return tryAcquire(1); }
// 释放独占锁
public void unlock() { release(1); }
// 当前独占锁是否空闲
public boolean isLocked() { return isHeldExclusively(); }
// 如果Worker的工作线程thread已经开启,那么发起中断请求。
void interruptIfStarted() {
Thread t;
// getState() >= 0表示thread已经开启
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
Worker实现了Runnable接口,那么就可以通过Worker对象创建一个新线程thread,这个thread就是Worker的工作线程,而任务都在run方法中执行。
Worker还继承自AbstractQueuedSynchronizer类。我们知道可以通过AQS类实现独占锁和共享锁,而Worker中实现了tryAcquire和tryRelease方法,说明Worker对象也是个独占锁对象。我们可以考虑一下Worker这个独占锁的作用是什么?
工作线程运行任务runWorker方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 将w的state状态设置成0,这样就允许对w的thread线程进行中断请求了。
w.unlock();
// completedAbruptly表示线程突然终结
boolean completedAbruptly = true;
try {
// 通过getTask从任务队列中获取任务task执行,这个方法是个阻塞方法。
while (task != null || (task = getTask()) != null) {
// 获取w独占锁,保证当本工作线程运行任务时,
// 不能对该线程进行中断请求。
w.lock();
/**
* 如果线程池大于STOP状态,且Worker工作线程中断标志位是false,
* 那么就调用wt的interrupt方法发起中断请求。
*/
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
// Worker工作线程发起中断请求
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,进行下一次循环
task = null;
// 将work完成的任务数completedTasks加一
w.completedTasks++;
// 释放w独占锁
w.unlock();
}
}
// completedAbruptly = false表示线程正常完成终结
completedAbruptly = false;
} finally {
// 进行一个工作线程完结后的后续操作
processWorkerExit(w, completedAbruptly);
}
}
runWorker方法是在每个工作线程的run方法中调用,通过getTask()方法从任务队列中获取任务task执行,这个方法可以阻塞当前工作线程,如果getTask()方法返回null,那么工作线程就会运行结束,释放线程。
虽然runWorker方法运行在每个工作线程中,但是对于一个Worker来说,只要它的工作线程能够运行runWorker方法,而且改变的也是这个Worker的成员变量,且这些成员变量也只能在runWorker方法改变,那么它就没有多线程并发问题啊,那么为什么在这里加锁呢?
这是因为Worker中有一个变量是可以被其他线程改变的,就是它的工作线程thread的中断请求,所以Worker独占锁的作用就是控制别的线程对它的工作线程thread中断请求的。
最后调用processWorkerExit方法,进行一个工作线程完结后的后续操作。
获取任务getTask方法
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// 获取线程池状态rs
int rs = runStateOf(c);
// 如果有需要检查任务队列workQueue是否为空
// 即rs >= STOP或者rs == SHUTDOWN且workQueue为空,那么返回null,停止工作线程
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 将工作线程数量减一
decrementWorkerCount();
return null;
}
// 获取工作线程数量wc
int wc = workerCountOf(c);
/**
* 如果allowCoreThreadTimeOut为true或者wc > corePoolSize时,
* 就要减少工作线程数量了。
* 当工作线程在keepAliveTime时间内,没有获取到可执行的任务,
* 那么该工作线程就要被销毁。
*/
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 工作线程数量减一,返回null,销毁工作线程。
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 从任务队列workQueue中获取了任务r,会阻塞当前线程。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 如果r不为null,返回这个任务r
if (r != null)
return r;
// r是null,表示获取任务超时
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
从阻塞任务队列workQueue中获取任务返回,因为是阻塞任务队列,所以可以阻塞当前线程。如果返回null,那么会完结调用getTask方法的那个工作线程。那么getTask方法在什么情况下返回null呢?
- 线程池的状态大于等于STOP,或者线程状态是SHUTDOWN且当前任务队列为空,那么返回null,停止工作线程。
- 获取任务时间超时,那么也会返回null,停止工作线程。因为线程池一般只维护一定数量的工作线程,如果超过这个数量,那么超过数量的工作线程,在空闲一定时间后,应该被释放。
终止线程池的方法
shutdown和shutdownNow方法
/**
* 终止线程池。不能在添加新任务了,但是已经添加到任务队列的任务还是会执行。
* 且对所有不是正在执行任务的工作线程都发起中断请求
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查是否拥有Shutdown的权限
checkShutdownAccess();
// 将线程池状态变成SHUTDOWN状态
advanceRunState(SHUTDOWN);
// 对所有不是正在执行任务的工作线程都发起中断请求
interruptIdleWorkers();
// 钩子方法,提供给子类实现。表示线程池已经shutdown了
onShutdown();
} finally {
mainLock.unlock();
}
// 尝试去终结线程池
tryTerminate();
}
/**
* 终止线程池。不能在添加新任务了,也不会执行已经添加到任务队列的任务,只是将这些任务返回。
* 且对所有工作线程都发起中断请求, 不管这个工作线程是否正在执行任务
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查是否拥有Shutdown的权限
checkShutdownAccess();
// 将线程池状态变成STOP状态
advanceRunState(STOP);
// 对所有工作线程都发起中断请求, 不管这个工作线程是否正在执行任务
interruptWorkers();
// 返回阻塞队列workQueue中未执行任务的集合
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 尝试去终结线程池
tryTerminate();
return tasks;
}
shutdown和shutdownNow区别:
- shutdown方法将线程池设置成SHUTDOWN状态,shutdownNow将线程池设置成STOP状态。
- shutdown方法调用之后不能再添加新任务了,但是已经添加到任务队列的任务还是会执行。shutdownNow方法调用之后不能再添加新任务了,也不会执行已经添加到任务队列的任务,只是将这些任务返回。
- shutdown方法会对所有不是正在执行任务的工作线程都发起中断请求,shutdownNow方法会对所有工作线程都发起中断请求, 不管这个工作线程是否正在执行任务。
advanceRunState方法
private void advanceRunState(int targetState) {
// 采用乐观锁的方法,来并发更改线程池状态。
for (;;) {
int c = ctl.get();
// 如果runStateAtLeast方法返回true,表示当前线程池状态已经是目标状态targetState
// 采用CAS函数尝试更改线程池状态,如果失败就循环继续。
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
这个方法来改变线程池状态,使用乐观锁的方式保证并发安全。
中断空闲状态下的工作线程
/**
* 对所有不是正在执行任务的工作线程都发起中断请求。
*/
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 遍历工作线程Worker集合
for (Worker w : workers) {
Thread t = w.thread;
// 如果工作线程中断标志位是false,
// 且能够获取锁,即当前工作线程没有运行任务
if (!t.isInterrupted() && w.tryLock()) {
try {
// 发起中断请求。
// 因为获取了锁,所以在进入中断请求时,worker工作线程不会执行任务
t.interrupt();
} catch (SecurityException ignore) {
} finally {
// 释放锁
w.unlock();
}
}
// 是否只进行一个工作线程的中断请求。
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
遍历工作线程Worker集合,如果工作线程处于空闲状态,且没有被中断,那么就发起中断请求。通过独占锁Worker知道,当前工作线程是否在执行任务。
对所有已开启的工作线程发起中断请求
/**
* 对所有工作线程都发起中断请求, 不管这个工作线程是否正在执行任务
*/
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
// 如果w的工作线程thread已经开启,那么发起中断请求。
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
遍历工作线程Worker集合,调用Worker的interruptIfStarted方法,如果工作线程已开启,那么就会发起中断。
尝试完结线程池的方法
final void tryTerminate() {
for (;;) {
int c = ctl.get();
/**
* 如果线程池是RUNNING状态,
* 或者线程池是TIDYING状态(是因为已经有别的线程在终止线程池了)
* 或者线程池是SHUTDOWN状态且任务队列不为空,
* 线程池不能被terminate终止,直接return返回
*
*/
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 线程池中工作线程数量不是0,线程池不能被terminate终止,所以要return
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 钩子方法,提供给子类实现。表示线程池已经终止。
terminated();
} finally {
// 设置线程池状态是TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
}
}
线程池在什么情况下算是完全停止了呢?有三个条件:
- 线程池不是RUNNING状态。
- 线程池中工作线程数量是0。
- 线程池中任务队列为空。
所以再看看tryTerminate(),前面两个if判断条件,就可以理解了。
等待线程池完结的方法
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
// 如果是TERMINATED已终止状态,那么就返回true
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
// 如果已经超时就返回false
if (nanos <= 0)
return false;
// 让当前线程等待。并设置超时时间nanos
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
如果线程池不是TERMINATED状态,就让当前线程在termination条件上等待,直到线程池变成TERMINATED状态,或者等待时间超时才会被唤醒。
工作线程退出的方法
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果工作线程突然被终结,那么工作线程的数量就没有减一。
if (completedAbruptly)
// 将工作线程数量减一。
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 将工作线程的任务完成数添加到线程池完成任务总数中
completedTaskCount += w.completedTasks;
// 从工作线程集合中移除本工作线程
workers.remove(w);
} finally {
mainLock.unlock();
}
// 因为有一个工作线程已经完成被释放,那么就去尝试终结线程池。
tryTerminate();
int c = ctl.get();
// 如果线程池状态小于STOP,
// 就要判断终结了这个工作线程之后,线程池中工作线程数量是否满足需求。
if (runStateLessThan(c, STOP)) {
// 如果工作线程正常终结,
// 那么要看线程池中工作线程数量是否满足需求。
if (!completedAbruptly) {
// 不允许核心池线程释放,那么最小值是corePoolSize,否则就可以为0
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 但是如果任务队列中还有任务,那么工作线程数量最少为1
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果工作线程数量小于min值,就要创建新的工作线程了。
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 开启一个新的工作线程
addWorker(null, false);
}
}
工作线程被释放,有两种情况,一种是运行完成正常结束,一种是发生异常意外终止。
当工作线程被释放时,需要将它从工作线程集合workers中移除,将该工作线程任务完成数添加到线程池完成任务总数中。调用tryTerminate方法尝试终结线程池。
另外因为有一个工作线程被释放,那么就要考虑线程池中当前工作线程数量是否符合要求,要不要添加新的工作线程。
线程工厂ThreadFactory
设置线程名称是很重要的,尤其是在排查问题的时候。
在JDK中,有实现ThreadFactory就只有一个地方(DefaultThreadFactory)。而更多的时候,我们都是继承它然后自己来写这个线程工厂的。
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);//原子类,线程池编号
private final ThreadGroup group;//线程组
private final AtomicInteger threadNumber = new AtomicInteger(1);//线程数目
private final String namePrefix;//为每个创建的线程添加的前缀
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();//取得线程组
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
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;
}
}
在上面的代码中,可以看到线程池中默认的线程工厂实现是很简单的,它做的事就是统一给线程池中的线程设置线程group、统一的线程前缀名以及统一的优先级。
第一种:Spring框架提供的CustomizableThreadFactory
ThreadFactory springThreadFactory = new CustomizableThreadFactory("springThread-pool-");
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10), springThreadFactory);
exec.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":记忆过了.....");
}
});
说明:Spring框架也提供了线程池类ThreadPoolTaskExecutor
ThreadPoolTaskExecutor threadExecutor = new ThreadPoolTaskExecutor();
threadExecutor.setCorePoolSize(CORE_POOL_SIZE);
threadExecutor.setMaxPoolSize(MAX_POOL_SIZE);
threadExecutor.setQueueCapacity(5000);
threadExecutor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
threadExecutor.setThreadNamePrefix("elasticsearch-task");
threadExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
threadExecutor.initialize();
第二种:Google guava 工具类 提供的 ThreadFactoryBuilder ,使用链式方法创建。
//这里需要添加%d, 在源码中会与序号进行拼接
ThreadFactory guavaThreadFactory = new ThreadFactoryBuilder().setNameFormat("guava-pool-%d").build();
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10), guavaThreadFactory);
exec.submit(() -> {
System.out.println(Thread.currentThread().getName() + ":--记忆中的颜色是什么颜色---");
});
第三种:Apache commons-lang3 提供的 BasicThreadFactory。
//这里需要添加%d,在源码中会跟序号拼接
ThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("basicThreadFactory-%d").build();
ExecutorService exec = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(10), basicThreadFactory);
exec.submit(() -> {
System.out.println(Thread.currentThread().getName() + ":--记忆中的颜色是什么颜色---");
});
饱和策略(拒绝策略)
拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。
jdk默认提供了四种拒绝策略:
- AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
- DiscardPolicy - 直接丢弃,其他啥都没有。
- DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。
如果以上策略都不能满足需要,也可以通过实现 RejectedExecutionHandler 接口来定制处理策略。如记录日志或持久化不能处理的任务。