线程池的实现原理以及业务中的实践
一个体面人的技术之旅。
一 、写在前面
1.1 线程池是什么?
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
线程过多会带来额外的开销,其中包括创建销毁线程的开销(java线程对应操作系统的线程,创建线程需要切换用户状态,和占用内存等资源,而其大部分操作系统都会限制系统中的最大线程数量)、调度线程的开销(涉及到线程挂起和恢复,挂起需要记录线程状态)等等,同时也降低了计算机的整体性能。
*用户态和内核态切换的代价*
当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。
<https://www.jianshu.com/p/eac05373f30f>
1.2 线程池的好处
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
1.3 线程池能解决的问题
三个方面:
- 如果没有线程池的服用特性,线程会频繁的创建/销毁。会带来额外的消耗。
- 如果没用线程池的线程个数控制,则可能产生系统资源耗尽的风险。
- 如果没用线程池的管理策略,系统内部是不支持自定义管理线程的,高并发环境下会降低系统的稳定性。
二、线程池的核心设计与实现
这一部分我们主要通过jdk1.8的ThreadPoolExecutor源码来分析线程池的核心设计和逻辑。
2.1 总体设计
首先看一下ThreadPoolExecutor的类结构。

其顶层接口是Executor,该接口的作用是:将任务的提交和任务的执行进行解耦。用户不用关心提交的任务是何时,怎样,什么方式来运行的,只需要负责将任务提交出去就好。大白话解释就是,任务传递,将任务看作参数传递给别人,别人去处理。这样就和我们解耦了。
用代码解释一下,原始的方式:new Thread(new (RunnableTask())).start();线程池的方式 Executor e = an Executor; e.execute(new RunnableTask());e.execute(new RunnableTask2())
ExecutorService接口是对Executor的拓展,增加了如下功能:(1) 一个或者多个异步任务运行生成Future的方法;(2)提供了管控线程池的方法,停止线程池。
AbstarctExecutorService是上层的抽象类,实现了除了execute()、**shutdown()**的其它大部分方法,保证了下层的实现只需要关注execute()等未实现的方法即可。
最下层的ThreadPoolExecutor类实现了最复杂、最核心的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
如下面运行机制核心图所示:

线程池内部通过阻塞队列workQueue来实现一个生产者消费者模型,将线程和任务解耦,使用者将任务提交到workQueue,线程从其中获取任务,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行(循环),最终当线程获取不到任务的时候,线程就会被回收。
从上面的核心逻辑我们有如下三个问题:
-
线程池是根据什么对一个新增任务的判断?
线程池维护了自身的状态,根据新任务添加时自身状态来对任务接下来的流程进行判断。
-
线程池如果管理任务的呢?
-
线程池又是如何管理线程的?
下面我们会针对这三方面进行详细解释。
2.2 生命周期管理
上面第一个问题提到了线程池的状态,我们知道所有的api是没有设置线程池状态的,所有线程池的状态其实是其自身维护的。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起,如下代码所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
我们来详细解释一下这个位运算,先看一下ctlOf的源码;private static int ctlOf(int rs, int wc) { return rs | wc; }可以看到它做了一个或的操作。我们在看rs的值都有哪些
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
可以看到全部是一个int向左位移一定的位数,那么其右边的bit上都是0,那么做了或操作后,高位即保留了其自身的状态,低位也保留了wc的数量。eg:ctlOf(-1,0)------->1001 0000 0000 0000 0000 0000 0000 0000
关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如以下代码所示:
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
与上1110 00000....,就是c的高三位的值,刚刚好是状态值。
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
这里解释一下上面线程池状态的意义:
| 运行状态 | 状态描述 |
|---|---|
| RUNNING | 能接收新任务,并且也可以处理阻塞队列中的任务。 |
| SHUTDOWN | 关闭状态,不能接收新任务,但是可以处理阻塞队列中的任务。 |
| STOP | 不能接收新任务,不能处理阻塞队列中的任务,会中断正在处理任务的线程。 |
| TIDYING | 所有任务都终止,wokerCount(有效线程数)为0. |
| TERMINATED | 在terminated()方法执行完后进入该状态 |
状态转换图入下图所示:

