juc.png
juc.xmind

写在前面的话

并发编程最早的实践都在操作系统里。高层语言的并发模型都要基于底层系统对硬件抽象和并发的设计来设计和实现,不能超出操作系统允许的范围。所谓的高级抽象总体上是简化对 OS 底层机制的复杂调用。

并发与异步

本文聚焦并发(Concurrency),即多任务在同一时间段内的交替或并行执行,核心问题是资源共享、线程同步与协作。

**异步(Asynchronous)**是另一维度:调用方发起操作后不等结果返回即继续执行,通过回调、Future或事件机制获取结果。异步可通过单线程事件循环实现,也可依托多线程并发实现。

二者关系:并发关注"多任务如何执行与协调",异步关注"调用是否阻塞等待"。并发编程常涉及异步,但本文不展开异步编程模式(如响应式流、协程),相关内容请参阅《Java 线程池笔记》

管程

理论和实践之间是有鸿沟的,要弥合这种鸿沟,通常需要我们去学习别人的实践。比如并发的标准设计思想来自于操作系统里的管程(monitor),我们应当学习管程,进而了解标准的并发模型-管理共享变量和线程(并发任务)间通信的基本理论模型。

什么是管程

管程(Monitor)是一种并发编程的抽象数据结构,由 C.A.R. Hoare 和 Per Brinch Hansen 在 1970 年代提出。它比"锁"的概念更大——锁只是管程的一部分。

管程的完整定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────────┐
│ 管程(Monitor) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. 共享数据(Shared Data) │ │
│ │ 被保护的数据结构/变量,只能通过管程的方法访问 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 2. 互斥机制(Mutual Exclusion) │ │
│ │ 同一时刻只有一个线程能执行管程内的代码 │ │
│ │ 即"临界区"的保护 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ 3. 条件变量(Cond) │ │ 3. 条件变量(Cond) │ │
│ │ 条件A: 等待队列 │ │ 条件B: 等待队列 │ ... │
│ │ wait() / signal() │ │ wait() / signal() │ │
│ └───────────────────────┘ └───────────────────────┘ │
│ │ │ │
│ └───────────────┬───────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 4. 入口队列(Entry Queue) │ │
│ │ 等待进入管程的线程,竞争获取执行权 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

管程 ≠ 锁

  • 锁只提供互斥,解决"同一时刻只有一个线程进入"
  • 管程还提供条件同步,解决"线程间协作等待"

管程与 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────┐
│ Monitor BoundedBuffer │
├─────────────────────────────────────────────────────────────────────┤
│ 共享数据: buffer[N], count, in, out │
├─────────────────────────────────────────────────────────────────────┤
│ 条件变量: notFull (缓冲区不满), notEmpty (缓冲区不空) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ put(item): │
│ synchronized(buffer) { │
while (count == N) buffer.wait(); // notFull 等待 │
buffer[in] = item; count++; in = (in+1) % N; │
buffer.notifyAll(); // signal notEmpty │
│ } │
│ │
│ get(): │
│ synchronized(buffer) { │
while (count == 0) buffer.wait(); // notEmpty 等待 │
│ item = buffer[out]; count--; out = (out+1) % N; │
buffer.notifyAll(); // signal notFull │
return item; │
│ } │
└─────────────────────────────────────────────────────────────────────┘

注意:Java 的 synchronized 只有单一 Wait Set,所以用 notifyAll() 唤醒所有等待者
ReentrantLock 可以创建多个 Condition,实现精确唤醒

三种管程模型

历史上出现过三种管程模型,它们的区别在于 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 有以下原因:

  1. 实现简单:不需要在 signal 后立即切换线程上下文
  2. 与 OS 调度器兼容:被唤醒线程进入入口队列,由 OS 调度器统一管理
  3. 公平性更好:被唤醒线程重新竞争,避免新来的线程一直等待
  4. 避免优先级反转:高优先级线程 signal 后继续执行,不会被低优先级的等待者阻塞

代价:开发者必须在 while 循环中重新检查条件,因为被唤醒后条件可能已被其他线程改变。

Mesa 模型

Java 采用 Mesa 模型:

  • 互斥(Mutual Exclusion):通过锁机制保证同一时刻只有一个线程能进入管程内部执行。
  • 同步(Synchronization):利用条件变量(Condition Variable)实现线程间的等待与唤醒。
  • Signal and Continue:

当线程发出通知(signal/notify)时,它继续持有锁并运行,而被唤醒的线程仅仅是进入就绪队列,并不立即抢占 CPU。

  • 必须使用 while 循环:

由于线程被唤醒后不一定立即执行,当它重新获得锁时,环境条件可能已发生变化,因此必须在一个 while 循环中重新检查等待条件(while
(condition) { wait(); })。

为什么用 set 而不用 queue

  1. Queue 暗示 FIFO(先进先出):
  • 如果我们叫它 EntryQueue,开发者会本能地认为:先来的线程一定先拿到锁。
  • 但实际上,Java 的 synchronized 是非公平锁(Non-fair Lock)。
  1. 实际上更像“一堆人”而不是“一队人”:
  • 在 JVM 的具体实现策略中,当锁被释放时,并不保证 EntrySet 中排在最前面的线程一定能抢到锁(可能被刚来的线程抢走,或者被随机唤醒)。
  • 对于 WaitSet,notify() 唤醒的线程也不一定是先 wait() 的那个线程(取决于具体 JVM 实现)。
  • 所以,用 Set(集合) 这个词能更准确地表达“这里有一群线程在等,但谁先出去不一定”的语义。

总结:叫 Set 是为了告诉你,不要依赖它们的唤醒顺序。

Entry Set 的命名含义

一个常见的问题是:为什么等待的队列明明没有进入同步块,却叫 Entry Set?

答案:Entry Set 的 “Entry” 不是指"进入同步块",而是指"进入管程的入口队列"

从管程(Monitor)的角度看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
管程(Monitor)就像一个"房间"
┌─────────────────────────────────────┐
│ 管程(Monitor) │
│ ┌───────────────────────────────┐ │
│ │ Entry Set(入口队列) │ │ ← 等待进入房间的队列
│ │ - 线程在这里排队 │ │
│ │ - 还没有拿到"入场券"(锁) │ │
│ └───────────────────────────────┘ │
│ ↓ 获取锁 │
│ ┌───────────────────────────────┐ │
│ │ Owner(持有者) │ │ ← 已经在房间里的人
│ │ - 持有锁的线程 │ │
│ │ - 正在执行临界区代码 │ │
│ └───────────────────────────────┘ │
│ ↓ 调用 wait()
│ ┌───────────────────────────────┐ │
│ │ Wait Set(等待队列) │ │ ← 暂时离开房间的人
│ │ - 释放锁,等待条件 │ │
│ │ - 被唤醒后要重新排队 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘

为什么叫 Entry Set

  1. Entry = 入口:这是线程想要进入管程(Monitor)的入口排队区
  2. 还没进入:线程确实还没有进入管程内部(没有拿到锁)
  3. 准备进入:但它们已经在管程的"门口"排队了

对比 Wait Set

队列 位置 状态 含义
Entry Set 管程的入口 等待获取锁 想要进入管程的线程
Wait Set 管程的休息区 等待条件满足 已经进入过管程,但暂时离开

一个形象的比喻

想象一个餐厅

  • 餐厅 = 管程(Monitor)
  • 餐桌 = 临界区资源
  • 座位 = 锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────┐
│ 餐厅 │
│ │
│ 【门口排队区】Entry Set
│ - 顾客在这里排队等位 │
│ - 还没有拿到座位 │
│ - 准备进入餐厅 │
│ ↓ 空出座位 │
│ 【用餐区】Owner │
│ - 已经拿到座位的顾客 │
│ - 正在用餐(执行代码) │
│ ↓ 去洗手间 │
│ 【洗手间】Wait Set
│ - 暂时离开餐桌的顾客 │
│ - 等待"洗手间空出来"(条件满足) │
│ - 出来后要重新排队等座位 │
└─────────────────────────────────────┘
  • Entry Set = 门口排队等位的顾客(还没拿到座位)
  • Owner = 正在用餐的顾客(已经拿到座位)
  • Wait Set = 去洗手间的顾客(暂时离开座位,回来要重新排队)

