Lazy loaded image
🗒️JDK线程池源码分析
Words 4171Read Time 11 min
2025-10-23
2025-11-13
type
status
date
slug
summary
tags
category
icon
password
原文
notion image

线程池状态维护

线程池状态只能单向转换
RUNNING → SHUTDOWN → TIDYING → TERMINATED
RUNNING → STOP → TIDYING → TERMINATED
SHUTDOWN: 关闭中 不允许接受新任务,允许处理阻塞队列中已存在的任务,正在执行的任务会继续执行
STOP: 停止状态,不允许接受新任务,队列中剩余任务会被抛弃,立即中断所有正在执行的任务
TIDYING:所有任务已终止,工作线程为0,准备执行终止前的最后操作(如钩子方法 terminated())
TERMINATED: 已终止,所有资源已是否,钩子方法terminated()执行完毕。
具体触发条件:
  • RUNNING → SHUTDOWN:调用 shutdown()。
  • RUNNING/SHUTDOWN → STOP:调用 shutdownNow()。
  • SHUTDOWN → TIDYING:队列空且所有任务执行完毕,工作线程数为 0。
  • STOP → TIDYING:所有正在执行的任务被中断,工作线程数为 0。
  • TIDYING → TERMINATED:terminated() 钩子方法执行完成。
线程池状态由线程池的shutdown()、shutdownNow()来触发线程池状态变更。