2.3 任务执行机制
2.3.1 任务调度
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
- 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
下面根据源码分析一下:
public void execute(Runnable command) {
//空任务直接抛异常
if (command == null)
throw new NullPointerException();
//获取线程池的状态和workCount
int c = ctl.get();
//如果小于核心线程数,直接创建线程并执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
//如果直接执行失败,(这里可能是线程池不是RUNING状态)因为并发所有这里,再次获取最新的
//线程池状态和workCount
c = ctl.get();
}
//如果线程是RUNING,将任务放到阻塞队列(如果没满,则if 为true,进入线程池状态判断状态,如果状态正常流程结束
//如果线程池不是RUNING了,移除刚刚放到队列的任务,并拒绝该任务
//如果线程池的工作线程数为0了,那么要新建一个工作线程。
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))
//如果创建线程失败,则拒绝该任务
reject(command);
}
2.3.2 任务缓冲
向上面提到的,线程池的核心思想是将任务和线程解耦,那么就需要把用户提交的任务存储在线程池中,等待工作线程的获取。那么存储任务就是对任务的缓冲,是通过BlockingQueue来实现的。不同的阻塞队列可以实现不同的缓冲机制,这也是我们可以自定义功能的一个拓展点。下面我们介绍一下阻塞队列的成员:
| 名称 | 描述 |
|---|---|
| ArrayBlockingQueue | 一个用数组实现的有界阻塞队列,先进先出(FIFO)的原则,支持公平锁和非公平锁。 |
| LinkedBlockingQueue | 一个由联表组成的有界队列,FIFO原则。注意此队列的默认长度为Integer.MAX_VALUE, 所以最好指定容量。 |
| PriorityBlockingQueue | 一个支持线程优先级排序的无界队列,默认自然序排序,也可以自定义compareTo()方法来指定元素排序规则,不能保证同优先级元素顺序。 |
| DelayQueue | 一个实现了PrioirtyBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。 |
| SynchronousQueue | 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个场景就是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。 |
| LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法 |
| LinkedBlockingDeque | 双端的链表,多线程并发,可以将锁的竞争最多降到一半。 |
所有的阻塞队列的实现机制,大致上都是 ReentLock 和 两个Condition notEmpty notFull.在对其数据结构进行操作时,通过Condition.await()/singal()来实现线程通信。
2.3.3 任务申请
由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况(Worker 中的 getTask())。
流程图如下:

getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。
源码分析:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?上一次获取任务是否超时
//循环获取
for (;;) {
//获取此时线程池的状态和工作线程数
int c = ctl.get();
//获取线程池的状态
int rs = runStateOf(c);
// 判断线程池是否停止
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
//cas操作workCount减一,这里就是表明工作线程数减少一
decrementWorkerCount();
//返回null
return null;
}
//获取工作线程数
int wc = workerCountOf(c);
//判断工作线程是否可以被淘汰
//通过allowCoreThreadTimeOut 和当前工作线程数是否大于核心线程数决定
//allowCoreThreadTimeOut 默认为false,也就是不会回收工作线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//线程数是否大于最大线程或者允许回收线程并且上一次获取任务超时
//并且,工作线程数大于一,其任务队列没有任务。
//这时候返回null
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//可回收线程先是获取任务
//不可回收线程,阻塞获取任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
//标记该线程上一次获取任务超时
timedOut = true;
} catch (InterruptedException retry) {
//发生异常,标记未获取超时,继续循环获取
timedOut = false;
}
}
}
重点掌握,什么时候可以线程获取不到任务。必须是超过线程池的最大数量,没超过最大线程数,但是可回收线程上一次获取超时,加上当前线程池还存在其它工作线程,如果不存在其它线程,但是任务队列为空的时候。这个条件常常是面试的问题, 这里就要讨论,线程未获取到任务的情况,是超时未获取到,还是发生了异常。如果是超时,还需要看当前线程池的还存不存在其它线程,如果不存在其它线程,要在判断任务队列是否为空。如果是发生了异常,那么还需要考虑allowCoreThreadTimeOut参数。
2.3.4 任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
看一下ThreadPoolExecutor的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
得出拒绝策略是一个接口,其设计如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
主要调用在前面讲的任务调度那里。
- 在讲任务放到队列中后,重新检查线程池状态,如果线程池不是运行状态,并且从任务队列移除成功,则拒绝该任务
- 如果线程池中超过最大线程数,则调用
我们可以通过实现上面接口,来拓展我们自己的线程池。当然JDK已经提供了四种策略。其特点如下:
| 名称 | 描述 |
|---|---|
| ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。这是线程池默认的拒绝策略,在任务不能在提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此策略,因为可以通过异常即时捕获到系统的问题。 |
| ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,无声无息。建议一些无关的任务用此策略。 |
| ThreadPoolExecutor.DiscardPolicyOldestPolicy | 丢弃队列最前面的任务,然后重新提交该任务。 |
| ThreadPoolExecutor.CallerRunsPolicy | 由调用线程(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕。业务上必须要执行完成每一个任务。 |
2.4 Worker线程管理
worker线程,是线程池设计的特殊工作线程。
2.4.1Worker线程
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;//Worker持有的线程
Runnable firstTask;//初始化的任务,可以为null
volatile long completedTasks; 所完成任务数量
}
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程(new Thread(Task)),可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。
Worker执行任务得模型如下图所示:

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表HashSet<Worker> workers = new HashSet<Worker>()去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期(最终还是通过JVM去回收的,不是立马就回收掉)。这个时候重要的就是如何判断线程是否在运行。
判断Worker是不是在运行,其实可以用一个标志位来监控,当运行runWorker()和正在执行task.run()的时候去改变这个状态,比如 Volatile int state. 其实在jdk中这种思想早就存在轮子,不需要造轮子了。它就是大名鼎鼎的AQS。
Worker是通过继承AQS,使用AQS来实现独占锁这个功能。
这里讲一下本人的理解,为什么要给Worker加锁,其实英文注释讲的很明白,This protects against interrupts that are intended to wake up a worker thread waiting for a task from instead interrupting a task being run。 翻译过来是 该锁会保护一个正在等待任务被执行的Worker不被interrupt操作打断。也就是说,如果worker 执行了runWorker,并且未上锁,如果runWorker里面存在 thread.sleep condition.await等可以被thread.interpret()打断的point,那么当其它方法,比如shoutdown在终结线程时,它不能根据锁去判断该线程是不是在运行(因为我们假设没上锁),这时候它会对所有线程执行thread.interpret(),那么我们这个正在工作的worker就被打断了。而jdk的设计者并不希望正在执行的线程被打断,就用锁机制来限制。加了锁之后,在shutdown()清理线程的时候,它会先tryLock(),如果获取到锁,说明worker空闲可以打断回收。这个点也是为什么jdk作者说使用AQS,而不是ReentrantLock的原因。这一点下面在说。
为什么不使用ReentrantLock而使用AQS呢?
这里说一下,这个问题本人也是想了很久,最后终于想明白了。其实jdk的注释里面也已经说了。我们先看一下原文:
We implement a simple
* non-reentrant mutual exclusion lock rather than use
* ReentrantLock because we do not want worker tasks to be able to
* reacquire the lock when they invoke pool control methods like
* setCorePoolSize.
原文的意思:我实现了一个简单的非重入排它锁,而不是直接使用reentrankLock是因为我们不想当worker中的任务去执行操作线程池的方法,如 setCorePoolSize,时可以通过tryLock()获取到锁。
那么这句话什么意思?看下面:
setCorePoolSize()等其它操作线程池控制的方法源码中都含有同一个方法->
interruptIdleWorkers();这个方法是打断空闲线程用的,我们再看它的实现->
它是通过(!t.isInterrupted() && w.tryLock()) 这个判断条件来判断是不是可以打断当前线程,
如果是ReentrantLock,那么即时这个线程不是打断状态(说明在执行),但是它可重入,导致tryLock为true,从而导致了打断了运行状态中的worker。
ok 明白了把。确实很绕,佩服作者。
那么这里总结一下:
- lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
- 如果正在执行任务,则不应该中断线程。
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
- 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
在线程回收过程中就使用到了这种特性,回收过程如下图所示:

2.4.2 Worker线程增加
增加线程是通过线程池中的addWorker方法(最初的入口时execute()),该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

2.4.3 Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可(final的类成员变量,是gc root)。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务(默认allowCoreThreadTimeOut= false. 如果为true,核心线程获取也是有超时时间的),非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用,processWorkerExit(w, completedAbruptly)。
线程回收的工作流程如下图所示:

事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但由于引起线程销毁的可能性有很多,线程池还要判断是什么引发了这次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,重新分配线程。
代码解析:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//判断线程是不是因为异常而获取不到任务
if (completedAbruptly)
// 如果是异常打断的,就不需要减少workerCount,如果是正常的获取不到任务,那么先减少
//线程池中的工作线程数量
decrementWorkerCount();
//因为要更改成员变量,就是线程引用的集合,所以拿到锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//记录完成任务数
completedTaskCount += w.completedTasks;
//在线程集合中,移除该线程引用
workers.remove(w);
} finally {
mainLock.unlock();
}
//因为有线程拿不到任务了,可能是线程池停止工作了,所以这里尝试把线程池变成terminate状态
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);
}
}
2.4.4 Worker线程执行任务
在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:
- while循环不断地通过getTask()方法获取任务。
- getTask()方法从阻塞队列中取任务。
- 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
- 执行任务。
- 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
执行流程如下图所示:

代码
final void runWorker(Worker w) {
//获取当前线程,也就是Worker里面的线程
Thread wt = Thread.currentThread();
//拿到任务
Runnable task = w.firstTask;
//清楚worker线程的任务属性
w.firstTask = null;
//将worker的状态改为0 ,这时候是可以中断该worker的
w.unlock(); // allow interrupts
//设置worker是不是突然中断的标志位,默认是突然中断
boolean completedAbruptly = true;
try {
//循环获取任务
while (task != null || (task = getTask()) != null) {
//如果拿到了任务,或者原本持有任务,则上锁,表示该worker正在执行任务,不可以打断
w.lock();
// 如果此时,线程池是stop的,那么要保证执行线程可以打断;
// 如果不是stop, 保证 thread 不是被打断的.
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();
}
}
//正常完成,将标志位设置为true
completedAbruptly = false;
} finally {
//如果获取不到任务,退出该线程
processWorkerExit(w, completedAbruptly);
}
}
三 、线程池设置管理的场景
3.1 业务场景
场景1:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
场景2:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
参考文献
- Java线程池实现原理及其在美团业务中的实践
- JDK 1.8 ThreadPoolExecutor源码

本文深入解析了Java线程池的实现原理,包括线程池状态管理、任务调度与缓冲、线程创建与回收,以及在实际业务中的应用场景,如快速响应用户请求和处理大数据任务。
271

被折叠的 条评论
为什么被折叠?