关键理解

  1. Entry Set 的线程

    • 想要进入管程
    • 还没有拿到锁
    • 在管程的入口排队
    • 状态:BLOCKED
  2. Wait Set 的线程

    • 已经进入过管程
    • 主动释放锁(调用 wait()
    • 等待条件满足
    • 被唤醒后要重新进入 Entry Set 排队
    • 状态:WAITING

所以,Entry Set 的命名是准确的:它是线程想要进入管程的入口队列,而不是"已经进入同步块的队列"。

模型映射

Mesa 模型 Mesa 语义 synchronized ReentrantLock Java State 超时 JVisualVM 底层机制
Entry Set
(锁竞争)
等待获取锁 Monitor Entry List AQS Sync Queue BLOCKED
WAITING (parking)
Monitor
Park
ObjectMonitor
LockSupport.park()
Wait Set
(条件等待)
等待条件满足 Monitor Wait Set AQS Condition Queue WAITING
TIMED_WAITING
Wait
Park
ObjectMonitor
LockSupport.park()
Owner
(持有者)
持有锁的线程 Monitor Owner exclusiveOwnerThread RUNNABLE - Running -

管程视角 vs Java 线程状态

一个常见的问题是:按照 Mesa 模型,线程是否只有三种状态——Owner、Entry Set、Wait Set?

答案是:管程视角和 Java 线程状态是两个不同的抽象层次。

管程视角看,相对于某个特定管程,线程确实只有四种位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────────┐
│ 管程视角的线程位置 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Outside ──→ Entry Set ──→ Owner ──→ Wait Set ──→ Entry Set ──→ ...│
│ │ │ │ │ │
│ │ │ │ │ │
│ 未涉及 等待获取锁 持有锁 等待条件 │
│ 该管程 (想进进不去) (在临界区) (主动释放) │
│ │
│ 注意:Outside 的线程可能正在执行其他代码, │
│ 对当前管程来说"不存在"
└─────────────────────────────────────────────────────────────────────┘

但从 Java 线程状态看,有六种状态,且两者不是一一对应

管程位置 对应的 Java 线程状态 说明
Outside NEW / RUNNABLE / TERMINATED Outside 不等于"空闲",线程可能在执行其他工作
Entry Set BLOCKED(synchronized)或 WAITING(ReentrantLock) 同一管程位置,Java 状态因实现不同而异
Owner RUNNABLE 持有锁,正在执行临界区代码
Wait Set WAITINGTIMED_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) → Owner
    • ReentrantLock: 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: synchronizedBLOCKED 状态
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// java 从 1.0 开始就实现了
public class Thread implements Runnable {
private Runnable target;

public Thread(Runnable target) {
this.target = target;
}

@Override
public void run() {
if (target != null) {
target.run(); // 只能调用 run(),没有返回值
}
}
}

HotSpot JVM 中的线程创建

底层的 cpp 源码是(以下代码为简化示意,基于 HotSpot JDK 11,不同版本实现可能有差异):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// HotSpot JVM 源码:thread.cpp (JDK 11)
JVM_ENTRY(void, JVM_StartThread(JNIEnv* jni, jobject jthread))
JVMWrapper("JVM_StartThread");

// 1. 从 Java 对象获取 C++ Thread 对象
JavaThread* native_thread = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));

// 2. 状态检查
if (native_thread->is_being_ext_suspended()) {
native_thread->set_terminated_before_start(true);
}

if (native_thread != NULL) {
if (!native_thread->has_been_started()) {
// 3. 创建操作系统线程
os::create_thread(native_thread, thr_type, stack_size);

// 4. 设置线程状态为 INITIALIZED
native_thread->set_state(INITIALIZED);

// 5. 启动操作系统线程
os::start_thread(native_thread);
}
}
JVM_END

// Linux 实现 (os_linux.cpp)
bool os::create_thread(Thread* thread, ThreadType thr_type) {
// 1. 创建 pthread_attr_t 属性
pthread_attr_t attr;
pthread_attr_init(&attr);

// 2. 设置栈大小
size_t stack_size = ...; // 根据线程类型设置
pthread_attr_setstacksize(&attr, stack_size);

// 3. 关键:创建 pthread 线程
pthread_t tid;
int ret = pthread_create(&tid, &attr, thread_native_entry, thread);

if (ret == 0) {
// 4. 保存线程 ID
thread->set_thread_id(tid);
return true;
}
return false;
}

// 线程入口函数(entry point,而非回调)
// 入口函数:新线程被 OS 调度器首次选中执行时的起点(主动)
// 回调:注册一个函数,等某个异步事件完成后被调用(被动)
// pthread_create 的第三个参数指定的就是这个入口函数
static void* thread_native_entry(Thread* thread) {
// 1. 设置线程状态
thread->set_state(RUNNABLE);

// 2. 关键:调用 Java 层的 run() 方法
thread->run();

// 3. 线程结束处理
return NULL;
}

// HotSpot JVM 中的关键结构,这个 javaThread 既持有操作系统线程句柄,也持有 Java 线程句柄。
// 这样实现了平台无关性。
// JavaThread 确实是三位一体的设计:
// 1. JVM 层:JavaThread* 本身(管理 JVM 内部状态)
// 2. OS 层:OSThread* _osthread(操作系统资源)
// 3. Java 层:oop _threadObj(java.lang.Thread 对象)
class JavaThread: public Thread {
private:
oop _threadObj; // 对应的 Java Thread 对象
OSThread* _osthread; // 操作系统线程
volatile JavaThreadState _state; // 线程状态

public:
void run() {
// 调用 Java 层的 run() 方法
this->thread_main_inner();
}

void thread_main_inner() {
if (has_java_lang_thread()) {
// 通过 JNI 调用 Java 层的 run() 方法
JavaCalls::call_virtual(
&result,
klass,
method,
threadObj,
CHECK
);
}
}
};

// 通过 JNI 调用 Java 方法
static void call_run_method(JNIEnv* env, jobject jthread) {
jclass threadClass = env->FindClass("java/lang/Thread");
jmethodID runMethod = env->GetMethodID(threadClass, "run", "()V");

// 调用 Thread.run() 方法
env->CallVoidMethod(jthread, runMethod);
}

线程创建的调用链

整体调用的流程是从 Java 到 C++ 再到 Java 的:

1
2
3
4
5
6
7
8
9
10
11
12
// 调用链:
thread.start()
→ Thread.start() [Java]
start0() [native]
JVM_StartThread() [JVM C++]
→ os::create_thread() [JVM C++]
pthread_create() [Linux C]
thread_native_entry() [JVM C++]
→ JavaThread::run() [JVM C++]
→ JNI: CallVoidMethod(threadObj, "run") [JNI]
→ Thread.run() [Java]
→ target.run() [Java] // 最终调用用户代码

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 中看到 parkingmonitor 的根本原因。

管程语义层面的差异