单个变量维护多个状态

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
线程池内部维护了一个ctl复合变量,它表示线程池运行状态(高3位)+工作线程数(其余29)
111 线程池状态 | 00000 00000000 00000000 00000000 工作线程数
工作线程数为 = 5_3687_0912 ,最多能创建536870912工作线程。
📖
用一个变量表示多个状态在 jdk源码属于常见操作。
  1. 并发问题
    1. 线程池是多线程共享的资源,“状态变更” 和 “线程数增减” 是高频并发操作。若用两个独立变量分别存储状态和线程数,可能出现以下问题:
      • 假设先修改状态,再修改线程数,中间可能被其他线程打断,导致两者状态不一致(如状态已变为 SHUTDOWN,但线程数还没更新)。反之,先修改线程数再改状态,也可能出现逻辑冲突(如线程数已超上限,但状态仍为 RUNNING
      一个变量存储两种状态,使用 CAS 操作可以保证原子性,要么两者都修改成功,要么都失败,防止出现并发出现数据不一致的情况。
  1. 节省内存空间,提高缓存利用率
      • 无须在定义一个变量来维护
      • 单个变量在内存中是连续存储的,CPU 能一次加载,这样提高缓存命中率,间接的提审了性能。
  1. 简化判断逻辑
    1. 通过位运算就可以获取工作线程数和线程池状态。

提交任务

execute 提交任务

  • 在创建线程池对象时,只是配置线程池的相关属性并没有创建线程。
  • 只有在提交任务时才会真正的创建线程。
(1) 获取线程池状态 (2) 核心线程数已满 且能加入队列,此时的线程池还是运行状态。
(3) 能执行到3处说明 线程池运行状态且工作线程大于等于核心线程数。
如果一开始定义核心线程数为0时,线程池也会在此处创建线程。
(4) 当线程池调用 shutdown()、shutdownNow()时,线程池的状态会被更改为 SHUTDOWN、STOP ,此时如果有新任务提交时,则会实现拒绝策略。

addWorker

尝试添加任务,添加成功启动任务。execute()→addWorker()
先申请工作线程数加1,然后将 Runnable 封装成 Worker 对象添加到 集合中。
addWorker(Runnable firstTask, boolean core) firstTask 当前提交的任务,core 是否使用核心线程数。
(1) 自旋更新 ctl 变量,工作线程数加 1
retry: 两层for(;;)循环,CAS增加工作线程计数成功后 通过break retry; 标记直接跳出两层循环。
(2) 尝试创建新线程
(3) new Worker(firstTask);将提交的 runnable封装成worker 对象,创建新线程。
(4) 提交的 runnable 对象,会被封装成 Worker对象并添加到一个 HashSet集合。
该HashSet 维护了所有活跃的动作线程的 worker。
(5) t是刚创建的Worker对象中的线,这里调用线程的 start方法触发worker的run()-> runWorker()→提交的 runnable 的 run()
(6) 到这里线程提交的任务是结束了。前面已经创建了线程并开始线程,工作线程开始它的任务。

线程复用

线程复用避免了重复创建线程的开销。
核心逻辑:让工作线程在完成一个任务后不销毁,而是继续从队列中获取任务执行。
notion image

worker对象

线程池内部的工作单元,本质上是一个实现了Runnable接口的内部类,将工作线程与“循环执行任务的逻辑”绑定。
  • Worker类继承 AQS并实现了 Runnable接口,Worker是实现线程复用的核心组件。
  • Worker 对象持有提交的任务和一个工作线程,这个线程的目标执行目标是 worker 自身的 run方法。
(1) 创建线程
创建线程的时候传入的 this对象,这里指的是当前的 worker对象。这样调用worker对象中的线程的start方法后会触发 worker 对象中 runnable的 run 方法,这样的对象组成方式是为线程复用服务的。
Worker对象的组成
Worker对象的组成
final Thread thread; // 可以交给外部线程调用 start()
Runnable firstTask; // 获取到初始的任务或者队列中的任务 这样可以调用run方法
伪代码描述线程复用过程
Thread t = worker.thread()
t.start() 启动worker中的线程
→ worker.run() 在这一步将首次提交任务运行结束后,之后获取队列中任务(runnable)执行其 run 方法就可以实现线程复用。
→ while(task≠null){
firstTask.run()
}

runWorker 循环

提交任务的线程执行 t.start(),触发 worker对象的 run 方法(worker 对象本身就是一个Runnable)
执行初始任务→循环从队列中取出任务→直到无法获取任务时进入 WAITTING 状态。
1️⃣
while循环条件的 getTask()方法 从任务队列(阻塞队列)中取出任务去执行,如果队列中一直有任务,则能保证用当前线程去执行。
提交任务会存放在阻塞队列中,如果队列中没有任务了,线程则进入 WAITTING 状态
2️⃣
线程执行任务时会上锁,这样可以根据锁判断线程是否空闲。
在interruptIdleWorkers方法中会执行 w.tryLock()来判断线程是否空闲
(3) 检查线程池状态
STOP为强制停止状态(有线程触发了 shutdownNow 方法),这里获取到任务后就不会执行。
shutdownNow 已经发出interrupt信号,这里会再次中断当前线程,确保在线程池在 STOP 状态下能够强制中断线程。
 

模拟线程复用

获取任务

在线程复用中的runWorker循环中,通过 getTask()从阻塞队列中取出任务。

线程池关闭

shutdown():
  • 平滑关闭,拒绝新任务,让已提交到队列中或在运行中的任务运行直到完成
  • 它只会中断空闲线程,促使一些等待的工作结束,不会主动中断正在执行的任务。
shutdownNow():
  • 立即关闭 拒绝新任务、尽量停止正在执行的任务并清空队列并返回尚未开始执行的任务列表(调用者可以对这些任务进一步做处理)。

shutdown()

1️⃣
更新线程池状态为 SHUTDOWN
线程池为 SHUTDOWN 状态时,线程池无法在接受新任务。在 execute 方法中会判断线程池状态。SHUTDOWN状态会执行完队列中的任务。
2️⃣
interruptIdleWorkers 中断所有空闲线程
找到处于等待/阻塞状态(例如在 getTask() 阻塞等待任务)的 worker,并中断它们,使它们能检查状态并可能退出;目前正在运行任务的线程通常不会被中断。

如何判断线程是空闲的

1️⃣
w.tryLock() 判断线程是否空闲关键代码,w 为Worker。如果能加上锁说明当前线程没有在执行任务。
工作线程执行任务时会加锁(runWorker()中
tryLock 只是执行一次 CAS,没有自旋操作。
 

shutdownNow()

 
1️⃣
interruptWorkers 中断所有线程
无论线程是否处于空闲状态,直接向 workers中的所有线程发出中断信号。
 

调用关闭方法真的可以关闭线程池吗

线程池的关闭还要取决于提交的任务中的代码配合。
提交任务即 Runnable 任务 会被封装成 Worker对象,由 Worker 中的 Thread 去执行Runnable。在 Java 中线程的中断是被动时的,如果 Runnable 中没有捕获中断异常,或者没有检测当前线程的中断异常,那么即使调用了 shutdown()、shutdownNow() ,线程中的任务无论是运行结束、还是正在运行中的任务都会忽略线程池关闭方法发出的中断信号。

获取任务

线程执行完首次提交的任务后,然后去阻塞队列中取出任务。
runWorker()→ getTask()
如果 getTask() 返回 null,runWorker()会退出 while 循环,进入清理方法,销毁worker(),那么线程也会被销毁。
1️⃣
线程池关闭,工作线程减一
如果只是 shutdown 状态且队列中还有任务时,会跳过该 if 条件,保证当前线程尝试从阻塞队列中获取任务。
如果是 STOP 状态 则只会执行工作线程数减一,并返回 null,这样让当前线程跳出 while 循环,进入清理(销毁 worker、推进终止),最后线程运行结束(销毁)。
2️⃣
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
是否能销毁核心线程 或 核心线程是否已满
timed的结果会影响 从队列获取任务的方式
  • true: workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
    • 当前线程获取任务时,最多等待 keepAliveTime,超时等待。
      如果超时,timedOut为 true。下一次循环的时候,会尝试推进线程结束。
      notion image
  • false: workQueue.take();
    • 当前线程无法从阻塞队列任务获取时进入 WAITTING 状态。
3️⃣
该if语句是线程结束的关键
(wc > maximumPoolSize || (timed && timedOut))
wc > maximumPoolSize 防御性编程,异常性超过最大线程数的线程可以在此处退出
(timed && timedOut) 要为 true,allowCoreThreadTimeOut 要设置为 true 或 核心线程已满
(wc > 1 || workQueue.isEmpty())
wc > 1 工作线程大于 1 或 阻塞队列为空
  • 避免阻塞队列中的任务,没有线程去消费。
    • 假设现在工作线程数为 1,队列中还有任务,这 1 个工作线程还能消费阻塞队列中的任务。
  • 如果阻塞队列没有任务了,最后一个线程也可以退出。

allowCoreThreadTimeOut

  • allowCoreThreadTimeOut : true为允许销毁核心线程,false为不允许销毁
  • allowCoreThreadTimeOut 默认false,所以创建线程池之后再调用allowCoreThreadTimeOut(false),这样的代码没有意义。
  • allowCoreThreadTimeOut(): 会向所有空闲的工作线程发出中断信号
 
 
notion image
上一篇
Sentinel
下一篇
CyclicBarrier jdk8 源码分析

Comments
Loading...