Java 并发编程笔记
写在前面的话
并发编程最早的实践都在操作系统里。高层语言的并发模型都要基于底层系统对硬件抽象和并发的设计来设计和实现,不能超出操作系统允许的范围。所谓的高级抽象总体上是简化对 OS 底层机制的复杂调用。
并发与异步
本文聚焦并发(Concurrency),即多任务在同一时间段内的交替或并行执行,核心问题是资源共享、线程同步与协作。
**异步(Asynchronous)**是另一维度:调用方发起操作后不等结果返回即继续执行,通过回调、Future或事件机制获取结果。异步可通过单线程事件循环实现,也可依托多线程并发实现。
二者关系:并发关注"多任务如何执行与协调",异步关注"调用是否阻塞等待"。并发编程常涉及异步,但本文不展开异步编程模式(如响应式流、协程),相关内容请参阅《Java 线程池笔记》。
管程
理论和实践之间是有鸿沟的,要弥合这种鸿沟,通常需要我们去学习别人的实践。比如并发的标准设计思想来自于操作系统里的管程(monitor),我们应当学习管程,进而了解标准的并发模型-管理共享变量和线程(并发任务)间通信的基本理论模型。
什么是管程
管程(Monitor)是一种并发编程的抽象数据结构,由 C.A.R. Hoare 和 Per Brinch Hansen 在 1970 年代提出。它比"锁"的概念更大——锁只是管程的一部分。
管程的完整定义:
1 | |
管程 ≠ 锁:
- 锁只提供互斥,解决"同一时刻只有一个线程进入"
- 管程还提供条件同步,解决"线程间协作等待"
管程与 Java 的映射
Java 中每个对象都可以作为管程:
| 管程概念 | Java 实现 | 说明 |
|---|---|---|
| 共享数据 | 对象的实例字段 | 被 synchronized 保护的数据 |
| 互斥机制 | synchronized / ObjectMonitor |
进入/退出临界区 |
| 条件变量 | Object.wait()/notify()/notifyAll() |
线程间协作 |
| 入口队列 | Entry Set | 等待获取锁的线程集合 |
| 等待队列 | Wait Set | 调用 wait() 后的线程集合 |
[辨析] MESA 管程模型 vs MESI 缓存协议
这是两个完全不同领域的概念,容易混淆:
-
MESA 模型(管程领域):Xerox PARC 提出的管程实现模型,定义了如何用互斥锁和条件变量实现线程同步。Java 的
synchronized+wait/notify就是 MESA 模型的典型实现。核心特点是:线程被唤醒后需要重新检查条件(因为notify不会传递信号,可能被"丢失")。 -
MESI 协议(硬件领域):CPU 缓存一致性协议,用于保证多核 CPU 缓存之间的数据一致性。它定义了缓存行的四种状态(Modified/Exclusive/Shared/Invalid)。
简单说:MESA 是软件层面的线程同步模型,MESI 是硬件层面的缓存一致性协议。它们解决不同层次的问题,但名字拼写相似容易混淆。
关键洞察:
- Java 的
synchronized是管程的互斥部分 - Java 的
wait()/notify()是管程的条件同步部分 - 两者结合才是完整的管程语义
管程的典型应用:生产者-消费者
管程的经典应用是解决生产者-消费者问题:
1 | |
三种管程模型
历史上出现过三种管程模型,它们的区别在于 signal 后的行为:
| 模型 | signal 后通知者行为 | signal 后等待者行为 | 代表语言 |
|---|---|---|---|
| Hoare | 立即挂起,让出 CPU | 立即获得锁并执行 | 理论模型,少用于实践 |
| Mesa | 继续执行,持有锁 | 进入入口队列,重新竞争锁 | Java、大多数现代语言 |
| Hansen | signal 后立即退出管程 | 立即获得锁并执行 | Concurrent Pascal |
名称来源:Mesa 不是缩写,而是来自施乐帕洛阿尔托研究中心(Xerox PARC)于 1970 年代开发的 Mesa 编程语言。这门语言最早实现了这种 signal 后通知者继续执行的管程风格,后被 Java、C#、Modula-3 等语言采纳,成为工业界的主流管程实现。
Java 采用 Mesa 模型,这也是为什么被唤醒的线程必须在 while 循环中重新检查条件。
Mesa 模型与操作系统层面的伪唤醒:Java 的管程实现无法消除操作系统层面的条件变量伪唤醒(spurious wakeup)现象——这是"不能超出操作系统允许的范围"的具体体现。因此 wait() 必须在 while 循环中,而非 if 语句中,以应对两种情况:(1)被 notify 唤醒后条件可能已被其他线程改变;(2)操作系统层面的伪唤醒。
为什么 Java 选择 Mesa 而非 Hoare 模型?
Hoare 模型理论上语义更清晰(唤醒即执行),但 Java 选择 Mesa 有以下原因:
- 实现简单:不需要在 signal 后立即切换线程上下文
- 与 OS 调度器兼容:被唤醒线程进入入口队列,由 OS 调度器统一管理
- 公平性更好:被唤醒线程重新竞争,避免新来的线程一直等待
- 避免优先级反转:高优先级线程 signal 后继续执行,不会被低优先级的等待者阻塞
代价:开发者必须在 while 循环中重新检查条件,因为被唤醒后条件可能已被其他线程改变。
Mesa 模型
Java 采用 Mesa 模型:
- 互斥(Mutual Exclusion):通过锁机制保证同一时刻只有一个线程能进入管程内部执行。
- 同步(Synchronization):利用条件变量(Condition Variable)实现线程间的等待与唤醒。
- Signal and Continue:
当线程发出通知(signal/notify)时,它继续持有锁并运行,而被唤醒的线程仅仅是进入就绪队列,并不立即抢占 CPU。
- 必须使用 while 循环:
由于线程被唤醒后不一定立即执行,当它重新获得锁时,环境条件可能已发生变化,因此必须在一个 while 循环中重新检查等待条件(while
(condition) { wait(); })。
为什么用 set 而不用 queue
- Queue 暗示 FIFO(先进先出):
- 如果我们叫它 EntryQueue,开发者会本能地认为:先来的线程一定先拿到锁。
- 但实际上,Java 的 synchronized 是非公平锁(Non-fair Lock)。
- 实际上更像“一堆人”而不是“一队人”:
- 在 JVM 的具体实现策略中,当锁被释放时,并不保证 EntrySet 中排在最前面的线程一定能抢到锁(可能被刚来的线程抢走,或者被随机唤醒)。
- 对于 WaitSet,notify() 唤醒的线程也不一定是先 wait() 的那个线程(取决于具体 JVM 实现)。
- 所以,用 Set(集合) 这个词能更准确地表达“这里有一群线程在等,但谁先出去不一定”的语义。
总结:叫 Set 是为了告诉你,不要依赖它们的唤醒顺序。
Entry Set 的命名含义
一个常见的问题是:为什么等待的队列明明没有进入同步块,却叫 Entry Set?
答案:Entry Set 的 “Entry” 不是指"进入同步块",而是指"进入管程的入口队列"。
从管程(Monitor)的角度看
1 | |
为什么叫 Entry Set
- Entry = 入口:这是线程想要进入管程(Monitor)的入口排队区
- 还没进入:线程确实还没有进入管程内部(没有拿到锁)
- 准备进入:但它们已经在管程的"门口"排队了
对比 Wait Set
| 队列 | 位置 | 状态 | 含义 |
|---|---|---|---|
| Entry Set | 管程外的入口 | 等待获取锁 | 想要进入管程的线程 |
| Wait Set | 管程内的休息区 | 等待条件满足 | 已经进入过管程,但暂时离开 |
一个形象的比喻
想象一个餐厅:
- 餐厅 = 管程(Monitor)
- 餐桌 = 临界区资源
- 座位 = 锁
1 | |
- Entry Set = 门口排队等位的顾客(还没拿到座位)
- Owner = 正在用餐的顾客(已经拿到座位)
- Wait Set = 去洗手间的顾客(暂时离开座位,回来要重新排队)
关键理解
-
Entry Set 的线程:
- 想要进入管程
- 还没有拿到锁
- 在管程的入口排队
- 状态:
BLOCKED
-
Wait Set 的线程:
- 已经进入过管程
- 主动释放锁(调用
wait()) - 等待条件满足
- 被唤醒后要重新进入 Entry Set 排队
- 状态:
WAITING
所以,Entry Set 的命名是准确的:它是线程想要进入管程的入口队列,而不是"已经进入同步块的队列"。
模型映射
| Mesa 模型 | Mesa 语义 | synchronized |
ReentrantLock |
Java State | 超时 | JVisualVM | 底层机制 |
|---|---|---|---|---|---|---|---|
| Entry Set (锁竞争) |
等待获取锁 | Monitor Entry List | AQS Sync Queue | BLOCKEDWAITING (parking) |
否 | Monitor Park |
ObjectMonitor LockSupport.park() |
| Wait Set (条件等待) |
等待条件满足 | Monitor Wait Set | AQS Condition Queue | WAITINGTIMED_WAITING |
是 | Wait Park |
ObjectMonitor LockSupport.park() |
| Owner (持有者) |
持有锁的线程 | Monitor Owner | exclusiveOwnerThread | RUNNABLE |
- | Running | - |
管程视角 vs Java 线程状态
一个常见的问题是:按照 Mesa 模型,线程是否只有三种状态——Owner、Entry Set、Wait Set?
答案是:管程视角和 Java 线程状态是两个不同的抽象层次。
从管程视角看,相对于某个特定管程,线程确实只有四种位置:
1 | |
但从 Java 线程状态看,有六种状态,且两者不是一一对应:
| 管程位置 | 对应的 Java 线程状态 | 说明 |
|---|---|---|
| Outside | NEW / RUNNABLE / TERMINATED |
Outside 不等于"空闲",线程可能在执行其他工作 |
| Entry Set | BLOCKED(synchronized)或 WAITING(ReentrantLock) |
同一管程位置,Java 状态因实现不同而异 |
| Owner | RUNNABLE |
持有锁,正在执行临界区代码 |
| Wait Set | WAITING 或 TIMED_WAITING |
取决于是否使用超时版本的 wait |
关键洞察:
- 管程视角描述的是"线程相对于管程的位置"
- Java 线程状态描述的是"线程在其生命周期中的状态"
- 一个在 Entry Set 中的线程(管程视角),在 Java 中可能是
BLOCKED(synchronized)也可能是WAITING(ReentrantLock)
1. Mesa 模型的 “Signal and Continue” 语义
notify()/signal()后,通知者继续持有锁- 被唤醒的线程从 Wait Set 移入 Entry Set,必须重新竞争锁
- 唤醒路径:
synchronized: Wait Set → Entry Set (BLOCKED) → OwnerReentrantLock: Condition Queue → Sync Queue (WAITING) → Owner
2. Entry Set(锁竞争)❌ 永远不会有 TIMED_WAITING
synchronized不支持超时ReentrantLock.lock()不支持超时tryLock(timeout)不进队列,在当前线程自旋
3. Wait Set(条件等待)✅ 支持 TIMED_WAITING
wait(timeout)/await(timeout, unit)支持超时- 设计哲学:条件等待是主动等待业务条件,需要"等不到就放弃"的语义
4. JVisualVM 分类逻辑
- Monitor:
synchronized的BLOCKED状态 - Park:
LockSupport.park()导致的WAITING状态 - Wait:
Object.wait()导致的WAITING状态
5. 线程状态与 OS 调度
- RUNNABLE = OS Ready + OS Running(JVM 无法区分)
- BLOCKED / WAITING: 线程在 JVM 队列中,未持有 CPU
Ready Queue 是 OS 层面的,JVM 不可见
Thread 模型底层实现
在深入 Java 线程状态之前,有必要理解 Thread 模型的底层实现。Java Thread 之所以只支持 run() 方法而非 call(),是因为底层操作系统线程 API(如 pthread)的设计约束。
Java Thread 类的设计
1 | |
HotSpot JVM 中的线程创建
底层的 cpp 源码是(以下代码为简化示意,基于 HotSpot JDK 11,不同版本实现可能有差异):
1 | |
线程创建的调用链
整体调用的流程是从 Java 到 C++ 再到 Java 的:
1 | |
JavaThread 三位一体设计
classDiagram
class JavaThread {
-OSThread* _osthread
-oop _threadObj
-JavaThreadState _state
-address _stack_base
-size_t _stack_size
+run()
+thread_main_inner()
+osthread() OSThread*
+threadObj() oop
}
class OSThread {
-pthread_t _thread_id
-int _thread_state
+set_thread_id(pthread_t)
+thread_id() pthread_t
}
class JavaThreadObj {
<<Java Object>>
-Runnable target
-int threadStatus
+start()
+run()
}
JavaThread o-- OSThread : "持有操作系统线程"
JavaThread o-- JavaThreadObj : "关联Java对象"
线程启动时序
sequenceDiagram
participant User as Java代码
participant JVM as JVM(JavaThread)
participant OS as 操作系统
User->>JVM: new Thread(runnable)
JVM->>JVM: 创建JavaThread对象
JVM->>JVM: 创建OSThread对象
JVM->>JVM: 关联JavaThread和OSThread
User->>JVM: thread.start()
JVM->>JVM: 检查状态(threadStatus)
JVM->>JVM: 添加到线程组
JVM->>JVM: 调用start0()(native)
JVM->>OS: os::create_thread()
OS->>OS: 创建pthread线程
OS->>OS: 设置入口为thread_native_entry
OS-->>JVM: 线程创建成功
JVM-->>User: start()返回
Note over OS: 新线程开始执行
OS->>JVM: thread_native_entry(JavaThread*)
JVM->>JVM: 设置线程状态为RUNNABLE
JVM->>JVM: thread->run()
JVM->>JVM: thread_main_inner()
JVM->>JVM: JNI: 查找Thread.run()方法
JVM->>JVM: JavaCalls::call_virtual()
JVM->>User: 调用Thread.run()
User->>User: target.run()(如果target!=null)
User-->>JVM: 返回
JVM->>JVM: 线程结束清理
JVM->>OS: 释放操作系统资源
关键洞察:Java Thread 的 run() 方法无返回值的设计,直接源于 POSIX pthread 的线程入口函数签名 void* (*)(void*)——虽然有返回值,但 JVM 选择不使用它。这是"高级抽象要基于底层系统"的又一例证。后来 Java 5 引入的 Callable<V> 和 Future<V> 通过包装模式(FutureTask 包含 Callable)绕过了这一限制,实现了带返回值的异步任务。
Java 线程状态
synchronized vs ReentrantLock 的等待-通知队列模型对比
graph LR
subgraph "synchronized(内置 Monitor)"
direction TB
ES1["Entry Set<br>(锁池)<br>BLOCKED"] -->|"竞争成功"| OW1["Owner<br>(持有锁)<br>RUNNABLE"]
OW1 -->|"wait()"| WS1["Wait Set<br>(等待池)<br>WAITING"]
WS1 -->|"notify() /<br>notifyAll()"| ES1
OW1 -->|"退出 synchronized"| ES1
end
subgraph "ReentrantLock(AQS)"
direction TB
SQ2["Sync Queue<br>(同步队列/CLH)<br>WAITING(park)"] -->|"前驱释放锁"| OW2["exclusiveOwnerThread<br>(持有锁)<br>RUNNABLE"]
OW2 -->|"await()"| CQ2["Condition Queue<br>(条件队列)<br>WAITING(park)"]
CQ2 -->|"signal() /<br>signalAll()"| SQ2
OW2 -->|"unlock()"| SQ2
end
关键差异:
synchronized竞争锁失败时线程状态为 BLOCKED,而ReentrantLock竞争锁失败时线程状态为 WAITING(通过LockSupport.park()实现)。这是 jstack 中看到parking和monitor的根本原因。
管程语义层面的差异:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 条件变量数量 | 单一 Wait Set | 多个 Condition(newCondition() 可创建任意数量) |
| 条件唤醒精度 | notifyAll() 唤醒所有等待者 |
condition.signal() 精确唤醒特定条件队列 |
| 管程粒度 | 对象级别(锁绑定到对象) | 代码块级别(锁独立于对象) |
| 可中断性 | 等待锁时不可中断(BLOCKED) |
lockInterruptibly() 可响应中断 |
关键洞察:当需要多个条件变量(如生产者-消费者中的"不满"和"不空")时,ReentrantLock 的多 Condition 设计比 synchronized 的单一 Wait Set 更精确,避免不必要的唤醒。