维度 synchronized ReentrantLock
条件变量数量 单一 Wait Set 多个 Condition(newCondition() 可创建任意数量)
条件唤醒精度 notifyAll() 唤醒所有等待者 condition.signal() 精确唤醒特定条件队列
管程粒度 对象级别(锁绑定到对象) 代码块级别(锁独立于对象)
可中断性 等待锁时不可中断(BLOCKED lockInterruptibly() 可响应中断

关键洞察:当需要多个条件变量(如生产者-消费者中的"不满"和"不空")时,ReentrantLock 的多 Condition 设计比 synchronized 的单一 Wait Set 更精确,避免不必要的唤醒。

java-thread-state.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public class MesaMonitorExample {
private final Object lock = new Object();
private boolean conditionSatisfied = false;

// === 等待者线程 (Thread-A) ===
public void doWait() {
// [阶段 1]:尝试进入 synchronized 块
// 状态详情:
// 1. Monitor 锁:未持有(正在 Entry Set 排队竞争)
// 2. CPU:未持有(被 OS 挂起)
// 3. Mesa Set:处于 Entry Set (入口队列/锁池)
// 4. 线程状态:BLOCKED
synchronized (lock) {
// ============================================================
// [微观状态间隙 - 从抢到锁到执行第一行代码]
//
// 1. 【Lock Acquired】:
// Monitor 锁竞争成功。线程从 Entry Set 移出,成为 Owner。
// Java 线程状态:BLOCKED -> RUNNABLE。
//
// 2. 【OS Scheduling (Ready)】:
// 虽然 Java 认为你是 RUNNABLE,但在 OS 看来,你只是进入了
// "CPU 就绪队列" (Ready Queue),正在等待分配时间片。JVM 本身并不理解 OS 的这个队列,所以 RUNNABLE 包含了“可以运行”和“正在运行”两种状态。
// 此时:持有锁 | 未持有 CPU | 状态:RUNNABLE (Ready)。
//
// 3. 【Context Switch (Running)】:
// OS 调度器选中了本线程,加载寄存器,PC 指针指向下一行指令。
// 此时:持有锁 | 持有 CPU | 状态:RUNNABLE (Running)。
// ============================================================

// [阶段 2]:真正执行代码
// 状态详情:持有锁 | 持有 CPU | Owner (持有者) | RUNNABLE (Running)
System.out.println("Thread-A: Acquired lock, checking condition...");

while (!conditionSatisfied) {
try {
System.out.println("Thread-A: Condition false, calling wait()...");

// [阶段 3:主动入冷宫 - 调用 wait()]
// 状态详情(执行瞬间):
// 1. Monitor 锁:原子性释放
// 2. CPU:主动放弃
// 3. Mesa Set:从 Owner 移入 Wait Set (第一重队列)
// 4. 线程状态:RUNNABLE -> WAITING
// 注意:此时线程完全“睡死”,必须等待 notify 救援
lock.wait();

// ============================================================
// [阶段 4:漫长的回归之路 - 穿越“两重队列”]
//
// 1. 【被 notify 唤醒时】:
// Thread-A 从 Wait Set 移出,直接被扔进 Entry Set (第二重队列)。
// 因为锁还在通知者手里!
// 此时状态:WAITING -> BLOCKED。
//
// 2. 【等待锁释放】:
// 在 Entry Set 中排队,直到通知者离开 synchronized。
//
// 3. 【竞争锁 & OS 调度】:
// 抢到锁 -> BLOCKED 变 RUNNABLE (Ready) -> 获得 CPU (Running)。
// ============================================================

// [阶段 5:重回舞台]
// 此时 Thread-A 已经成功拿回了锁和 CPU
System.out.println("Thread-A: Woke up, re-acquired lock.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

System.out.println("Thread-A: Condition met, doing work.");
}
// [阶段 6]:离开 synchronized 块
// 状态详情:释放锁 | 持有 CPU | 离开 Owner 变为 Outside | RUNNABLE
}

// === 通知者线程 (Thread-B) ===
public void doNotify() {
synchronized (lock) {
// [阶段 7]:获取锁
// 状态详情:持有锁 | 持有 CPU | Owner | RUNNABLE
System.out.println("Thread-B: Acquired lock, changing condition...");
conditionSatisfied = true;

// [阶段 8:只管唤醒,不管开门]
// 状态详情(关键点):
// 1. Monitor 锁:仍然持有!(Signal and Continue)
// 2. Mesa Set:Owner(Thread-B 还在舞台上)
// 3. 对 Thread-A 的影响:将 A 从 Wait Set 移入 Entry Set (BLOCKED)
lock.notify();

System.out.println("Thread-B: Notified, but STILL holding lock.");

// 模拟 Thread-B 继续占用锁,此时 Thread-A 只能在 Entry Set 阻塞
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
// [阶段 9:真正放手]
// 离开 synchronized 块,释放 Monitor 锁。
// 此时 Entry Set 里的 Thread-A 才有机会去抢锁,完成它的“回归之路”。
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
// 显式锁(替代 synchronized)
private final ReentrantLock lock = new ReentrantLock();
// 显式条件变量(替代 Object monitor methods)
private final Condition condition = lock.newCondition();

private boolean conditionSatisfied = false;

// === 等待者线程 (Thread-A) ===
public void doAwait() {
// [阶段 1]:尝试获取锁
// 状态详情:
// 1. AQS State:尝试 CAS 修改 state。
// 2. AQS Queue:如果失败,进入 AQS Sync Queue (同步队列) 排队。
// 3. 线程状态:BLOCKED (Parked)。
lock.lock();

try {
// ============================================================
// [微观状态间隙 - AQS 版]
//
// 1. 【Lock Acquired】:
// CAS 成功,或被前驱节点唤醒。
// 线程从 AQS Sync Queue 移出 (Head 节点后继)。
// State:BLOCKED -> RUNNABLE。
//
// 2. 【OS Scheduling】:
// 进入 OS Ready Queue,等待 CPU。JVM 本身并不理解 OS 的这个队列,所以 RUNNABLE 包含了“可以运行”和“正在运行”两种状态。
// 此时:持有 ReentrantLock | 未持有 CPU。
//
// 3. 【Running】:
// 获得 CPU 时间片,开始执行下一行。
// ============================================================

// [阶段 2]:真正执行代码
System.out.println("Thread-A: Acquired lock, checking condition...");

while (!conditionSatisfied) {
try {
System.out.println("Thread-A: Condition false, calling await()...");

// [阶段 3:主动入冷宫 - 调用 await()]
// 状态详情(执行瞬间):
// 1. Lock 释放:彻底释放锁(fullyRelease),无论重入多少次。
// 2. Mesa 位置:
// a. 构造一个 Node,加入 Condition Queue (条件队列)。
// b. 线程被挂起 (LockSupport.park)。
// 3. 线程状态:RUNNABLE -> WAITING。
condition.await();

// ============================================================
// [阶段 4:AQS 内部的漫长回归之路]
//
// 1. 【Signal 发生时】:
// Thread-A 的 Node 从 Condition Queue 被“踢”到了 AQS Sync Queue 尾部。
// 注意:此时它仅仅是换了个队排,锁还在 Signal 线程手里!
// 状态:WAITING -> BLOCKED (等待获取锁)。
//
// 2. 【等待锁释放】:
// 在 AQS Sync Queue 中自旋或挂起,直到轮到自己。
//
// 3. 【抢锁成功】:
// acquireQueued 返回,从 await() 方法内部返回。
// ============================================================

// [阶段 5:重回舞台]
// 此时 Thread-A 处于 AQS Sync Queue 的 Head 位置并拿到了锁
System.out.println("Thread-A: Woke up, re-acquired lock.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

System.out.println("Thread-A: Condition met, doing work.");
} finally {
// [阶段 6]:释放锁
// 必须在 finally 中释放!
// 动作:修改 AQS state,唤醒 AQS Sync Queue 中的下一个节点 (Successor)。
lock.unlock();
}
}

// === 通知者线程 (Thread-B) ===
public void doSignal() {
lock.lock(); // 获取锁
try {
// [阶段 7]:持有锁执行业务
System.out.println("Thread-B: Acquired lock, changing condition...");
conditionSatisfied = true;

// [阶段 8:只管迁移,不管开门]
// 关键点:signal() 仅仅是将节点从 Condition Queue 转移到 AQS Sync Queue。
// Thread-B **仍然持有锁**!
// Thread-A 此时在 AQS Sync Queue 尾部排队,状态从 WAITING 变为了 BLOCKED。
condition.signal();

System.out.println("Thread-B: Signaled, but STILL holding lock.");

// 模拟业务耗时
try { Thread.sleep(1000); } catch (InterruptedException e) {}

} finally {
// [阶段 9:真正放手]
// 释放锁 (state = 0)。
// 此时 AQS Sync Queue 里的 Thread-A (如果排在前面的话) 被 unpark 唤醒,
// 从而完成 await() 的返回。
System.out.println("Thread-B: Releasing lock...");
lock.unlock();
}
}
}

线程状态转换全景图

上图展示了六种 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

  1. 正在执行的线程。
  2. 可以被执行但没有拿到处理器资源。

BLOCKED

blocked 其实是 blocked waiting。

  1. 等待 monitor,进入 synchronized method/block
  2. 或者等 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
2
3
4
5
6
7
sun.misc.Unsafe.park 行: 不可用 [本地方法]
java.util.concurrent.locks.LockSupport.park 行: 175
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt 行: 836
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued 行: 870
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire 行: 1199
java.util.concurrent.locks.ReentrantLock$NonfairSync.lock 行: 209
java.util.concurrent.locks.ReentrantLock.lock 行: 285

jstack 总会告诉我们 waiting 的位置,比如等待某个 Condition 的 await 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);
ReentrantLock lock = new ReentrantLock();
final Thread t1 = new Thread(() -> {
System.out.println("t1 before lock");
lock.lock();
try {
// 此时 t1 是 Runnable
queue.put(1); // 此时刺激主线程开始读 t2
System.out.println("t1 begin to sleep");
Thread.sleep(1000000L);
} catch (Exception ex) {
}
System.out.println("t1 prepare to release lock");
lock.unlock();
System.out.println("t1 release lock");
});

final Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (Exception ex) {
}
System.out.println("t2 before lock");
// 此时 t2 可能被 t1 阻塞,进入 waiting 状态
lock.lock();
System.out.println("t2 prepare to release lock");
lock.unlock();
System.out.println("t2 release lock");
});
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
// 此时主线程在等待一个信号来刺激自己往下走
queue.take();
// 往下走的目的就是校验 t2 的状态
while (t2.isAlive()) {
System.out.println(t2.getState());
}
}

对这个程序进行 thread dump,可以看出 ReentrantLock 就是依赖于 park 导致的 waiting:

parking即waiting.png
sleeping即timed-waiting.png

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

object-monitor.png

所以 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

终结的线程,执行已经结束了。

中断退出也是一种结束。

几种线程状态的对比

  1. blocked:线程想要获取锁进入临界区之前,会求锁,求不到锁会进入 entry_set,然后放弃 cpu。高并发时 blocked 会增多。
  2. 工作线程池开始伸缩,扩容的时候:jvm.thread.waiting.count 的数量会变少。过程是,core 线程先满,然后队列再满,这时候等待从队列里获取任务,waiting 在 take 动作上的线程已经降为0了,然后开始产生非core线程,线程数才开始增长。
  3. 工作吞吐变多,而调用下游的工作线程在阻塞的时候,jvm.thread.time_waiting.count 会变多,因为 rpc 框架自带超时,而这些超时是会让工作线程进行计时等待的。
  4. 流量变大的时候,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
  1. 通常:
    1. 静态方法 = “我要操作当前线程”(self-operation)。static 相当于 per thread,一个好记的例子是通常 ThreadLocal 设置为 static 的,这样每个线程可以分到一个它的实例,而不是每个线程在每个对象里分到它的实例。
    2. 实例方法 = “我要操作指定线程”(cross-thread operation)。
  2. 这背后的逻辑是:
    1. 每个线程操作自己是比较安全的,static 可以默认在不指定对象的情况下操作自己。
    2. 而跨线程操作其他线程是比较危险的,因为其他线程的生死如果不是自然发展和结束的,很可能导致锁不释放,条件变量不正确设置,通知没有正确发出。这也就意味着系统可能死锁。
      1. 主动控制线程何时开始是安全的。
      2. 主动控制进程何时结束是危险的,因为你不能替他释放资源-这是禁止使用 stop、spend api 这类操作的全部理由。
    3. 可以跨线程操作的是比较温和的操作:
      1. start():可以让程序员开启线程周期。
      2. interrupt():可以设置一个标志位,算是轻微的主动写入别的线程状态的一种低侵入的 api。
      3. join(): 观测另一个对象的状态,通过内部自旋 wait 来等待另一个线程死亡。
    4. 其他 static 方法:
      1. yield():主动让出 CPU,让同优先级线程有机会运行。是对调度器的"建议",不保证效果。和 interrupt 的温和写入,但不必然强制操作形成对比。

线程中断机制深入解析

上一节我们提到,interrupt() 是一种"温和的跨线程写入"操作——它只是设置一个标志位,不会强制终止线程。这个看似简单的设计,背后隐藏着从操作系统信号机制到 JUC 框架的完整传递链路,也引发了 Java 社区关于"中断线程还是取消任务"的深层争论。本节将从 HotSpot 底层实现出发,逐层向上剖析 interrupt 的完整机制。

从 HotSpot 底层看 interrupt 的本质

JVM 层面的数据结构

在 HotSpot JVM 中,每个 Java 线程对应一个 JavaThread C++ 对象,其中维护着一个 volatile 的中断标志位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// HotSpot JVM 简化示意(C++)
class JavaThread : public Thread {
volatile bool _interrupted; // 中断标志位——线程级别的全局状态

public:
void interrupt() {
_interrupted = true;
// 如果线程正在阻塞(park/wait/sleep),唤醒它
if (is_blocked_on_park()) {
unpark(); // 唤醒 LockSupport.park()
}
if (is_blocked_on_monitor()) {
notify_monitor(); // 唤醒 Object.wait()
}
if (is_blocked_on_sleep()) {
wakeup_sleep(); // 唤醒 Thread.sleep()
}
}

bool is_interrupted(bool clear_interrupted) {
bool old = _interrupted;
if (clear_interrupted) {
_interrupted = false; // Thread.interrupted() 走这个分支
}
return old; // isInterrupted() 走 clear=false 分支
}
};

关键洞察_interrupted 是一个线程级别的全局布尔值,不是任务级别的。同一个线程上顺序执行的所有任务,共享这一个标志位。这是后续所有设计问题的根源。

interrupt() 与 unpark() 的辨析

一个常见的误解是认为 Thread.interrupt() 等价于"设置中断标志位 + unpark()"。虽然这个理解方向正确,但需要补充几个关键细节:

interrupt() 的完整动作分解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void interrupt() {
// 动作 1:设置中断标志位(始终执行)
this._interrupted = true;

// 动作 2:根据线程当前状态,选择唤醒方式
if (is_blocked_on_park()) {
unpark(); // LockSupport.park() 场景
}
if (is_blocked_on_monitor()) {
notify_monitor(); // Object.wait() 场景
}
if (is_blocked_on_sleep()) {
wakeup_sleep(); // Thread.sleep() 场景
}
// 注意:synchronized 等待锁时,无法唤醒!
}

关键差异点

  1. unpark() 不是唯一唤醒方式
阻塞方式 唤醒机制 是否抛异常 标志位处理
LockSupport.park() Unsafe.unpark() ❌ 不抛异常 ✅ 保留标志
Object.wait() ObjectMonitor 通知 ✅ 抛 InterruptedException ❌ 清除标志
Thread.sleep() OS 信号唤醒 ✅ 抛 InterruptedException ❌ 清除标志
synchronized 等待 无法唤醒 - ✅ 保留标志
  1. unpark() 的特殊语义

LockSupport.unpark() 有一个重要特性:

1
2
3
4
// 如果线程还没 park,unpark 会设置一个"permit"
// 下次 park() 会立即返回,不阻塞
LockSupport.unpark(thread); // 设置 permit = 1
LockSupport.park(); // 检测到 permit,立即返回,不清除中断标志!

这就是为什么 AQS 在 acquireQueued() 中必须用 Thread.interrupted()(清除版本):

1
2
3
4
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); // 必须清除标志,否则 park() 会立即返回!
}
  1. synchronized 的特殊处理

这是最关键的区别:

1
2
3
4
5
6
// synchronized 等待锁时,interrupt() 无法唤醒!
synchronized (lock) {
// 即使在等待锁的过程中被 interrupt()
// 也只是设置标志位,线程仍然在 BLOCKED 状态
// 必须等到获取锁后才能检查中断
}

原因:JVM 的 Monitor 实现基于操作系统的 mutex(如 Linux 的 pthread_mutex),而 OS mutex 不支持可中断的锁获取。这是"高级抽象要基于底层系统"的边界——Java 无法实现操作系统层面不支持的语义。

ReentrantLock.lockInterruptibly() 可以响应中断:

1
2
3
4
5
6
try {
reentrantLock.lockInterruptibly();
// 等待锁时被 interrupt(),立即抛出 InterruptedException
} catch (InterruptedException e) {
// 可以立即退出,不必等到获取锁
}

总结

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────┐
│ Thread.interrupt() 的唤醒路径 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 设置 _interrupted = true │
│ │ │
│ ├── 线程在 RUNNABLE 状态 │
│ │ └── 仅设置标志位,不做其他操作 │
│ │ 线程需要自己检查 isInterrupted() │
│ │ │
│ ├── 线程在 Object.wait() / Thread.sleep() / Thread.join() │
│ │ └── JVM 通过 OS 信号唤醒线程 │
│ │ → 线程从 native 方法返回 │
│ │ → 检测到 _interrupted == true │
│ │ → 清除标志位(_interrupted = false) │
│ │ → 抛出 InterruptedException │
│ │ │
│ ├── 线程在 LockSupport.park() │
│ │ └── JVM 调用 Unsafe.unpark() │
│ │ → park() 立即返回 │
│ │ → 不抛异常,不清除标志位 │
│ │ → 调用者需自行检查 Thread.interrupted() │
│ │ │
│ └── 线程在 NIO Channel 阻塞(如 Selector.select()) │
│ └── 关闭底层 fd(文件描述符) │
│ → 抛出 ClosedByInterruptException │
│ → Channel 被关闭 │
│ │
└─────────────────────────────────────────────────────────────────┘

关于 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
2
3
4
5
6
7
8
9
10
11
12
// 关键区别演示
Thread targetThread = new Thread(() -> {
// 假设此线程已被 interrupt

// 查询方式 1:不清除标志
targetThread.isInterrupted(); // 返回 true,标志仍为 true
targetThread.isInterrupted(); // 返回 true,标志仍为 true

// 查询方式 2:清除标志(静态方法,操作当前线程)
Thread.interrupted(); // 返回 true,标志变为 false!
Thread.interrupted(); // 返回 false,因为上一次已清除
});

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 情况 A:线程在 wait/sleep/join 阻塞时收到 interrupt
// → 抛出 InterruptedException,清除中断标志
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 进入这里时,中断标志已被清除为 false
// 如果不恢复,上层调用者将无法感知中断
Thread.currentThread().interrupt(); // 恢复中断标志
}

// 情况 B:线程在运行时收到 interrupt
// → 不抛异常,需要手动检查
while (!Thread.currentThread().isInterrupted()) {
doWork(); // 不会自动中断,需要在循环条件中检查
}

// 情况 C:线程在 LockSupport.park() 时收到 interrupt
// → park() 立即返回,不抛异常,不清除标志
LockSupport.park();
// park 返回后,中断标志仍为 true
// AQS 在这里用 Thread.interrupted() 检查并清除
if (Thread.interrupted()) {
// 记录中断状态,延迟处理
interrupted = true;
}

// 情况 D:线程在等待 synchronized 锁时收到 interrupt
// → 无法响应!必须等到获取锁后才能检查
synchronized (lock) {
// 即使在等待锁的过程中被 interrupt,
// 也必须等到进入这里才能检查
if (Thread.currentThread().isInterrupted()) {
// 处理中断
}
}

[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 模式 1:传播异常(推荐,让调用者决定如何处理)
public void doWork() throws InterruptedException {
Thread.sleep(1000); // 直接让异常传播
}

// 模式 2:恢复中断标志(当方法签名不能抛 InterruptedException 时)
public void doWork() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 必须恢复!
// 然后选择:return、break、或抛出非受检异常
throw new RuntimeException("Task interrupted", e);
}
}

// 模式 3:吞掉中断(几乎总是错误的)
public void doWork() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ❌ 空 catch 块——中断信号永久丢失
}
}

