引言
面试官:如果让你设计线程池,你会怎么设计?
小贱:… 发生肾么事情了,面试官你不讲码德。
面试官:出门右拐,坐三轮车走成华大道到二仙桥。
对于线程池,有经验的程序员一定不会陌生,在Java中用Executor框架,啪的一下,很快啊就搞定了。实现一个线程有多种方式,我们为什么要使用线程池呢?
我们为什么要用线程池
在Hotspot虚拟机中,Java线程与操作系统的线程是一一对应,所以线程的创建与销毁都需要与操作系统线程同步。对于cpu密集型的线程任务,这样做无疑是灾难性的,因为频繁的创建与销毁线程会消耗大量的资源,所以我们引入了线程池来对线程进行管理。
线程池主要有以下几个优点:
- 通过线程复用,避免频繁创建与销毁线程,节约资源的同时,提高了响应速度;
- 通过线程池,方便我们管理线程;
- 通过线程池,我们可以控制最大并发数,避免无限制创建线程导致系统资源消耗殆尽,最终导致系统挂掉(限流组件hystrix中就有通过线程池的方式来进行限流)。
其实这种“池化”的思想很常见,比如:数据库中连接池、tomcat连接池,继续回到本文要分析的在Executor线程池。
线程池分析
如下图所示,在Executor框架,关于线程池的主要有两个关键的实现类ThreadPoolExecutor及ScheduledThreadPoolExecutor,后者也是基于前者实现的,所以本文重点分析前者。
老规矩,既然要使用,肯定要先构造一个对象实例。线程池的基本原理要从ThreadPoolExecutor入手,所以我们先扒开ThreadPoolExecutor的源码,可以发现根据参数的不同,它有很多构造函数,最基础的构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
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;
}
从源码中可以发现,构造函数中参数也很多,下面我们分析这些参数的作用:
- corePoolSize: 线程池中核心线程数,当线程池中的线程数量小于corePoolSize时,添加一个任务,会新创建一个线程来执行任务;
- maximumPoolSize: 线程池中最大线程数,当工作队列满了以后,如果有新的任务添加进来,线程池会创建新的线程来执行任务。线程池中的线程数大于maximumPoolSize时,新添加的任务会触发线程池的拒绝策略;
- workQueue: 工作队列,即线程池的等待队列,当线程池中的线程数量等于corePoolSize时,新添加的任务会被放入等待队列中;
- handler: 拒绝策略,框架中已经帮我们定义好了4种拒绝策略:
AbortPolicy:丢弃并抛出异常,线程池默认拒绝策略;
CallerRunsPolicy:用调用者来执行任务,该策略下任务一定会被执行;
DiscardPolicy: 丢弃任务,但是不抛出任何异常;
DiscardOldestPolicy:丢弃队列中最老的任务;
除此之外,还可以自己定义拒绝策略,只需要实现RejectedExecutionHandler
接口的rejectedExecution(...)
方法即可。
- keepAliveTime: 非核心线程(最大线程-核心线程)闲置时最大的存活时间,超过这个时间仍然空闲的话,非核心线程将会被回收;
- unit: 非核心线程闲置最大存活时间的时间单位,比如:TimeUnit.SECONDS:秒,TimeUnit.MINUTES:分;
- threadFactory: 创建新线程的线程工厂。
综上,我们可以总体流程如下图所示:
具体我们分析源码,任务提交源码如下
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get(); //获取线程池状态
if (workerCountOf(c) < corePoolSize) { //如果workerCount小于核心线程数
if (addWorker(command, true)) //添加新线程
return; //成功就返回
c = ctl.get(); //失败就再次获取线程池状态
}
//如果workerCount大于等于核心线程数,判断线程池状态,并添加任务到等待队列
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);
}
else if (!addWorker(command, false))
//如果workerCount大于maximumPoolSize,拒绝策略
reject(command);
}
从代码中可以发现,任务提交就主要思想就是:根据线程池状态,判断是否应该新开线程、添加到队列还是应该拒绝,基本上和流程图吻合。
接着我们继续看看添加任务的方法:addWorker(...)
//workers集合
private final HashSet<Worker> workers = new HashSet<Worker>();
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c); //获取线程池状态
// Check if queue empty only if necessary.
//线程池状态大于等于SHUTDOWN,说明已经进入关闭阶段,所以不应该添加
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;//当线程数大于corePoolSize或者maximumPoolSize 直接返回false
if (compareAndIncrementWorkerCount(c)) //CAS添加线程数
break retry; //跳出retry循环
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs) //如果线程池状态发生改变,重新retry循环
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//前面已经完成了workerCount+1 下面才开始添加新的worker
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);//创建一个新的worker对象
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());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);//将woker加入到work集合
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); //将前面的workerCount数-1
}
return workerStarted;//返回是否添加成功
}
该方法有两个参数Runnable firstTask
表示任务, boolean core
布尔类型变量core如果为true表示添加核心线程,否者表示添加普通线程。整体分为两大部分:
- 通过CAS 将workerCount加1;
- 添加worker对象到wokers集合。
从上述分析中,我们可以发现,任务提交时,会判断线程池是否处于RUNNING状态,那么线程池的状态有哪些?又是如何表示的呢?(小朋友你是否有很多问号)
在源码中有一个巧妙的设计,就是ThreadPoolExecutor的成员变量ctl,这是一个原子类型的复合整型变量,其底层通过CAS和volatile来保证线程安全,其中前3位表示线程池状态,后29位表示有效工作线程数,结构如下图所示。
相关源码如下:
从源码中可以看到线程池的状态用整型变量表示,一共有:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED
五种状态,具体的整型值还需要通过位计算得到,看上去很复杂,但是具体的值我觉得根本不用去记,只需要抓住重点就好,有以下几个重点:
- RUNNING状态值是唯一为负的状态值,可以简单记为-1,相应的SHUTDOWN可以记为0,STOP记为1,以此类推;
- 线程池状态的迁移,只能从小到大,不能逆向迁移,即可以从-1->0,但是不可能0->-1。
具体的状态迁移如下图所示:
shutdown()和shutdownNow()的区别及实现原理
从上图中我们可以发现,调用shutdown()或shutdownNow()方法后,线程池并不会马上关闭,而是需要一个过程,最终调用terminated()
函数后才会真正关闭线程池,那么shutdown()和shutdownNow()有什么区别呢?
先说结论:
- shutdown()不会清空任务队列,会等待所有任务执行完成,shutdownNow()会清空任务队列;
- shutdown()只中断空闲线程,shutdownNow()会中断所有线程。
接着我们再通过源码分析下它们的实现原理,源码如下:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //获取锁
try {
checkShutdownAccess();//检查是否有关闭线程池权限
advanceRunState(SHUTDOWN);//把线程池状态设为SHUTDOWN
interruptIdleWorkers(); //中断空闲线程
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();//释放锁
}
tryTerminate();
}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();//获取锁
try {
checkShutdownAccess();//检查是否有关闭线程池权限
advanceRunState(STOP);//把线程池状态设为STOP
interruptWorkers();//中断所有线程
tasks = drainQueue();//清空队列
} finally {
mainLock.unlock();//释放锁
}
tryTerminate();
return tasks;
}
其实两个方法的执行过程还是比较简单的,根据上面讲的线程池生命周期及区别能比较好的理解,关键区别在于interruptIdleWorkers()
和interruptWorkers()
这两个方法,前者只中断空闲线程,后者中断所有线程。 线程池是如何判断一个线程是否是空闲线程的呢?源码下面无秘密,接着扒源码,如下:
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();//加锁
try {
for (Worker w : workers) { //遍历所有的Worker对象
Thread t = w.thread;//取出对象中的线程
//w.tryLock()尝试获取锁,如果获取成功则说明线程空闲,
//获取失败则说明线程正在执行某个任务
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt(); //中断线程
} catch (SecurityException ignore) {
} finally {
w.unlock(); //释放锁
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();//释放锁
}
}
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted(); //不尝试获取锁,一律直接发送中断信号
} finally {
mainLock.unlock();
}
}
从上面可以看到,判断一个线程是否为空闲线程的关键在于:Worker对象是否能加锁成功,如果能加锁成功,则说明是空闲线程,如果加锁失败则说明正在执行某个任务。所以我们盲猜一下,Work对象在进行任务获取时,肯定会先获取锁,那么Worker到底是什么呢?
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable{
...
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
...
}
朋友们,看到Worker继承了AbstractQueuedSynchronizer是不是恍然大悟了,这玩意继承了AQS啊,AQS我应该不用多讲了吧,不知道的可以看看,所以它的本质就是一把锁,上面代码中只摘录了部分源码:
- 构造函数:可以看到把锁的状态置为-1,并把任务作为firstTask赋给该Worker。
- run()方法:这个方法是比较关键的一个方法,通过该方法执行任务,是通过调用ThreadPool 的
runWorker(...)
方法来实现。
任务执行过程分析
源码如下所示:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;//获取任务
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
//通过getTask()不断从队列中获取任务执行
w.lock();//执行任务前要先加锁,和我们猜的一模一样
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
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();//执行任务的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++;//成功完成任务,completedTasks+1
w.unlock();//释放锁
}
}
completedAbruptly = false;//判断worker是否正常退出
} finally {
processWorkerExit(w, completedAbruptly);//worker退出
}
}
总结任务执行过程:
- 获取worker对象的task,如果为空,则通过getTask()去队列中获取,如果获取不到,则一直阻塞,直到获取到任务或者被中断,如果是被中断则转到步骤8,且记录work为非正常退出;
- 获取到任务后,work对象获取锁;
- 判断当前线程池状态,如果是STOP状态则不允许执行任务;
- 执行任务开始前的钩子函数;
- 调用任务run()方法执行任务;
- 执行任务完成后的钩子函数;
- 累加成功任务数,释放锁;
- worker退出。
任务获取
在上面的过程中,总体流程都比较清晰,其中重要的两个方法是获取任务的getTask()
和work退出的processWorkerExit(...)
,接着我们再分析这两个方法,源码如下:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get(); //获取ctl
int rs = runStateOf(c); //获取线程池状态
// Check if queue empty only if necessary.
//关键点 1
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();//CAS 减少线程数
return null;//return null 则 runWorker(..)中退出while循环
}
int wc = workerCountOf(c); //获取工作线程数
// Are workers subject to culling?
//allowCoreThreadTimeOut 是否允许核心线程超时,默认为false
//timed 表示需不需要做超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//如果工作线程数>maximumPoolSize 或者 超时且需要做超时控制
//且工作线程数>1,且工作队列为空
if ((wc > maximumPoolSize || (timed && timedOut)) //关键点2
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;//return null 则 runWorker(..)中退出while循环
continue;
}
try {
//关键点 3
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
//如果获取到任务,返回任务
if (r != null)
return r;
//超过超时等待都没有获取到任务
timedOut = true;
} catch (InterruptedException retry) {
//中断设置为false,进入下一次循环
timedOut = false;
}
}
}
在源码中有两个比较关键的地方:
- 根据线程池状态rs判断是否应该返回null:
如果rs >= SHUTDOWN,说明调用了shutdown()方法且此时等待队列为空
workQueue.isEmpty()
,则说明没有任务可以获取,返回null;
如果rs >= STOP,即调用了shutdownNow()方法,此处返回为空。
当getTask返回null 则 runWorker(…)中退出while循环。
-
通过关键点2处的判断,来控制线程池中线程的数量尽量维持在corePoolSize内;
-
队列为空时,poll(…)或者take()就会阻塞,它们的区别就是poll()带超时,take()不带;一旦捕获到异常,就会响应中断。
总结任务获取过程:
- 首先通过一些方法获取线程池相关信息,并根据线程池状态判断是否返回为空;
- 通过一系列判断来维持线程池中线程的数量最好在核心线程数以内;
- 通过阻塞队列获取任务,如果为空则进行阻塞,并会响应中断。
流程图如下:
Worker对象退出
接着分析Wokrer对象的退出方法:processWorkerExit(...)
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
//当非正常退出线程将线程数减1
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);//把自己从workers中移除
} finally {
mainLock.unlock();
}
//和shutdown()中一样,尝试调用这个方法将线程池关闭
tryTerminate();
int c = ctl.get();
//当要退出时,检查线程池状态如果小于STOP,且队列不为空,
//但是当前没有可以执行的工作线程,
//则添加一个新的线程去消耗队列中的任务
//起到一个保证线程池中的正常任务一定会被执行的作用
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
总结woker退出时,清理工作:
- 判断是否为非正常退出,如果是非正常退出则通过CAS将线程数-1,正常退出的线程在getTask()中已经通过CAS将线程数减1;
- 会尝试调用tryTerminate()方法来关闭线程池;
- 判断当前线程池是否需要新添加线程来保证未执行的任务会被执行。
线程池关闭
在线程池的生命周期中我们也提及到线程池最终会通过terminated()
钩子函数来关闭线程池,在shutdown(), shutdownNow()及processWorkerExit(…)三个方法中都通过tryTerminate()
来关闭线程池,所以接着分析线程池的关闭方法tryTerminate()
:
final void tryTerminate() {
for (;;) {
int c = ctl.get();//获取线程池状态
//如果线程池状态为RUNNING直接返回,不需要关闭
//如果线程池状态大于TIDYING,已经到了关闭,不需要关闭,直接返回
//如果是SHUTDOWN状态,且等待队列不为空,说明还需要执行任务,不需要关闭,直接返回
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//workerCount大于0,不能停止线程池,需要唤醒那些等待的空闲线程
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
//当等待队列为空且workerCount为0,且等待队列为空时
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try { //通过CAS改变线程池状态为TIDYING
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();//钩子函数关闭线程池
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
如何优雅的关闭线程池
从上面的分析我们已经知道,线程池的关闭是一个过程,那么我们应该如何优雅的关闭线程池呢?
先看怎么用
executorService.shutdown();
//或者
executorService.shutdownNow();
try {
boolean loop = true;
do {
loop = !executorService.awaitTermination(2, TimeUnit.SECONDS);
//阻塞, 直到线程池里所有任务结束
} while (loop);
} catch (InterruptedException ex) {
...
}
可以看到我们可以调用awaitTermination(...)
方法来等待线程池关闭。
private final Condition termination = mainLock.newCondition();
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
可以看到awaitTermination(...)
方法很简单,两个参数:timeout是阻塞时间,unit是时间单位。内部的逻辑也很清晰,就是不断循环判断线程池是否已经达到了最终TERMINATED
状态,是就返回true,不是的话通过条件变量termination
来阻塞一段时间,苏醒继续判断。
Executors工具类提供的线程池
为了便于我们开发使用线程池,Executors工具类提供了几个线程池工程方法,具体如下:
public class Executors {
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 ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
}
- newFixedThreadPool(int nThreads):固定大小的线程池
a. corePoolSize等于maximumPoolSize,即该线程池全部是核心线程;
b. 该线程池的等待队列为LinkedBlockingQueue
为无界队列,使用时可能会出现不断往队列里添加任务导致系统资源耗尽,最终OOM。
- newSingleThreadExecutor():核心线程数和最大线程数均为1的固定大小线程池
同样,该线程池的等待队列为
LinkedBlockingQueue
为无界队列,使用时可能会出现不断往队列里添加任务导致系统资源耗尽,最终OOM。
- newCachedThreadPool():可缓存的线程池
a. 该线程池中没有核心线程,最大线程数为Integer.NAX_VALUE,基本相当于无限大,即有需要就会创建线程来执行任务,没有需要就回收线程;
b. 该线程池等待队列为SynchronousQueue
,该队列本身没有容量,一个线程调用put()会阻塞,直到另一线程调用get(),然后两线程同时解锁,所以使用时需要注意,如果生产速度大于消费速度,会导致创建很多线程,最终OOM。
- newWorkStealingPool():一种基于ForkJoinPool线程池实现的线程池。
综上: 我们在使用线程池时,最好使用new ThreadPoolExecutor(…)的方式来构造,这样做到线程池状态心里有数从而可控,避免无界队列或者创建大量线程导致资源耗尽,导致OOM,最终影响系统。
看到这里了,各位大帅逼和大漂亮就来个素质三连,点赞,评论,关注吧。