1、为什么要使用线程池
当前主流商业Java虚拟机的线程模型都是基于操作系统原生线程模型来实现,即采用1:1的线程模型。
如果不理解可以看这篇文章:java与线程
现在的服务器基本都是多个核心,多个核心可以达到并行运算。对于我们常见的web服务,有cpu密集型计算,也有IO密集型计算,线程数设置过多,会导致线程上下切换过于频繁,消耗性能,设置过少,不能充分利用cpu,部分线程处于等待状态。合理的设置线程数,可以充分提高cpu使用效率。其次,每次任务来,线程都进行创建、销毁也造成不必要的性能浪费。
总结,为什么要使用线程池?
- 降低资源消耗。避免线程重复创建、销毁造成不必要的开销
- 提高了响应速度。当任务来了,不必等待线程创建利用活跃线程、或唤醒沉睡线程去执行
- 合理的设置并发线程数,提高cpu使用效率。
2、线程池的生命周期
JDK中线程池的核心实现类是ThreadPoolExecutor。它的参数如下:
参数 | 说明 |
---|---|
corePoolSize | 核心线程数量 |
maximumPoolSize | 线程池维护线程的最大数量 |
keepAliveTime | 线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁 |
unit | keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS |
workQueue | 线程池所使用的任务缓冲队列 |
RejectedExecutionHandler | 当缓冲队列已满,线程数也达到最大线程数时,执行拒绝策略 |
RejectedExecutionHandler 几种默认拒绝策略:
策略 | 功能说明 |
---|---|
AbortPolicy(中止策略) | 当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程 |
CallerRunsPolicy(调用者运行策略) | 当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理 |
DiscardPolicy(丢弃策略) | 直接静悄悄的丢弃这个任务,不触发任何动作 |
DiscardOldestPolicy(弃老策略) | 果线程池未关闭,就弹出队列头部的元素,然后新任务加入队列 |
自定义策略 | 自己实现拒绝策略功能 |
ThreadPoolExecutor通过以 ctl 字段对线程池的运行状态和线程池中有效线程的数量进行控制。它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存 runState,低29位保存 workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
同时它内部封装的获取线程池状态、获取线程池线程数量的方法。如以下代码所示:
// 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 536870911 二进制原码:0001 1111 1111 1111 1111 1111 1111 1111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// ctl的前3位用来识别当前线程池状态,后29位记录线程数量。初始化是Running状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 这个方法计算取ctl的前3位值,后29位都置为0,得到当前线程池状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 这个方法计算取ctl的后29位值,前3位都置为0,得到当前线程数量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 通过状态和线程数生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
// 线程的5种状态
// 二进制原码:1010 0000 0000 0000 0000 0000 0000 0000
// 二进制补码:1110 0000 0000 0000 0000 0000 0000 0000
private static final int RUNNING = -1 << COUNT_BITS;
// 二进制原码:0000 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 二进制原码:0010 0000 0000 0000 0000 0000 0000 0000
private static final int STOP = 1 << COUNT_BITS;
// 二进制原码:0100 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING = 2 << COUNT_BITS;
// 二进制原码:0110 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED = 3 << COUNT_BITS;
关于线程池的状态,有5种,分别是:
运行状态 | 状态描述 |
---|---|
RUNNING | 运行状态,能接受新提交的任务,并且也能处理阻塞队列中的任务 |
SHUTDOWN | 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务 |
STOP | 停止状态,不能接受新提交的任务,抛弃阻塞队列中的任务,会中断正在处理任务的线程 |
TIDYING | 清空状态,所有任务都已终止,workerCount(有效线程数)为0 |
TERMINATED | 终止状态,terminated()方法执行完后进入该状态 |
其生命周期转换如下所示:
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。
3、线程池整体工作流程
向线程池提交任务是由execute方法完成的,接下来我们通过分析源码来解析线程池整个提供任务的工作流程。
ThreadPoolExecutor.execute
这里删除了官方注释,整个方法就短短几行。
public void execute(Runnable command) {
// 1.任务非空判断
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 2.添加核心线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 3.任务添加进阻塞队列等待被调用
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 线程池不再运行,则删除任务
if (!isRunning(recheck) && remove(command))
reject(command);
// 因为核心线程数可以修改,这里添加任务成功后判断是否有线程可以执行任务,没有则添加线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 4.添加非核心线程执行任务
else if (!addWorker(command, false))
// 5.如果失败,执行拒绝策略
reject(command);
}
该方法的主要逻辑是:
- 新提交的任务进行非空校验
- 判断当前线程数 是否小于 corePoolSize。如果是,则新增核心线程执行任务
- 如果否,则把任务放进 workQueue。
- 如果放不下,则会添加非核心线程执行任务。
- 如果添加失败,则执行拒绝策略
这里会有个问题,为什么步骤3 要双重校验线程池状态?(双重检验:修改之前检验,修改之后检验)
答:第一次校验合格,再往workQueue添加任务,这些操作不是原子性的,也就意味着在第一次校验之后和添加任务成功之前,线程池状态是可以被改变的。所以这里在添加成功后,再次进行线程池状态校验。
整体流程图如下:
ThreadPoolExecutor.addWorker
这个方法主要添加工作线程,如果firstTask不为null,则执行firstTask;否则,去阻塞队列获取任务执行。
private boolean addWorker(Runnable firstTask, boolean core) {
// 位置标记,嵌套循环调用 continue retry; 能重新回到这里
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
/**
1.如果线程池状态是 RUNNING, 继续往下执行
2.如果线程池状态是 SHUTDOWN, 如果first != null,返回false。
因为 SHUTDOWN状态不允许添加新的任务,但是允许添加线程去执行任务
3.如果线程池状态>= SHUTDOWN,返回false。因为 STOP状态 不能添加新任务,
阻塞队列中任务排空,线程开始释放
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
/**
1.当前线程数 大于 允许最大线程数,返回 false
2.如果添加的是核心线程,当前线程数 >= 核心线程数,返回 false
3.如果添加的是非核心线程,当前线程数 >= 最大线程池容量,返回 false
*/
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// CAS操作修改 当前线程数+1。保证数据安全,一致性
if (compareAndIncrementWorkerCount(c))
// 操作成功 打破嵌套循环
break retry;
// cas操作增加线程数失败
c = ctl.get();
// 这里判断 线程池状态是否变化
if (runStateOf(c) != rs)
// 变化了 跳到最外层循环
continue retry;
// 没变化,则继续内循环
}
}
/**
这2个循环,直至添加线程成功or失败
失败则返回 fasle
成功则继续往下执行
值得一提的是,上面操作只是线程池状态、数量检验,然后修改线程数量,
并没有实际的创建线程
*/
/** 下面开始创建线程,并添加到线程容器,便于回收 */
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 重点 创建Worker线程 封装了工作线程、初始任务
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());
/**
1.如果线程池状态是 Runnings,添加线程到works
2.如果线程池状态是 SHUTDOWN,初始任务是null,
才可以添加线程到 works
*/
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive())
throw new IllegalThreadStateException();
/**
workers是个HashSet集合,添加也是线程不安全。
为什么用set?后续删除可以根据hash删除引用,被垃圾回收。
*/
workers.add(w);
int s = workers.size();
// 这个操作是线程不安全的
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
// 放锁
mainLock.unlock();
}
// 如果添加成功,则启动线程
if (workerAdded) {
/**
在创建 Worker 时,worker 有 thread 的引用,
同时 thread.Runnable 也引用了 worker,
所有最终还是会调用 woreker.run
*/
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
通过该方法申请线程,会有两种申请方式,如下图所示:
4、Worker线程和workQueue任务队列
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。而workQueue任务队列就是充当生产者角色,当有任务添加进来,会通知处于waiting的线程去任务队列获取任务,而Worker线程在这里充当了消费者。
4.1 Worker线程(消费者)
Worker 类主要是维护线程运行任务时的中断控制状态,以及次要的信息记录。这里可中断状态主要针对是 SHUTDOWN 状态,它是由 AQS 的 state 字段控制的,怎么理解?SHUTDOWN 状态 调用中断方法需要先获取锁,CAS 操作修改 state = 0->1,修改成功才能执行 t.interrupt() 方法。
Worker 类部分代码如下:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;// Worker持有的线程,该线程同时拥有worker的引用
Runnable firstTask;// 初始化的任务,可以为null
}
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask,实现 Runnable 接口便于被线程引用,最终调用 worker.run() 方法。
- thread:是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务,它的 Runnable 变量引用了 Worker;
- firstTask:用它来保存传入的第一个任务,这个任务可以有也可以为 null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务;如果这个值是null,那么就需要去执行任务列表(workQueue)中的任务。
Worker 是通过继承 AQS,使用 AQS 来实现独占锁这个功能。没有使用可重入锁 ReentrantLock,而是使用 AQS 实现了一个简单的非可重入互斥锁。这里问题来了,worker 在执行任务的时候为什么要加锁?
答:主要针对线程池状态从 RUNNING——>SHUTDOWN 转换时,需要尝试中断闲置的线程,但又不能中断正在执行任务的线程,所以Worker 通过 AQS 实现非重入独占锁来确保正在执行任务的线程不会被中断。(这里可能有人会问为什么要中断闲置线程?可以先思考下,文章末尾有答案)
- lock 方法一旦获取了独占锁,表示当前线程正在执行任务中,不是空闲状态,则不应该中断线程(PS:如果任务业务代码里面有 wait、sleep 等操作,如果工作线程被标记了中断,那么工作线程遇到这些操作则会中断抛出异常)
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程标识中断。
- 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。(PS:tryLock 本质是实现了 AQS.tryAcquire 方法,与 ReentrantLock 不一样,它是实现了非重入功能)
在线程池状态 RUNNING 转换为 SHUTDOWN 状态后,会调用该方法。shutdown()——>interruptIdleWorkers()。
/**
见名知意,中断闲置的线程。什么是闲置的线程?就是阻塞任务队列空了,
线程进入 AQS 等待队列的线程。
*/
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// worker 创建后被添加到 workers(HashSet)
for (Worker w : workers) {
Thread t = w.thread;
// 在中断之前先获取非重入独占锁
if (!t.isInterrupted() && w.tryLock()) {
// tryLock 来判断线程是否在闲置。
try {
// 给线程线程上中断标识
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
整体流程图如下:
而在线程池状态 RUNNING 转换为 STOP状态后,会调用该方法。shutdownNow()——>interruptWorkers()
// 见名思意,中断线程
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
可以看出,并不会去判断线程是否在闲置,而是直接标识线程中断。
整体流程图如下所示:
值得一提的是,shutdown() 和 shutdownNow() 方法,最终还会调用 tryTerminate(); 该方法更多的是做一些补偿措施,比如还有线程阻塞在 waiting 状态,则会再次遍历 works,调用 interrupt 方法让线程能顺利回收。
ThreadPoolExecutor.runWorker
工作线程 start 后会调用这个方法,它包括了线程去阻塞队列获取任务、执行任务逻辑等。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
/**
worker初始化时,state是设置为-1的,相当于加了锁;这里更新为0,相当于释放锁。 允许线程中断
*/
w.unlock();
boolean completedAbruptly = true;
try {
/**
这个循环很重要
1. 判断worker初始化时的初始任务 不为空,就执行初始任务
2. 如果初始任务为空,则去阻塞队列获取任务,获取到了,就往下执行;
那如果获取不到任务怎么办?
*/
while (task != null || (task = getTask()) != null) {
// 加锁,SHUTDOWN状态 正在执行任务,则不应该中断线程
w.lock();
// 1.如果线程池是 STOP 状态,应确保线程是被设置了中断标记
// 2.如果线程池不是 STOP 状态,确保线程是非中断状态。
/**
对于判断条件2,线程池如果是 SHUTDOWN 状态,工作线程在执行任务之前
是可能被设置为中断状态的。这种逻辑主要发生在 shutdown() 方法,
当线程释放锁后,在执行getTask()任务时,这期间另外一个线程执行完了
shutdown() 方法,那么这个线程是会被标记上中断标识的。
*/
/**
Thread.interrupted():不管是否设置了中断标识,
都会清除当前线程的中断标识。如果原先有中断标识则返回true,
如果没设有则返回false。
*/
/**
对于判断条件2 这里出现双重检验校验逻辑,
第一次校验如果不是 STOP 状态,然后去清除线程的中断标识,清除成功后,
再次校验线程状态是不是 STOP 状态,为什么要再次检验是不是 STOP 状态?
*/
/**
这里第一次检验和清除线程可中断标识之间不是原子操作的,
会受其他线程干扰。所以在执行Thread.interrupted()返回true后,
应该考虑在这之间有这种情况,有其他线程修改了线程池状态为 STOP,
并给线程设置了中断标识(这种逻辑主要发生在 shutdownNow()方法),
所以我们必须又给线程重新打上中断标识
*/
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
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 {
task = null;
w.completedTasks++;
// 释放锁后,该线程允许被打上 中断标记
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 调用getTask()方法返回null时,会执行到这里,回收线程
processWorkerExit(w, completedAbruptly);
}
}
上面的代码中可以看到有 beforeExecute、afterExecute,它们都是钩子函数,可以分别在子类中重写它们用来扩展 ThreadPoolExecutor,例如添加日志、计时、监视或者统计信息收集的功能。
工作线程在处理完任何后,它还会通过getTask()方法向阻塞队列获取任务。
ThreadPoolExecutor.getTask
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 这个逻辑很重要,判断线程是不是可以回收就是在这里
/**
1.如果线程池是 SHUTDOWN 状态,阻塞任务队列也是空的,
那么workerCount - 1,返回 null,回收线程
2.如果线程池是 STOP 状态,那么workerCount - 1,返回null,回收线程
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
/**
allowCoreThreadTimeOut:默认未false,
是否允许核心线程在超过 keepAliveTime 被回收,即都是非核心线程
*/
// time:true 表示当前线程是 非核心线程,false 当前线程 是核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/**
判断当前线程数量是否大于maximumPoolSize。线程池必须保证,不超过。
为什么会有这种情况? 因为线程池对外提供了
修改最大线程数量的方法 setMaximumPoolSize()
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
// cas操作失败继续循环
continue;
}
try {
Runnable r = timed ?
// 如果是非核心线程 进入这个方法,自动唤醒
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
// 如果是核心线程 进入这个方法,等待被唤醒
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
workQueue.poll(),当非核心线程拿取不到任务时候,会自动唤醒,然后再进入 for (;;)循环,如果判断 workQueue.isEmpty(),那么会返回 null,最后这个非核心线程会被回收
这个方法看起来很简单,定义了核心线程、非核心线程拿取任务的核心逻辑。
4.2 WorkQueue阻塞任务队列(生产者)
接下来是另外一个重点,生产者 workQueue,线程的阻塞、唤醒都是靠它完成的(本质是靠 ReentrantLock 完成线程通信的)。它是一个接口,我们下文都以它的一个实现类 ArrayBlockingQueue 来解读。
ArrayBlockingQueue.take
从上文可知,核心线程去阻塞队列拿取任务是调用这个方法的。看这个方法之前先思考一个问题,如果阻塞队列为空,线程会怎么办,自旋吗?
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁,保证这是个线程安全的队列
lock.lockInterruptibly();
try {
// 如果队列里面没有任务
while (count == 0)
// 重点这个方法。notEmpty = ReentrantLock.newCondition();
// 底层就是 AQS$ConditionObject 来实现唤醒和通知功能
notEmpty.await();
// 取出队列里面的任务
return dequeue();
} finally {
// 放锁
lock.unlock();
}
}
当workQueue中没任务时,线程挂起,进入 AQS 等待队列,等待被唤醒
字段 notEmpty 是什么,AOS$ConditionObject,它是实现等待通知的关键所在,可以将当前线程挂起进入AQS等待队列尾。当被调用signal()通知方法后,则会进入AQS同步队列尾,等持有锁的线程释放锁的后,则会被真正唤醒,抢占锁,拿取任务,执行任务。如果不明白,AQS等待队列的实现,可以看这篇文章:深入理解AQS原理
我们看下阻塞队列添加任务的方法是不是调用了signal()。
ArrayBlockingQueue.offer
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 通知
notEmpty.signal();
}
从这里可以看出,如果AQS等待队列有闲置线程,每添加一个任务,则会唤醒一个闲置线程。如果没闲置线程,等待被运行的线程获取并执行。
ArrayBlockingQueue.poll
接下来我们分析,非核心线程去阻塞任务队列读取任务的方法
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
// 加锁,保证这是个线程安全的队列
lock.lockInterruptibly();
try {
while (count == 0) {
// 队列里面没有任务
if (nanos <= 0)
// 设置的超时 时间小于 0
// 直接返回null 回收线程
return null;
// 进入限时等待状态,超过时间会被自动唤醒 or 被主动唤醒
// 如果超时自动被唤醒,并且队列里面没有任务,则返回 null 线程被回收。(PS:这里是 非核心线程超时回收的逻辑所在)
nanos = notEmpty.awaitNanos(nanos);
}
// 取出队列里面的任务
return dequeue();
} finally {
// 放锁
lock.unlock();
}
}
4.3 Worker线程和WorkQueue队列工作流程图:
5、附加:
为什么进入SHUTDOWN和STOP状态时,要给线程设置中断标识。
- 当进入 SHUTDOWN、STOP 状态,对于已经挂起的工作线程进入了AQS等待队列,线程也就进入了无限等待 waiting 状态,这时阻塞任务队列是无法添加新的任务,也就永远无法唤醒它们。所以为了回收这部分线程,在调用 shutdown()、shutdownNow() 方法时,试图给这部分状态线程上中断标识,以便回收它们。