interrupt 在 AQS 中的传递机制

AQS(AbstractQueuedSynchronizer)是 java.util.concurrent 的基石,它对 interrupt 的处理方式与 Object.wait() 截然不同,体现了两种完全不同的中断哲学。

AQS 的"延迟中断"策略

AQS 的 acquire() 方法(非中断版本)采用延迟中断策略:先获取锁,再处理中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// AbstractQueuedSynchronizer.java(JDK 8)

// 入口方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// acquireQueued 返回 true 表示"获取锁的过程中曾被中断过"
// 此时锁已经获取成功,再补上一次 interrupt
selfInterrupt(); // → Thread.currentThread().interrupt()
}

// 核心排队方法
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false; // 记录是否曾被中断
for (;;) {
final Node predecessor = node.predecessor();
if (predecessor == head && tryAcquire(arg)) {
setHead(node);
predecessor.next = null;
return interrupted; // 获取成功,返回中断记录
}
if (shouldParkAfterFailedAcquire(predecessor, node) &&
parkAndCheckInterrupt()) // park 并检查中断
interrupted = true; // 记录中断,但不退出循环!
// 继续自旋尝试获取锁
}
}

// park 并检查中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞,可被 unpark 或 interrupt 唤醒
return Thread.interrupted(); // 检查并清除中断标志
// 注意:这里用的是 interrupted()(清除版本)
// 目的是:清除标志后继续自旋,避免 park() 立即返回
// 因为 park() 在中断标志为 true 时会立即返回(不阻塞)
}
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() 有一个关键特性:如果中断标志为 truepark() 会立即返回而不阻塞。如果不清除标志,线程将陷入"park → 立即返回 → 再 park → 立即返回"的忙等待循环,浪费 CPU。