1 | |
1 | |
线程状态转换全景图
上图展示了六种 Java 线程状态及其转换关系,下文详细解释每种状态的含义和触发条件。
stateDiagram-v2
[*] --> NEW : new Thread()
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : 等待 synchronized 锁
BLOCKED --> RUNNABLE : 获取到 Monitor 锁
RUNNABLE --> WAITING : wait() / join() / park()
WAITING --> BLOCKED : notify() / notifyAll() / join唤醒<br>(需重新竞争 Monitor 锁)
WAITING --> RUNNABLE : unpark()<br>(LockSupport场景,不涉及Monitor锁)
RUNNABLE --> TIMED_WAITING : sleep(ms) / wait(ms)<br>join(ms) / parkNanos()
TIMED_WAITING --> BLOCKED : 超时或被唤醒<br>(需重新竞争 synchronized 锁)
TIMED_WAITING --> RUNNABLE : 超时或被唤醒<br>(无需竞争锁)
RUNNABLE --> TERMINATED : run() 结束 / 异常退出
note right of RUNNABLE
包含 OS 层面的两个子状态:
Ready(就绪)和 Running(运行中)
JVM 无法区分这两者
end note
note right of BLOCKED
仅由 synchronized 产生
ReentrantLock 竞争锁时
线程状态是 WAITING(park)
end note
线程状态详解
NEW
没有启动过的线程。
RUNNABLE
- 正在执行的线程。
- 可以被执行但没有拿到处理器资源。
BLOCKED
blocked 其实是 blocked waiting。
- 等待 monitor,进入 synchronized method/block
- 或者等 wait()/await()以后再次进入 synchronized method/block。解除 wait 以后以后不是直接 runnable,而是进入 blocked,如果 notify 后通知线程立刻离开同步块,则几乎不可能用程序观察到从 blocked 进入 runnable。如果通知者在 notify() 之后赖着不走(比如执行了一个耗时操作),或者同时有 100 个线程在竞争这把锁:
- 那个被唤醒的线程会长时间停留在 BLOCKED 状态,直到它抢到锁为止。可以通过 jstack 或者 Thread.getState() 清晰地观察到它处于 BLOCKED 状态。
WAITING
在调用这三种不计时方法以后,线程进入 waiting 态:
- Object.wait
- Thread.join
- LockSupport.park 我们经常在文档里看到的 thread lies dormant 就是被这个方法处理过的结果
waiting 意味着一个线程在等待另一个线程做出某种 action。wait 在等其他对象 notify 和 notifyAll,join 在等其他线程终结。
如:
java.util.concurrent.LinkedBlockingQueue.take -> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await -> java.util.concurrent.locks.LockSupport.park
Reentrantlock 的 lock 接口的栈帧则是:
1 | |
jstack 总会告诉我们 waiting 的位置,比如等待某个 Condition 的 await 操作。
1 | |
对这个程序进行 thread dump,可以看出 ReentrantLock 就是依赖于 park 导致的 waiting:


如果使用 synchronized,则会显示 object monitor:

所以 waiting 可能是在条件变量上等待,也可能是在 synchronizer 本身上等待,不可一概而论。
按照 jvisualvm 的分类方法,线程还可以分为:
- 等待
- 驻留(park)
- 监视(monitor)
TIMED_WAITING
调用了计时方法,等待时间结束后才或者被其他唤醒方法唤醒结束等待。
- Thread.sleep
- Object.wait
- Thread.join
- LockSupport.parkNanos
- LockSupport.parkUntil
如:
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take -> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos -> java.util.concurrent.locks.LockSupport.parkNanos -> sun.misc.Unsafe.park
除了 sleep 以外,jstack 总会告诉我们 time_waiting 的位置,比如等待某个 Condition 的 await 操作。
TERMINATED
终结的线程,执行已经结束了。
中断退出也是一种结束。
几种线程状态的对比
- blocked:线程想要获取锁进入临界区之前,会求锁,求不到锁会进入 entry_set,然后放弃 cpu。高并发时 blocked 会增多。
- 工作线程池开始伸缩,扩容的时候:jvm.thread.waiting.count 的数量会变少。过程是,core 线程先满,然后队列再满,这时候等待从队列里获取任务,waiting 在 take 动作上的线程已经降为0了,然后开始产生非core线程,线程数才开始增长。
- 工作吞吐变多,而调用下游的工作线程在阻塞的时候,jvm.thread.time_waiting.count 会变多,因为 rpc 框架自带超时,而这些超时是会让工作线程进行计时等待的。
- 流量变大的时候,2 和 3 可能同时发生。
线程间方法的设计哲学
graph TB
subgraph "静态方法(操作当前线程 - 安全)"
sleep["Thread.sleep()"]
yield["Thread.yield()"]
interrupted["Thread.interrupted()"]
currentThread["Thread.currentThread()"]
end
subgraph "实例方法(操作目标线程 - 需谨慎)"
subgraph "安全操作"
start["start() - 开启生命周期"]
join["join() - 观测目标状态"]
interrupt["interrupt() - 设置标志位"]
getState["getState() / isAlive()"]
end
subgraph "危险操作(已废弃)"
stop["stop() - 强制终止"]
suspend["suspend() - 强制挂起"]
resume["resume() - 强制恢复"]
end
end
style stop fill:#ff6b6b,color:#fff
style suspend fill:#ff6b6b,color:#fff
style resume fill:#ff6b6b,color:#fff
style start fill:#51cf66,color:#fff
style join fill:#51cf66,color:#fff
style interrupt fill:#ffd43b,color:#333
- 通常:
- 静态方法 = “我要操作当前线程”(self-operation)。static 相当于 per thread,一个好记的例子是通常 ThreadLocal 设置为 static 的,这样每个线程可以分到一个它的实例,而不是每个线程在每个对象里分到它的实例。
- 实例方法 = “我要操作指定线程”(cross-thread operation)。
- 这背后的逻辑是:
- 每个线程操作自己是比较安全的,static 可以默认在不指定对象的情况下操作自己。
- 而跨线程操作其他线程是比较危险的,因为其他线程的生死如果不是自然发展和结束的,很可能导致锁不释放,条件变量不正确设置,通知没有正确发出。这也就意味着系统可能死锁。
- 主动控制线程何时开始是安全的。
- 主动控制进程何时结束是危险的,因为你不能替他释放资源-这是禁止使用 stop、spend api 这类操作的全部理由。
- 可以跨线程操作的是比较温和的操作:
- start():可以让程序员开启线程周期。
- interrupt():可以设置一个标志位,算是轻微的主动写入别的线程状态的一种低侵入的 api。
- join(): 观测另一个对象的状态,通过内部自旋 wait 来等待另一个线程死亡。
- 其他 static 方法:
- yield():主动让出 CPU,让同优先级线程有机会运行。是对调度器的"建议",不保证效果。和 interrupt 的温和写入,但不必然强制操作形成对比。
线程中断机制深入解析
上一节我们提到,interrupt() 是一种"温和的跨线程写入"操作——它只是设置一个标志位,不会强制终止线程。这个看似简单的设计,背后隐藏着从操作系统信号机制到 JUC 框架的完整传递链路,也引发了 Java 社区关于"中断线程还是取消任务"的深层争论。本节将从 HotSpot 底层实现出发,逐层向上剖析 interrupt 的完整机制。
从 HotSpot 底层看 interrupt 的本质
JVM 层面的数据结构
在 HotSpot JVM 中,每个 Java 线程对应一个 JavaThread C++ 对象,其中维护着一个 volatile 的中断标志位:
1 | |
关键洞察:_interrupted 是一个线程级别的全局布尔值,不是任务级别的。同一个线程上顺序执行的所有任务,共享这一个标志位。这是后续所有设计问题的根源。
interrupt() 与 unpark() 的辨析
一个常见的误解是认为 Thread.interrupt() 等价于"设置中断标志位 + unpark()"。虽然这个理解方向正确,但需要补充几个关键细节:
interrupt() 的完整动作分解:
1 | |
关键差异点:
unpark()不是唯一唤醒方式
| 阻塞方式 | 唤醒机制 | 是否抛异常 | 标志位处理 |
|---|---|---|---|
LockSupport.park() |
Unsafe.unpark() |
❌ 不抛异常 | ✅ 保留标志 |
Object.wait() |
ObjectMonitor 通知 | ✅ 抛 InterruptedException |
❌ 清除标志 |
Thread.sleep() |
OS 信号唤醒 | ✅ 抛 InterruptedException |
❌ 清除标志 |
synchronized 等待 |
无法唤醒 | - | ✅ 保留标志 |
unpark()的特殊语义
LockSupport.unpark() 有一个重要特性:
1 | |
这就是为什么 AQS 在 acquireQueued() 中必须用 Thread.interrupted()(清除版本):
1 | |
synchronized的特殊处理
这是最关键的区别:
1 | |
原因:JVM 的 Monitor 实现基于操作系统的 mutex(如 Linux 的 pthread_mutex),而 OS mutex 不支持可中断的锁获取。这是"高级抽象要基于底层系统"的边界——Java 无法实现操作系统层面不支持的语义。
而 ReentrantLock.lockInterruptibly() 可以响应中断:
1 | |
总结:
Thread.interrupt()= 设置中断标志位 + 根据阻塞方式选择唤醒机制(unpark/OS信号/无法唤醒)
这也是为什么"interrupt 的行为不一致会增加心智负担"——不同的阻塞方式响应方式完全不同!
graph TB
INT["Thread.interrupt() 被调用"]
INT --> SET["设置 _interrupted = true"]
SET --> CHECK{"目标线程当前状态?"}
CHECK -->|"RUNNABLE<br/>(正在运行)"| DONE["仅设置标志位<br/>不做其他操作"]
CHECK -->|"WAITING<br/>(LockSupport.park)"| UNPARK["调用 Unsafe.unpark()<br/>park() 立即返回<br/>不抛异常,保留标志"]
CHECK -->|"WAITING/TIMED_WAITING<br/>(wait/sleep/join)"| NOTIFY["通过 OS 信号唤醒<br/>抛出 InterruptedException<br/>自动清除标志"]
CHECK -->|"BLOCKED<br/>(等待 synchronized)"| IGNORE["无法唤醒<br/>仅设置标志位<br/>必须等到获取锁"]
CHECK -->|"NIO Channel<br/>(Selector.select)"| CLOSE["关闭底层 fd<br/>抛出 ClosedByInterruptException<br/>Channel 被破坏"]
style IGNORE fill:#868e96,color:#fff
style CLOSE fill:#ff6b6b,color:#fff
style UNPARK fill:#51cf66,color:#fff
style NOTIFY fill:#ffd43b,color:#333
操作系统层面的唤醒机制
当线程处于不同的阻塞状态时,interrupt() 的唤醒路径不同:
1 | |
关于 fd 和 NIO 的特殊处理:当线程阻塞在 NIO 的 Selector.select() 或 Channel.read() 上时,底层实际上是在等待操作系统的文件描述符(fd)就绪。interrupt() 会关闭这个 fd,导致阻塞的 I/O 操作立即返回并抛出 ClosedByInterruptException。这是一种比较激进的唤醒方式——它不仅唤醒了线程,还破坏了 Channel,使其不可再用。
CPU 出让与唤醒的本质
线程阻塞的本质是让出 CPU。不同的阻塞方式,让出 CPU 的机制不同:
graph TB
subgraph "阻塞方式与 CPU 出让"
PARK["LockSupport.park()"]
WAIT["Object.wait()"]
SLEEP["Thread.sleep()"]
IO["NIO Channel I/O"]
PARK -->|"Unsafe.park()"| OS_FUTEX["OS: futex / pthread_cond_wait<br/>线程从运行队列移除"]
WAIT -->|"ObjectMonitor::wait()"| OS_COND["OS: pthread_cond_wait<br/>释放 Monitor 锁 + 挂起"]
SLEEP -->|"os::sleep()"| OS_SLEEP["OS: nanosleep / Sleep<br/>定时器到期后唤醒"]
IO -->|"epoll_wait / select"| OS_IO["OS: I/O 多路复用<br/>等待 fd 就绪"]
end
subgraph "interrupt() 的唤醒方式"
UNPARK["Unsafe.unpark()<br/>→ 设置 permit = 1"]
NOTIFY["notify ObjectMonitor<br/>→ 从 WaitSet 移到 EntrySet"]
SIGNAL["发送信号<br/>→ 打断 nanosleep"]
CLOSE_FD["关闭 fd<br/>→ epoll 返回错误"]
end
OS_FUTEX -.->|"interrupt()"| UNPARK
OS_COND -.->|"interrupt()"| NOTIFY
OS_SLEEP -.->|"interrupt()"| SIGNAL
OS_IO -.->|"interrupt()"| CLOSE_FD
style CLOSE_FD fill:#ff6b6b,color:#fff
style UNPARK fill:#51cf66,color:#fff
三个 interrupt 相关方法的精确语义
Java 提供了三个与中断相关的方法,它们的行为差异是大量 bug 的根源:
| 方法 | 类型 | 行为 | 是否清除标志 | 典型用途 |
|---|---|---|---|---|
interrupt() |
实例方法 | 设置目标线程的中断标志为 true;若目标线程正在阻塞,则唤醒它 |
— | 跨线程发送中断信号 |
isInterrupted() |
实例方法 | 返回目标线程的中断状态 | 否 | 非破坏性地查询中断状态 |
Thread.interrupted() |
静态方法 | 返回当前线程的中断状态 | 是 | 检查并消费中断信号 |
1 | |
Thread.interrupted() 的清除行为是设计上的双刃剑:它实现了"检查并消费"的语义——一次中断信号只能被消费一次。这在单层代码中是合理的,但在嵌套调用中会导致"中断标志泄露"问题(后文详述)。
阻塞方法对 interrupt 的响应差异
不同的阻塞方法对 interrupt() 的响应方式截然不同,这种不一致性增加了开发者的心智负担:
flowchart TB
INT["Thread.interrupt() 被调用"]
INT --> CHECK{"目标线程<br/>当前状态?"}
CHECK -->|"RUNNABLE<br/>(正在运行)"| RUN["仅设置标志位<br/>不抛异常<br/>不清除标志"]
CHECK -->|"WAITING/TIMED_WAITING<br/>(wait/sleep/join)"| BLOCK["抛出 InterruptedException<br/>清除中断标志"]
CHECK -->|"WAITING<br/>(LockSupport.park)"| PARK_R["park() 立即返回<br/>不抛异常<br/>不清除标志"]
CHECK -->|"BLOCKED<br/>(等待 synchronized)"| SYNC["无法响应中断<br/>仅设置标志位<br/>必须等到获取锁后才能检查"]
RUN --> MANUAL["开发者需手动检查<br/>isInterrupted()"]
BLOCK --> CATCH["catch 块中标志已清除<br/>开发者需手动恢复"]
PARK_R --> AQS_CHECK["AQS 通过<br/>Thread.interrupted()<br/>检查并清除"]
SYNC --> LATER["获取锁后才能<br/>检查中断状态"]
style BLOCK fill:#ff6b6b,color:#fff
style PARK_R fill:#ffd43b,color:#333
style SYNC fill:#868e96,color:#fff
style RUN fill:#51cf66,color:#fff
用代码展示这四种情况:
1 | |
[PATTERN] 中断响应的三种模式:
| 模式 | 代表方法 | 行为 | 标志位处理 |
|---|---|---|---|
| 抛异常 + 清除 | wait(), sleep(), join() |
立即抛出 InterruptedException |
自动清除,需手动恢复 |
| 静默返回 + 保留 | LockSupport.park() |
方法返回,不抛异常 | 保留标志,由调用者处理 |
| 完全忽略 | synchronized 等待 |
无法响应,继续等待 | 保留标志,获取锁后才能检查 |
中断恢复:为什么必须 Thread.currentThread().interrupt()
当 InterruptedException 被捕获后,中断标志已被清除。如果当前代码只是"路过"(比如中间层、包装层),不是中断的最终处理者,就必须恢复中断标志,否则上层调用者将永远无法感知到中断的发生:
sequenceDiagram
participant Caller as 上层调用者
participant Middle as 中间层方法
participant Sleep as Thread.sleep()
participant External as 外部线程
External->>Middle: interrupt()
Note over Middle: 中断标志 = true
Middle->>Sleep: sleep(10000)
Note over Sleep: 检测到中断标志
Sleep-->>Middle: 抛出 InterruptedException
Note over Middle: 中断标志被清除为 false!
alt 正确做法:恢复中断
Middle->>Middle: Thread.currentThread().interrupt()
Note over Middle: 中断标志恢复为 true
Middle-->>Caller: 返回(或抛出业务异常)
Note over Caller: 可以检测到中断
else 错误做法:吞掉异常
Middle->>Middle: log.error("interrupted")
Note over Middle: 中断标志仍为 false
Middle-->>Caller: 返回
Note over Caller: 无法检测到中断!
end
标准的中断处理模式:
1 | |
interrupt 在 AQS 中的传递机制
AQS(AbstractQueuedSynchronizer)是 java.util.concurrent 的基石,它对 interrupt 的处理方式与 Object.wait() 截然不同,体现了两种完全不同的中断哲学。
AQS 的"延迟中断"策略
AQS 的 acquire() 方法(非中断版本)采用延迟中断策略:先获取锁,再处理中断。
1 | |
sequenceDiagram
participant T as 线程
participant AQS as AQS
participant Park as LockSupport
participant Ext as 外部线程
T->>AQS: acquire(1)
AQS->>AQS: tryAcquire(1) → false
AQS->>AQS: addWaiter() 入队
loop acquireQueued 自旋
AQS->>AQS: tryAcquire(1) → false
AQS->>Park: park(this)
Note over T: 线程阻塞
Ext->>T: interrupt()
Note over T: 中断标志 = true
Park-->>AQS: park() 返回
AQS->>AQS: Thread.interrupted()
Note over AQS: 返回 true,清除标志<br/>interrupted = true<br/>但不退出循环!
AQS->>AQS: tryAcquire(1) → false
AQS->>Park: park(this)
Note over T: 标志已清除,可以再次阻塞
Note over T: ... 最终获取到锁 ...
AQS->>AQS: tryAcquire(1) → true
end
AQS-->>T: return interrupted=true
T->>T: selfInterrupt()
Note over T: 补上中断标志
为什么 AQS 要用 Thread.interrupted()(清除版本)而不是 isInterrupted()?
因为 LockSupport.park() 有一个关键特性:如果中断标志为 true,park() 会立即返回而不阻塞。如果不清除标志,线程将陷入"park → 立即返回 → 再 park → 立即返回"的忙等待循环,浪费 CPU。
AQS 的"立即中断"策略
与 acquire() 不同,acquireInterruptibly() 采用立即中断策略:
1 | |
两种策略的对比:
| 方法 | 策略 | 中断时行为 | 典型使用者 |
|---|---|---|---|
lock() → acquire() |
延迟中断 | 记录中断,继续获取锁,成功后补 selfInterrupt() |
ReentrantLock.lock() |
lockInterruptibly() → acquireInterruptibly() |
立即中断 | 立即抛出 InterruptedException,取消排队 |
ReentrantLock.lockInterruptibly() |
ConditionObject.await() 中的中断检查
Condition.await() 的中断处理更加精细,它区分了"在 signal 之前被中断"和"在 signal 之后被中断"两种情况:
1 | |
interrupt 与线程状态转换的关系
interrupt 可以触发多种线程状态转换,但其影响取决于线程当前所处的状态。完整的线程状态转换图见前文线程状态转换全景图,此处重点分析 interrupt 对各状态的影响:
interrupt 对不同状态线程的影响:
1 | |
关键区别:synchronized vs ReentrantLock 对中断的响应
1 | |
这也是为什么在需要可中断的锁获取场景中,ReentrantLock 优于 synchronized 的原因之一。
Thread.interrupt 的五大设计缺陷
理解了 interrupt 的底层机制后,我们可以系统地分析它的设计缺陷。这些缺陷在简单场景中不明显,但在复杂的生产环境中会导致严重问题。
五大缺陷概览:
1 | |
缺陷一:中断的是线程,而非任务
1 | |
在线程池场景中,一个 Worker 线程会顺序执行多个任务。Future.cancel(true) 的实现是调用 Thread.interrupt(),但这个中断信号是发给线程的,不是发给任务的:
1 | |
Future.cancel(true) 的 Javadoc 明确警告:
“There are no guarantees beyond best-effort attempts to stop processing actively executing tasks.”
缺陷二:中断标志容易被"吞掉"
第三方库或子任务可能捕获 InterruptedException 后不恢复中断标志,导致上层调用者永远无法感知中断:
1 | |
缺陷三:中断标志泄露
这是最隐蔽的问题。当子任务操作了中断标志(特别是使用 Thread.interrupted() 清除了标志),父任务将无法观察到中断信号:
1 | |
泄露的执行流程:
1 | |
泄露的本质:子任务"消费"了本该属于父任务的中断信号。中断信号就像一封信,被中间人拆开读了还扔掉了,真正的收件人永远收不到。
缺陷四:嵌套任务时的混乱
在实际应用中,任务往往是层级嵌套的:
1 | |
当用户点击"取消"时,中断信号应该如何传播?使用 Thread.interrupt() 面临多重困境:
1 | |
缺陷五:行为不一致增加心智负担
前文已经详细分析了不同阻塞方法对 interrupt 的响应差异。总结来说:
wait()/sleep()/join():抛异常 + 清除标志LockSupport.park():静默返回 + 保留标志synchronized等待:完全忽略- NIO Channel:抛异常 + 关闭 Channel
开发者需要记住每种方法的行为,并在每个 catch 块中做出正确的处理决策。这种不一致性是 bug 的温床。
替代方案:任务级取消机制
interrupt 的核心问题在于:取消的粒度是线程,而非任务。现代并发编程的趋势是将取消机制从线程级提升到任务级。
AbortController 模式
借鉴 Web 标准的 AbortController,可以为每个任务创建独立的取消控制器:
1 | |
与 Thread.interrupt 的对比:
| 维度 | Thread.interrupt |
AbortController |
|---|---|---|
| 取消对象 | 线程(Thread) | 任务(Task/Operation) |
| 状态存储 | 线程对象的内部字段 | 独立的控制器对象 |
| 作用范围 | 整个线程生命周期 | 可控的代码块范围 |
| 层级关系 | 扁平(一个线程一个标志) | 树形(父子控制器可关联) |
| 清除行为 | interrupted() 会自动清除 |
持久状态,不会被清除 |
| 隔离性 | 无(共享线程标志) | 有(每个任务独立控制器) |
1 | |
结构化并发(Java 19+)
Java 从 JDK 19 开始引入结构化并发(StructuredTaskScope),将取消与代码作用域绑定,从语言层面解决了 interrupt 的设计缺陷:
1 | |
Kotlin 协程的取消
Kotlin 协程提供了更优雅的取消机制,每个协程有自己的 isActive 状态:
1 | |
interrupt 最佳实践
尽管 interrupt 有设计缺陷,但它仍然是当前 Java 生态的底层基础设施。以下是在不同场景下的最佳实践:
规则一:永远不要吞掉 InterruptedException
1 | |
规则二:优先使用 isInterrupted() 而非 interrupted()
1 | |
规则三:区分"取消"和"中断"
1 | |
决策树
1 | |
线程池中的强制中断与拒绝机制
理解了 Thread.interrupt 的底层机制和最佳实践后,我们来看看在线程池这一典型应用场景中,interrupt 与拒绝策略是如何协同工作的。
强制触发的完整场景分类
在线程池中,存在多种会强制触发线程中断或拒绝新任务的场景。这些场景可以分为"主动触发"和"系统被动触发"两类:
主动触发(用户代码显式调用):
| 触发方式 | 作用范围 | 中断对象 | 典型应用 |
|---|---|---|---|
| shutdownNow() | 整个线程池 | 所有工作线程 | 快速停止线程池 |
| Future.cancel(true) | 单个任务 | 指定任务的执行线程 | 取消异步任务 |
系统被动触发(内部机制自动触发):
| 触发场景 | 触发条件 | 响应方式 | 影响范围 |
|---|---|---|---|
| 线程池饱和 | 队列满 + 线程数达 maximumPoolSize | 触发 RejectedExecutionHandler | 当前被拒绝的任务 |
| awaitTermination 超时 | shutdown 后等待超时 | 强制关闭 | 等待线程 |
| ScheduledFutureTask 异常 | 周期任务未捕获异常 | 终止任务调度 | 当前周期任务 |
| tryTerminate 自旋中断 | 线程池终止过程中 | 中断空闲 Worker | 空闲工作线程 |
shutdownNow() 的中断流程
当调用 shutdownNow() 时,线程池会遍历所有 Worker 线程并调用 interrupt()。关键点:
- shutdownNow() 对所有线程调用 interrupt(),但中断是否生效取决于任务代码如何响应
- 如果任务在 wait()/sleep() 中,会抛出 InterruptedException
- 如果任务在 synchronized 等待锁,必须等到获取锁后才能检查中断
- 如果任务在 LockSupport.park(),会立即返回
Future.cancel(true) 的中断流程
Future.cancel(true) 的实现是调用执行该任务的线程的 interrupt()。重要警告:在线程池场景中,cancel(true) 存在"误伤"风险——如果任务已完成,线程可能正在执行下一个任务,interrupt() 信号会发给正在执行其他任务的线程。这就是"中断粒度错误"缺陷在线程池中的具体体现。
拒绝策略的触发时机与选择
根据 ThreadPoolExecutor.execute() 的源码,当队列满且线程数达最大值时,会触发拒绝策略。
四种内置拒绝策略对比:
| 策略 | 行为 | 适用场景 | 风险 |
|---|---|---|---|
| AbortPolicy | 抛出 RejectedExecutionException | 快速失败 | 任务丢失 |
| CallerRunsPolicy | 由调用者线程执行 | 削峰填谷 | 调用者线程阻塞 |
| DiscardPolicy | 静默丢弃任务 | 无感知丢弃 | 无反馈 |
| DiscardOldestPolicy | 丢弃队列中最老任务 | 优先级场景 | 重要任务可能丢失 |
周期任务异常导致的静默停止
这是最常见的生产环境陷阱之一。如果周期任务抛出未捕获异常,会导致后续调度终止。
必须遵循的铁律:周期任务必须捕获所有异常:
1 | |
与 interrupt 机制的关联
线程池中的强制中断与拒绝机制,本质上是对 Thread.interrupt 的上层封装:
- 拒绝策略处理的是"提交阶段"的过载
- 中断机制处理的是"执行阶段"的取消
- 两者共同构成线程池的流量控制和生命周期管理
最佳实践总结
- 不要依赖强制中断来停止任务,任务应该支持检查中断标志并优雅退出
- 优先使用 cancel(false),让正在执行的任务自然完成
- 合理选择拒绝策略,根据业务对任务丢失的容忍度选择
- 始终为周期任务捕获异常,避免任务静默停止
这些实践与前面 interrupt 最佳实践章节的原则一致,体现了从底层机制到上层应用的一致性设计思想。
interrupt 的历史演进
timeline
title Java 线程取消机制的演进
section Java 1.0 (1995)
Thread.stop() : 强制终止(不安全)
Thread.suspend/resume : 强制挂起/恢复(易死锁)
section Java 1.2 (1998)
Thread.interrupt() : 协作式中断
stop/suspend 被废弃 : 标记为 @Deprecated
section Java 5 (2004)
Future.cancel(true) : 封装 interrupt
ExecutorService : 线程池标准化
section Java 7 (2011)
ForkJoinPool : 工作窃取 + 取消传播
section Java 8 (2014)
CompletableFuture : 异步编排 + cancel
section Java 9 (2017)
Flow API : 响应式流 + Subscription.cancel()
section Java 19-21 (2022-2023)
StructuredTaskScope : 结构化取消
Virtual Threads : 虚拟线程(每任务一线程,减少复用问题)
核心教训:
- 隐式状态是万恶之源:
Thread.interrupt的隐式全局状态导致了所有混乱。 - 显式优于隐式:
AbortController的显式对象更易理解、测试、组合。 - 组合性是关键:好的取消机制应该能优雅地组合(父子、并行、超时等)。
- 资源安全优先:取消的最终目的是安全释放资源,不是立即停止线程。
[PATTERN] 中断机制的模式速查表:
| 遇到的问题 | 应用的模式 | 具体方案 | 关键注意点 |
|---|---|---|---|
| 需要取消阻塞中的线程 | 协作式中断 | interrupt() + 正确的异常处理 |
永远不要吞掉 InterruptedException |
| 需要可中断的锁获取 | AQS 立即中断 | lockInterruptibly() |
优于 synchronized 的中断响应 |
| 子任务可能吞掉中断 | 显式取消标志 | volatile boolean cancelled + interrupt 仅唤醒 |
取消标志是主信号,interrupt 是辅助唤醒 |
| 嵌套任务的级联取消 | 取消树 | AbortController 父子关联 |
每个任务独立控制器,自动级联 |
| 线程池中的任务取消 | 结构化并发 | StructuredTaskScope(Java 21+) |
取消与作用域绑定,自动传播 |
特别的切换方法
LockSupport.park
也就是线程挂起。
condition 的 await 底层调用的是 LockSupport.park。这个方法的参数是一个用作 monitor 的对象,会被设置到 Object 的特定 Offset 上。
park 只能带来 waiting。所以 sync 和 conditionObject 其实都让 thread waiting ,只不过代表 thread 的 node 处在的队列不一样而已-线程 node 在 sync queue 和 condition queue 都是 waiting。
JUC 的统一阻塞原语
LockSupport.park()/unpark() 是 JUC 中绑大多数阻塞/唤醒机制的底层基础。与传统的 Object.wait()/notify() 相比,它有两个关键优势:
- 不需要持有监视器锁:
wait()必须在synchronized块内调用,而park()可以在任意位置调用 - unpark 可以先于 park 调用:如果先调用
unpark(thread),后续该线程调用park()会立即返回(permit 机制) - permit 不可累加:多次调用
unpark()只设置一个 permit,后续park()只消费一次。与令牌桶模式(可累积令牌支持 burst)不同,park/unpark 更像不支持 burst 的漏桶——permit 永远只有 0 或 1
JUC 中的两种使用模式:
| 模式 | 代表组件 | 调用路径 |
|---|---|---|
| 直接使用 | FutureTask.awaitDone() |
LockSupport.park(this) → finishCompletion() 中 LockSupport.unpark(t) |
| 通过 AQS 间接使用 | ReentrantLock、Semaphore、CountDownLatch |
AQS.parkAndCheckInterrupt() → AQS.unparkSuccessor() |
设计约束:每个 park() 点都必须有对应的 unpark() 路径,否则线程会永久阻塞。这就是为什么 FutureTask 需要维护 waiters 链表(Treiber Stack)——记录所有等待线程的引用,确保 finishCompletion() 能遍历并 unpark() 每一个。
例外情况:
BlockingQueue实现(ArrayBlockingQueue、LinkedBlockingQueue)使用Condition.await()/signal(),底层仍是 park/unparkThread.sleep()不使用 park,是独立的 native 实现synchronized块的阻塞由 JVM 监视器实现,通过 Parker 最终调用 OS 原语(详见《线程安全与锁优化》)
wait
这个方法是对 object 用的。
从 wait 中醒来会有伪唤醒的 case,所以醒来的时候一定要先检查唤醒条件是否已经得到满足。原理见《为什么条件锁会产生虚假唤醒现象(spurious wakeup)?》
join
Thread.join() 执行流程
sequenceDiagram
participant th1 as 调用线程 (th1)
participant th2 as 目标线程对象 (th2)
participant JVM as JVM 运行时
Note over th1,JVM: th1 调用 th2.join()
th1->>th2: synchronized(th2) 获取 Monitor 锁
loop while (th2.isAlive())
th1->>th2: 检查 th2.isAlive()
th2-->>th1: true(仍在运行)
th1->>th2: th2.wait(0) 释放锁,th1 进入 WAITING
Note over th1: th1 挂起,释放 th2 的 Monitor 锁
Note over th2: th2 继续执行任务...
end
th2->>JVM: th2.run() 执行完毕
JVM->>th2: 线程终止,自动调用 th2.notifyAll()
th2-->>th1: 唤醒 th1
th1->>th2: 重新获取 th2 的 Monitor 锁
th1->>th2: 检查 th2.isAlive()
th2-->>th1: false(已终止)
Note over th1: 退出 while 循环,join() 返回
th1->>th1: 继续执行后续代码
1 | |
- 线程的 join 相当于当前线程在另一个会死亡的线程对象上等待,在 while 循环里无限 wait,在超时或者该线程死亡的时候从 wait 里解脱出来。
- 每个 thread 对象的内置状态变成死亡的时候,JVM 会主动调用这个对象的 notifyAll,这和任意条件对象的 wait 和 notifyAll 由程序员自己控制是不一样的。
关于线程池、异步编程、CompletableFuture 等高级并发工具:join 主要用于简单的线程间等待,但对于复杂的并发任务编排,建议使用线程池和 CompletableFuture 等更强大的工具。详细内容请参阅《Java 线程池笔记》。
虚拟线程对管程模型的影响(JDK 21+)
JDK 21 引入的虚拟线程(Virtual Threads)对传统管程模型带来了新的考量。
Pinned 问题
虚拟线程在 synchronized 块内调用阻塞方法(如 wait()、I/O 操作)时,会被钉住(pinned),无法让出载体线程:
1 | |
JDK 21+ 的实践建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 虚拟线程中需要等待/通知 | ReentrantLock + Condition |
避免 pinned 问题 |
| 虚拟线程中的简单互斥 | synchronized(临界区短时) |
可接受,但需确保无阻塞操作 |
| 高并发 I/O 场景 | 虚拟线程 + ReentrantLock |
最大化载体线程利用率 |
关键结论:虚拟线程环境下,管程的互斥部分(synchronized)可能成为性能瓶颈。这是 JDK 21 推荐在高并发场景使用 ReentrantLock 的原因之一。
虚拟线程与操作系统层面的约束:虚拟线程在 synchronized 块内阻塞会被 Pinned,因为 synchronized 的实现依赖 OS mutex,无法让出载体线程。这再次印证了本文开篇的原则——高级抽象要基于底层系统,不能超出操作系统允许的范围。
关于 JMM、volatile、内存模型等内容,请参阅《JVM 的内存模型与线程》。关于 JUC、锁等内容,请参阅《线程安全与锁优化》。






