Java中线程池的使用和原理分析
(一)概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起。
(二)核心类和核心接口介绍
- Executor - 运行任务的简单接口。
- ExecutorService - 扩展了 Executor 接口。扩展能力:支持有返回值的线程、支持管理线程的生命周期。
- ScheduledExecutorService - 扩展了 ExecutorService 接口。扩展能力:支持定期执行任务。
- AbstractExecutorService - ExecutorService 接口的默认实现。
- ThreadPoolExecutor - Executor 框架最核心的类,它继承了 AbstractExecutorService 类。
- ScheduledThreadPoolExecutor - ScheduledExecutorService 接口的实现,一个可定时调度任务的线程池。
- Executors - 可以通过调用 Executors 的静态工厂方法来创建线程池并返回一个 ExecutorService 对象。
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。首先我们先看构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。很显然,第四个构造方法有7个参数,我们来解释一下7个参数代表什么意思:
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把之后到达的任务放到缓存队列当中。
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程。
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
- unit:参数keepAliveTime的时间单位,有7种取值(天、小时、分钟、秒、毫秒、微秒、纳秒)。
- workQueue:一个阻塞队列,用来存储等待执行的任务,有四种实现(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue)。
- threadFactory:线程工厂,主要用来创建线程。
- handler:饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。饱和策略有四种(丢弃任务并抛出RejectedExecutionException异常、丢弃任务但是不抛出异常、丢弃队列最前面的任务然后重新尝试执行任务、由调用线程处理该任务)。
在ThreadPoolExecutor类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。shutdown()和shutdownNow()是用来关闭线程池的。
还有很多其他的方法,比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。
(三)线程池实现原理
我们先看一个总体的流程图:
1. 线程池状态
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
下面的图展示了上面讲到的5种状态:
- RUNNING - 运行状态。接受新任务,并且也能处理阻塞队列中的任务。
- SHUTDOWN - 关闭状态。不接受新任务,但可以处理阻塞队列中的任务。
- STOP - 停止状态。不接受新任务,也不处理队列中的任务。会中断正在处理任务的线程。在线程池处于RUNNING或 SHUTDOWN 状态时,调用shutdownNow方法会使线程池进入到该状态。
- TIDYING - 整理状态。如果所有的任务都已终止了,workerCount (有效线程数) 为 0,线程池进入该状态后会调用terminated方法进入TERMINATED状态。
- TERMINATED - 已终止状态。在terminated方法执行完后进入该状态。默认terminated方法中什么也没有做。
2. 重要成员变量
我们先来看一下ThreadPoolExecutor类中其他的一些比较重要成员变量:
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存活时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int maximumPoolSize; //线程池最大能容纳的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。corePoolSize在很多地方被翻译成核心池大小,其实我的理解这个就是线程池的大小。举个简单的例子:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做。当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待。如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来。然后就将任务也分配给这4个临时工人做。如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
3. 任务调度
下面我们从execute方法开始讲述任务是如何调度的:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//获取到成员变量ctl的值(线程池状态)
int c = ctl.get();
//如果工作线程的数量<核心池的大小
if (workerCountOf(c) < corePoolSize) {
//调用addWorker(这里传入的true代表工作线程数量<核心池大小)
//如果成功,方法结束。
if (addWorker(command, true))
return;
//否则,再重新获取一次ctl的值
//防止前面这段代码执行的时候有其他线程改变了ctl的值。
c = ctl.get();
}
//如果工作线程数量>=核心池的大小或者上一步调用addWorker返回false,继续走到下面
//如果线程池处于运行状态,并且成功将当前任务放入任务队列
if (isRunning(c) && workQueue.offer(command)) {
//为了线程安全,重新获取ctl的值
int recheck = ctl.get();
//如果线程池不处于运行状态并且任务从任务队列移除成功
if (! isRunning(recheck) && remove(command))
//调用reject拒绝执行,根据handler的实现类抛出异常或者其他操作
reject(command);
//否则,如果工作线程数量==0,调用addWorker并传入null和false
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//执行到这里代表当前线程已超越了核心线程且任务提交到任务队列失败。(可以注意这里的addWorker是false)
//那么这里再次调用addWroker创建新线程(这时创建的线程是maximumPoolSize)。
//如果还是提交任务失败则调用reject处理失败任务
else if (!addWorker(command, false))
reject(command);
}
4. 任务执行
任务执行离不开Worker对象。Worker就是所谓的工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务。firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // 这边将自己作为参数传递给thread对象
}
public void run() {
runWorker(this);
}
......
}
在这段Worker源码中我们只需要注意里面的run()方法和构造方法,因为在下面要讲的线程启动时会调用这个run()方法,这个run()方法里面又调用了runWorker()方法,传参就是Worker对象本身。可能感觉有点绕,先看流程图梳理一下:
上面讲execute方法时提到了里面的addWorker方法,这个方法就是提交任务,并且新建线程并启动的源头,下面我们来看这个addWorker方法:
private boolean addWorker(Runnable firstTask, boolean core) {
//上来就是retry,后面continue retry;语句执行之后都会从这里重新开始
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);//获取线程池运行状态
/*
* 获取当前线程池的状态,如果是STOP,TIDYING,TERMINATED状态的话,则会返回false
* 如果现在状态是SHUTDOWN,但是firstTask不为空或者workQueue为空的话,那么直接返回false
*/
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);//获取工作线程数量
/*
* addWorker传入的第二个Boolean参数用来判别当前线程数量是否大于核心池数量
* true,代表当前线程数量小于核心池数量
*/
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//增加工作线程数量
if (compareAndIncrementWorkerCount(c))
//如果成功,跳出retry
break retry;
c = ctl.get();
//判断线程池状态,改变了就重试一次
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 {
/*
* 前面顺利增加了工作线程数,那么这里就真正创建Worker
* 上面流程图中说过,创建Worker时会创建新的线程.如果这里创建失败
* finally中会将工作线程数-1
*/
w = new Worker(firstTask);
final Thread t = w.thread; // 取出Worker对象中的线程
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());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);//将Worker放入工作线程池
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();//启动线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
很明显,上面代码的t.start()方法启动了Worker对象里的线程,线程调用了Worker对象里面的run()方法,run()方法调用了runWorker()方法。下面我们来看runWorker()方法的源码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; //获取传入的Worker对象中的任务
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
/*
* 如果传入的任务为null,就从任务队列中获取任务,只要这两者有一个不为null,就进入循环体
*/
while (task != null || (task = getTask()) != null) {
w.lock();
//如果线程池状态已被标为停止,那么则不允许该线程继续执行任务!或者该线程已是中断状态,
//也不允许执行任务,还需要中断该线程!
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);
}
}
从源码中我们不难发现,可以看出runWorker(Worker w)实际上已经是在线程启动之后执行任务了,所以其主要逻辑就是获取任务,然后执行任务的run方法。也就是说要么我们提供一个任务,要么自己去阻塞队列中取一个任务,即调用getTask()方法。
下面我们也来看一下getTask()方法的源码:
private Runnable getTask() {
//记录循环体中上个循环在从阻塞队列中取任务时是否超时
boolean timedOut = false;
//无条件循环,主要是在线程池运行正常情况下
//通过循环体内部的阻塞队列的阻塞时间,来控制当前线程的超时时间
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);//获取线程池状态
/*先获取线程池的状态,如果状态大于等于STOP,也就是STOP、TIDYING、TERMINATED之一
*这时候不管队列中有没有任务,都不用去执行了;
*如果线程池的状态为SHUTDOWN且队列中没有任务了,也不用继续执行了
*将工作线程数量-1,并且返回null
**/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//获取工作线程数量
int wc = workerCountOf(c);
//是否启用超时参数keepAliveTime
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//如果这个条件成立,如果工作线程数量-1成功,返回null,否则跳出循环
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
//如果需要采用阻塞形式获取,那么就poll设定阻塞时间,否则take无限期等待。
//这里通过阻塞时间,间接控制了超时的值,如果取值超时,意味着这个线程在超时时间内处于空闲状态
//那么下一个循环,将会return null并且线程数量-1
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
5. 线程回收
线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
- lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
- 如果正在执行任务,则不应该中断线程。
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
- 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态,如果线程是空闲状态则可以安全回收。
这里会涉及到AQS的一部分知识,如果对AQS不熟悉可以略过此部分或者简单去看一下AQS的相关知识。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
Worker(Runnable firstTask) {
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
}
在线程回收过程中就使用到了这种特性,回收过程如下图所示:
2020年11月28日