AQS 的"立即中断"策略

acquire() 不同,acquireInterruptibly() 采用立即中断策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 可中断版本
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); // 入口处就检查
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node predecessor = node.predecessor();
if (predecessor == head && tryAcquire(arg)) {
setHead(node);
predecessor.next = null;
return;
}
if (shouldParkAfterFailedAcquire(predecessor, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); // 直接抛异常!不再自旋
}
} catch (Throwable t) {
cancelAcquire(node); // 取消节点
throw t;
}
}

两种策略的对比

方法 策略 中断时行为 典型使用者
lock()acquire() 延迟中断 记录中断,继续获取锁,成功后补 selfInterrupt() ReentrantLock.lock()
lockInterruptibly()acquireInterruptibly() 立即中断 立即抛出 InterruptedException,取消排队 ReentrantLock.lockInterruptibly()

ConditionObject.await() 中的中断检查

Condition.await() 的中断处理更加精细,它区分了"在 signal 之前被中断"和"在 signal 之后被中断"两种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ConditionObject.await() 简化逻辑
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); // 入口检查

Node node = addConditionWaiter(); // 加入条件队列
int savedState = fullyRelease(node); // 完全释放锁
int interruptMode = 0;

while (!isOnSyncQueue(node)) { // 还没被 signal 转移到同步队列
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break; // 被中断,退出等待
}

// 重新获取锁(可能再次阻塞)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;

// 根据中断模式决定处理方式
if (interruptMode == THROW_IE)
throw new InterruptedException(); // signal 前被中断 → 抛异常
else if (interruptMode == REINTERRUPT)
selfInterrupt(); // signal 后被中断 → 恢复标志
}

interrupt 与线程状态转换的关系

interrupt 可以触发多种线程状态转换,但其影响取决于线程当前所处的状态。完整的线程状态转换图见前文线程状态转换全景图,此处重点分析 interrupt 对各状态的影响:

interrupt 对不同状态线程的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────────────┐
interrupt() 对不同状态的影响 │
├─────────────────────────────────────────────────────────────────────┤
│ 当前状态 │ interrupt() 行为 │ 状态变化 │
├───────────────────┼─────────────────────────┼───────────────────────┤
RUNNABLE │ 仅设置标志位 │ RUNNABLERUNNABLE
BLOCKED │ 仅设置标志位,无法唤醒 │ BLOCKEDBLOCKED
│ │ (必须等获取锁后检查) │ │
WAITING(park) │ park()返回,保留标志 │ WAITINGRUNNABLE
WAITING(wait) │ 抛IE,清除标志 │ WAITINGRUNNABLE
TIMED_WAITING │ 抛IE或返回,取决于方法 │ TIMED_WAITING → │
│ │ │ RUNNABLE/BLOCKED
└───────────────────┴─────────────────────────┴───────────────────────┘

关键区别:synchronized vs ReentrantLock 对中断的响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// synchronized:等待锁时无法响应中断
synchronized (lock) {
// 如果另一个线程持有 lock,当前线程进入 BLOCKED 状态
// 此时 interrupt() 只设置标志位,不能唤醒线程
// 必须等到获取锁后才能检查中断
}

// ReentrantLock.lockInterruptibly():等待锁时可以响应中断
try {
reentrantLock.lockInterruptibly();
// 如果等待过程中被 interrupt,立即抛出 InterruptedException
} catch (InterruptedException e) {
// 可以立即响应中断,不必等到获取锁
}

这也是为什么在需要可中断的锁获取场景中,ReentrantLock 优于 synchronized 的原因之一。

Thread.interrupt 的五大设计缺陷

理解了 interrupt 的底层机制后,我们可以系统地分析它的设计缺陷。这些缺陷在简单场景中不明显,但在复杂的生产环境中会导致严重问题。

五大缺陷概览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────┐
│ Thread.interrupt 五大设计缺陷 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 缺陷一:中断粒度错误 │
│ ├── 问题:中断的是线程,而非任务 │
│ └── 后果:线程池中 interrupt 可能误伤正在执行的其他任务 │
│ │
│ 缺陷二:中断标志易被吞掉 │
│ ├── 问题:第三方库捕获 IE 后不恢复中断标志 │
│ └── 后果:上层调用者永远无法感知中断 │
│ │
│ 缺陷三:中断标志泄露 │
│ ├── 问题:子任务调用 Thread.interrupted() 清除了父任务的标志 │
│ └── 后果:父任务的中断信号被"消费",无法正确响应 │
│ │
│ 缺陷四:嵌套任务混乱 │
│ ├── 问题:无法区分中断来源(用户取消/超时/线程池关闭) │
│ └── 后果:不同来源需要不同处理,但中断机制无法区分 │
│ │
│ 缺陷五:行为不一致 │
│ ├── 问题:wait/sleep/join 抛异常清标志,park 静默保留 │
│ └── 后果:开发者需记住每种方法行为,是 bug 的温床 │
│ │
│ 根本原因:隐式全局状态(线程级标志)vs 显式任务状态 │
└─────────────────────────────────────────────────────────────────────┘

缺陷一:中断的是线程,而非任务

1
2
3
4
业务语义:我要取消"下载文件这个任务"
实现机制:我中断"执行下载的线程"

问题:一个线程可能同时执行多个任务!

在线程池场景中,一个 Worker 线程会顺序执行多个任务。Future.cancel(true) 的实现是调用 Thread.interrupt(),但这个中断信号是发给线程的,不是发给任务的:

1
2
3
4
5
6
7
8
9
10
ExecutorService executor = Executors.newFixedThreadPool(1); // 只有一个线程

Future<?> future1 = executor.submit(() -> downloadFile("大文件.zip"));
Future<?> future2 = executor.submit(() -> processData());

// 取消第一个任务
future1.cancel(true); // 调用 Worker 线程的 interrupt()

// 问题:如果 future1 已经完成,future2 正在执行,
// 那么 interrupt 信号会发给正在执行 future2 的线程!

Future.cancel(true) 的 Javadoc 明确警告:

“There are no guarantees beyond best-effort attempts to stop processing actively executing tasks.”

缺陷二:中断标志容易被"吞掉"

第三方库或子任务可能捕获 InterruptedException 后不恢复中断标志,导致上层调用者永远无法感知中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 糟糕的第三方库实现
public void badLibraryMethod() {
try {
doSomething();
} catch (Exception e) {
// 捕获所有异常,包括 InterruptedException
log.error("Error", e);
// 不恢复中断状态,也不重新抛出
}
}

// Worker 线程调用这个方法
while (!Thread.currentThread().isInterrupted()) {
badLibraryMethod(); // 如果内部吞掉了 InterruptedException,
// 这里的 isInterrupted() 检查将失效
}

缺陷三:中断标志泄露

这是最隐蔽的问题。当子任务操作了中断标志(特别是使用 Thread.interrupted() 清除了标志),父任务将无法观察到中断信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父任务:Worker 的主循环
class Worker implements Runnable {
public void run() {
while (!Thread.currentThread().isInterrupted()) { // 检查点 A
try {
Task task = getNextTask();
task.run(); // 子任务可能操作中断标志!
// 检查点 B:如果子任务调用了 Thread.interrupted(),
// 这里的循环条件判断就可能出错
} catch (InterruptedException e) {
break;
}
}
}
}

// 子任务:错误地使用了 Thread.interrupted()
class DownloadTask implements Runnable {
public void run() {
if (Thread.interrupted()) { // 读取并清除标志!
System.out.println("下载被取消");
return;
}
download();
}
}

泄露的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
时间线  外部线程        Worker线程         中断标志    DownloadTask
│ │ │ │ │
│ │ │ ←启动 false
T1 │ │ │ │
│ │ │ │ │
T2 interrupt() ────→ │ │ │
│ │ │ 设置标志 → true
│ │ │ │ │
T3 │ isInterrupted() │ │
│ │ 返回true,进入循环体 │ │
│ │ │ │ │
T4 │ │ ──task.run()──→ │ │
│ │ │ │ │
T5 │ │ interrupted() │
│ │ │ 返回true ────→ false
│ │ │ (清除标志!) │
│ │ │ │ │
T6 │ │ ←──返回──────── │ │
│ │ │ │ │
T7 │ isInterrupted() │ │
│ │ 返回false! │ │
│ │ (本应退出,却继续!) │ │
↓ │ │ │ │

问题根源:子任务调用 Thread.interrupted() 清除了父任务的中断标志

泄露的本质:子任务"消费"了本该属于父任务的中断信号。中断信号就像一封信,被中间人拆开读了还扔掉了,真正的收件人永远收不到。

缺陷四:嵌套任务时的混乱

在实际应用中,任务往往是层级嵌套的:

1
2
3
4
5
6
7
主任务:DownloadAndProcessVideo
├── 子任务 1DownloadVideo
│ ├── 子子任务 1.1DownloadSegment(0)
│ ├── 子子任务 1.2DownloadSegment(1)
│ └── 子子任务 1.3DownloadSegment(2)
├── 子任务 2ExtractAudio
└── 子任务 3MergeFiles

当用户点击"取消"时,中断信号应该如何传播?使用 Thread.interrupt() 面临多重困境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void downloadAndProcess(String videoId) {
try {
File video = downloadVideo(videoId); // 可能调用 interruptible 方法
File audio = extractAudio(video);
merge(video, audio);
} catch (InterruptedException e) {
// 这个中断是谁发的?
// A) 用户取消了整个 downloadAndProcess?
// B) downloadVideo 内部某个子操作超时?
// C) 线程池在 shutdown?
// 不同的来源,应该有不同的处理方式!
cleanup();
}
}

缺陷五:行为不一致增加心智负担

前文已经详细分析了不同阻塞方法对 interrupt 的响应差异。总结来说:

  • wait()/sleep()/join():抛异常 + 清除标志
  • LockSupport.park():静默返回 + 保留标志
  • synchronized 等待:完全忽略
  • NIO Channel:抛异常 + 关闭 Channel

开发者需要记住每种方法的行为,并在每个 catch 块中做出正确的处理决策。这种不一致性是 bug 的温床。

替代方案:任务级取消机制

interrupt 的核心问题在于:取消的粒度是线程,而非任务。现代并发编程的趋势是将取消机制从线程级提升到任务级。

AbortController 模式

借鉴 Web 标准的 AbortController,可以为每个任务创建独立的取消控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 任务级取消控制器
* 核心优势:每个任务有独立的取消状态,不共享线程级全局标志
*/
public class AbortController {
private final AtomicBoolean aborted = new AtomicBoolean(false);
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
private final AbortController parent;

public AbortController() { this(null); }

public AbortController(AbortController parent) {
this.parent = parent;
if (parent != null) {
if (parent.isAborted()) {
abort(); // 父已取消,子立即取消
} else {
parent.onAbort(this::abort); // 监听父取消事件
}
}
}

/** 触发取消(幂等) */
public void abort() {
if (aborted.compareAndSet(false, true)) {
for (Runnable listener : listeners) {
try { listener.run(); } catch (Exception ignored) {}
}
}
}

/** 查询状态——不会清除!这是与 Thread.interrupted() 的关键区别 */
public boolean isAborted() { return aborted.get(); }

/** 注册取消回调 */
public void onAbort(Runnable listener) {
if (isAborted()) { listener.run(); }
else { listeners.add(listener); }
}

/** 检查并抛出 */
public void throwIfAborted() {
if (isAborted()) throw new CancellationException("Operation aborted");
}
}

Thread.interrupt 的对比

维度 Thread.interrupt AbortController
取消对象 线程(Thread) 任务(Task/Operation)
状态存储 线程对象的内部字段 独立的控制器对象
作用范围 整个线程生命周期 可控的代码块范围
层级关系 扁平(一个线程一个标志) 树形(父子控制器可关联)
清除行为 interrupted() 会自动清除 持久状态,不会被清除
隔离性 无(共享线程标志) 有(每个任务独立控制器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AbortController 的隔离机制:

┌─────────────────────────────────────────┐
│ Worker Thread │
│ ├─ AbortController workerCtrl │
│ │ └─ isAborted() = false
│ │ │
│ │ ├─ Task A (独立上下文) │
│ │ │ ├─ AbortController ctrlA │
│ │ │ │ └─ isAborted() = false
│ │ │ └─ 只能看到 ctrlA 的状态 │
│ │ │ │
│ │ └─ Task B (独立上下文) │
│ │ ├─ AbortController ctrlB │
│ │ │ └─ isAborted() = true
│ │ └─ 只能看到 ctrlB 的状态 │
│ │ │
│ │ workerCtrl、ctrlA、ctrlB 完全独立 │
│ └─ 不会互相干扰 │
└─────────────────────────────────────────┘

结构化并发(Java 19+)

Java 从 JDK 19 开始引入结构化并发(StructuredTaskScope),将取消与代码作用域绑定,从语言层面解决了 interrupt 的设计缺陷:

1
2
3
4
5
6
7
8
9
10
11
// Java 21+ 结构化并发:取消与作用域绑定,自动传播
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> download = scope.fork(() -> downloadFile());
Subtask<String> process = scope.fork(() -> processData());

scope.join(); // 等待所有子任务
scope.throwIfFailed(); // 任一失败则取消其他

// 自动取消未完成的子任务,自动等待清理
}
// 作用域结束,所有子任务保证已终止

Kotlin 协程的取消

Kotlin 协程提供了更优雅的取消机制,每个协程有自己的 isActive 状态:

1
2
3
4
5
6
7
8
9
10
suspend fun downloadData() {
val job = launch {
while (isActive) { // 类似 AbortController.isAborted()
doWork()
}
}
delay(1000)
job.cancel() // 发送取消信号,不会中断线程
job.join() // 等待协程完成清理
}

interrupt 最佳实践

尽管 interrupt 有设计缺陷,但它仍然是当前 Java 生态的底层基础设施。以下是在不同场景下的最佳实践:

规则一:永远不要吞掉 InterruptedException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// [错误]:空 catch 块
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* 中断信号永久丢失 */ }

// [正确]:恢复中断状态
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Task interrupted", e);
}

// [正确]:直接传播
public void doWork() throws InterruptedException {
Thread.sleep(1000); // 让调用者决定如何处理
}

规则二:优先使用 isInterrupted() 而非 interrupted()

1
2
3
4
5
// [危险]:每次循环都清除标志
while (!Thread.interrupted()) { doWork(); }

// [安全]:只查询不清除
while (!Thread.currentThread().isInterrupted()) { doWork(); }

规则三:区分"取消"和"中断"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 推荐:使用显式的取消标志,interrupt 仅作为唤醒机制
public class CancellableTask {
private volatile boolean cancelled = false;

public void cancel() {
cancelled = true;
workerThread.interrupt(); // 仅用于唤醒阻塞
}

public void run() {
while (!cancelled) { // 主要检查取消标志
try {
doWork();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// interrupt 仅作为唤醒机制,继续循环检查 cancelled
}
}
}
}

决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
需要取消功能?
├── 简单场景(单次任务、无嵌套)
│ ├── 使用标准 Future / CompletableFuture
│ └── 遵循 interrupt 最佳实践

├── 复杂场景(嵌套任务、需要级联取消)
│ ├── 能使用结构化并发(Java 21+)?
│ │ └── 使用 StructuredTaskScope
│ ├── 能使用响应式编程?
│ │ └── 使用 Project Reactor / RxJava
│ └── 否则
│ └── 自定义 AbortController 机制

└── 遗留系统维护
└── 严格遵循 interrupt 规则,逐步重构

线程池中的强制中断与拒绝机制

理解了 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
2
3
4
5
6
7
scheduler.scheduleAtFixedRate(() -> {
try {
riskyOperation();
} catch (Throwable t) {
logger.error("Scheduled task failed, but will continue", t);
}
}, 0, 1, TimeUnit.SECONDS);

与 interrupt 机制的关联

线程池中的强制中断与拒绝机制,本质上是对 Thread.interrupt 的上层封装:

  • 拒绝策略处理的是"提交阶段"的过载
  • 中断机制处理的是"执行阶段"的取消
  • 两者共同构成线程池的流量控制和生命周期管理

最佳实践总结

  1. 不要依赖强制中断来停止任务,任务应该支持检查中断标志并优雅退出
  2. 优先使用 cancel(false),让正在执行的任务自然完成
  3. 合理选择拒绝策略,根据业务对任务丢失的容忍度选择
  4. 始终为周期任务捕获异常,避免任务静默停止

这些实践与前面 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 : 虚拟线程(每任务一线程,减少复用问题)

核心教训

  1. 隐式状态是万恶之源Thread.interrupt 的隐式全局状态导致了所有混乱。
  2. 显式优于隐式AbortController 的显式对象更易理解、测试、组合。
  3. 组合性是关键:好的取消机制应该能优雅地组合(父子、并行、超时等)。
  4. 资源安全优先:取消的最终目的是安全释放资源,不是立即停止线程。

[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() 相比,它有两个关键优势:

  1. 不需要持有监视器锁wait() 必须在 synchronized 块内调用,而 park() 可以在任意位置调用
  2. unpark 可以先于 park 调用:如果先调用 unpark(thread),后续该线程调用 park() 会立即返回(permit 机制)
  3. permit 不可累加:多次调用 unpark() 只设置一个 permit,后续 park() 只消费一次。与令牌桶模式(可累积令牌支持 burst)不同,park/unpark 更像不支持 burst 的漏桶——permit 永远只有 0 或 1

JUC 中的两种使用模式

模式 代表组件 调用路径
直接使用 FutureTask.awaitDone() LockSupport.park(this)finishCompletion()LockSupport.unpark(t)
通过 AQS 间接使用 ReentrantLockSemaphoreCountDownLatch AQS.parkAndCheckInterrupt()AQS.unparkSuccessor()

设计约束:每个 park() 点都必须有对应的 unpark() 路径,否则线程会永久阻塞。这就是为什么 FutureTask 需要维护 waiters 链表(Treiber Stack)——记录所有等待线程的引用,确保 finishCompletion() 能遍历并 unpark() 每一个。

例外情况

  • BlockingQueue 实现(ArrayBlockingQueueLinkedBlockingQueue)使用 Condition.await()/signal(),底层仍是 park/unpark
  • Thread.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
* 等待此线程终止,最多等待 millis 毫秒。超时值为 0 表示永久等待。
*
* ============================================================
* 设计巧妙之处(三个关键角色的分离):
* ============================================================
* 当 th1 调用 th2.join() 时:
* 1. 锁对象:th2(Thread 对象)
* 2. 检查对象:th2(通过 isAlive() 检查)
* 3. 等待线程:th1(调用 wait() 的线程)
*
* 执行流程:
* - th1 获取 th2 对象的 Monitor 锁
* - th1 检查 th2.isAlive()
* - th1 在 th2 对象上调用 wait(),释放锁并挂起
* - th2 执行完毕时,JVM 自动调用 th2.notifyAll()
* - th1 被唤醒,重新检查 th2.isAlive()(协作式逻辑)
*
* ============================================================
* 线程间接力式等待的本质:
* ============================================================
* join() 可以理解为:在目标线程对象上,在一个 isAlive 循环里封装了一段接力式的 wait()。
*
* 所谓"接力式",指的是:
* 1. 调用线程(th1)持有目标线程对象(th2)的锁
* 2. 在 while 循环中不断检查 th2.isAlive()
* 3. 如果还活着,就调用 th2.wait() 释放锁并挂起
* 4. 被唤醒后,重新抢回锁,再次检查 isAlive()
* 5. 重复这个过程,直到 th2 死亡
*
* 这是一种典型的"协作式等待"模式,而非"抢占式等待"。
*
* ============================================================
* 为什么 join() 里的 wait() 不会抛 IllegalMonitorStateException?
* ============================================================
* 核心原因:join() 方法本身就是 synchronized 的!
*
* public final synchronized void join(long millis) { ... }
*
* synchronized 修饰实例方法时:
* - 锁对象 = this(即目标线程对象 th2)
* - 调用 join() 的线程(th1)会先获取 th2 的锁
* - 然后在持有锁的状态下调用 wait(),完全合法
*
* 对比普通代码:
* // 错误示例:会抛 IllegalMonitorStateException
* Thread t = new Thread(...);
* t.start();
* t.wait(); // ❌ 没有持有 t 的锁
*
* // 正确示例:
* Thread t = new Thread(...);
* t.start();
* synchronized(t) {
* t.wait(); // ✅ 持有 t 的锁
* }
*
* // join() 的等价写法:
* Thread t = new Thread(...);
* t.start();
* t.join(); // ✅ join() 内部已经是 synchronized 的
*
* ============================================================
* Thread 对象的特殊性:
* ============================================================
* 1. 普通对象(Object、String 等):
* - 没有内置状态可以自动触发 notify()
* - wait/notify 完全由程序员手动控制
* - 适合作为条件变量
*
* 2. Thread 对象(特殊对象):
* - 有内置状态:线程生命周期(NEW → RUNNABLE → TERMINATED)
* - 状态变化触发通知:线程终止时,JVM 自动调用 notifyAll()
* - 不适合作为条件变量:会出现程序设计之外的 notifyAll()
* - Javadoc 警告:"不建议在 Thread 实例上使用 wait/notify/notifyAll"
*
* ============================================================
* 为什么 JVM 要自动 notifyAll()?
* ============================================================
* - 设计目的:专门为 join() 而设计
* - 常见需求:等待线程结束是非常常见的并发模式
* - 简化编程:无需手动管理通知逻辑
* - 设计哲学:Thread 对象代表执行流,生命周期结束是重要事件
*
* ============================================================
* 协作式编程体现:
* ============================================================
* - OS 调度(抢占式):JVM/OS 决定何时给 th1 CPU 时间片
* - 业务逻辑(协作式):th1 主动检查 isAlive(),决定是否继续等待
* - while 循环的意义:不是"被唤醒就执行",而是"醒来后检查条件"
*
* @param millis 等待的毫秒数
* @throws IllegalArgumentException 如果 millis 为负数
* @throws InterruptedException 如果当前线程被中断
*/
public final synchronized void join(long millis)
throws InterruptedException {
// 记录开始时间,用于计算已等待时长
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

// 无超时版本:永久等待直到线程结束
if (millis == 0) {
// 协作式编程的核心:while 循环主动检查条件
// - isAlive() 检查的是 this(th2)的状态
// - wait(0) 挂起的是调用线程(th1)
// - 使用 while 而非 if,防止伪唤醒(spurious wakeup)
while (isAlive()) {
// 调用线程(th1)在 this(th2)对象上等待
// 等价于:th2.wait(0)
//
// 释放 th2 对象的 Monitor 锁,th1 进入 WAITING 状态
//
// 可能的唤醒原因:
// 1. th2 结束,JVM 自动调用 th2.notifyAll()(Thread 对象的特殊性)
// 2. 伪唤醒(spurious wakeup)
wait(0);

// 被唤醒后,重新检查 isAlive()(协作式逻辑)
// 如果是伪唤醒且 th2 还活着,继续 wait
// 如果 th2 已死,退出循环
}
} else {
// 带超时版本:等待指定时间或线程结束
while (isAlive()) {
// 计算剩余等待时间
long delay = millis - now;

// 超时检查:如果已经等待了足够长的时间,退出循环
if (delay <= 0) {
break;
}

// 调用线程(th1)在 this(th2)对象上等待 delay 毫秒
// 等价于:th2.wait(delay)
//
// 可能的唤醒原因:
// 1. th2 结束,JVM 自动调用 th2.notifyAll()
// 2. 超时时间到
// 3. 伪唤醒
wait(delay);

// 更新已等待时长
now = System.currentTimeMillis() - base;

// 循环继续,重新检查 isAlive() 和剩余时间(协作式逻辑)
}
}

// 退出方法时,释放 th2 对象的 Monitor 锁
// th1 继续执行后续代码
}
  1. 线程的 join 相当于当前线程在另一个会死亡的线程对象上等待,在 while 循环里无限 wait,在超时或者该线程死亡的时候从 wait 里解脱出来。
  2. 每个 thread 对象的内置状态变成死亡的时候,JVM 会主动调用这个对象的 notifyAll,这和任意条件对象的 wait 和 notifyAll 由程序员自己控制是不一样的。

关于线程池、异步编程、CompletableFuture 等高级并发工具:join 主要用于简单的线程间等待,但对于复杂的并发任务编排,建议使用线程池和 CompletableFuture 等更强大的工具。详细内容请参阅《Java 线程池笔记》

虚拟线程对管程模型的影响(JDK 21+)

JDK 21 引入的虚拟线程(Virtual Threads)对传统管程模型带来了新的考量。

Pinned 问题

虚拟线程在 synchronized 块内调用阻塞方法(如 wait()I/O 操作)时,会被钉住(pinned),无法让出载体线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
传统平台线程:
┌─────────────────────────────────────────────────────────────────────┐
│ synchronized (lock) { │
lock.wait(); // 线程阻塞,但 OS 线程也被占用 │
│ } // 无所谓,一个平台线程 = 一个 OS 线程 │
└─────────────────────────────────────────────────────────────────────┘

虚拟线程(synchronized 块内阻塞):
┌─────────────────────────────────────────────────────────────────────┐
│ synchronized (lock) { │
lock.wait(); // 虚拟线程被钉住,载体线程无法释放! │
│ } // 载体线程被占用,无法执行其他虚拟线程 │
└─────────────────────────────────────────────────────────────────────┘

虚拟线程(ReentrantLock 块内阻塞):
┌─────────────────────────────────────────────────────────────────────┐
lock.lock(); │
try { │
│ condition.await(); // 虚拟线程挂起,载体线程可以释放! │
│ } finally { lock.unlock(); } // 载体线程可执行其他虚拟线程 │
└─────────────────────────────────────────────────────────────────────┘

JDK 21+ 的实践建议

场景 推荐方案 原因
虚拟线程中需要等待/通知 ReentrantLock + Condition 避免 pinned 问题
虚拟线程中的简单互斥 synchronized(临界区短时) 可接受,但需确保无阻塞操作
高并发 I/O 场景 虚拟线程 + ReentrantLock 最大化载体线程利用率

关键结论:虚拟线程环境下,管程的互斥部分(synchronized)可能成为性能瓶颈。这是 JDK 21 推荐在高并发场景使用 ReentrantLock 的原因之一。

虚拟线程与操作系统层面的约束:虚拟线程在 synchronized 块内阻塞会被 Pinned,因为 synchronized 的实现依赖 OS mutex,无法让出载体线程。这再次印证了本文开篇的原则——高级抽象要基于底层系统,不能超出操作系统允许的范围。

关于 JMM、volatile、内存模型等内容,请参阅《JVM 的内存模型与线程》。关于 JUC、锁等内容,请参阅《线程安全与锁优化》