本文思维导图总览:

下载思维导图源文件
写在前面的话
并发编程最早的实践都在操作系统里。高层语言的并发模型都要基于底层系统对硬件抽象和并发的设计来设计和实现,不能超出操作系统允许的范围。所谓的高级抽象总体上是简化对 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 采用 Signal-and-Continue 语义,通知者继续持有锁,被唤醒线程重新竞争锁时条件可能已被改变)。
-
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(); │ buffer[in] = item; count++; in = (in+1) % N; │ │ buffer.notifyAll(); │ } │ │ │ │ get(): │ │ synchronized(buffer) { │ │ while (count == 0) buffer.wait(); │ item = buffer[out]; count--; out = (out+1) % N; │ │ buffer.notifyAll(); │ 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 有以下原因:
- 实现简单:不需要在 signal 后立即切换线程上下文
- 与 OS 调度器兼容:被唤醒线程进入入口队列,由 OS 调度器统一管理
- 对新到达线程更公平:被唤醒线程重新竞争,新到达的线程不必等待正在被唤醒的线程优先执行(但对等待者而言,可能被新线程抢走锁)
- 避免优先级反转:高优先级线程 signal 后继续执行,不会被低优先级的等待者阻塞
代价:开发者必须在 while 循环中重新检查条件,因为被唤醒后条件可能已被其他线程改变。
Mesa 模型
Java 采用 Mesa 模型:
- 互斥(Mutual Exclusion):通过锁机制保证同一时刻只有一个线程能进入管程内部执行。
- 同步(Synchronization):利用条件变量(Condition Variable)实现线程间的等待与唤醒。
- Signal and Continue:
当线程发出通知(signal/notify)时,它继续持有锁并运行,而被唤醒的线程仅仅是进入就绪队列,并不立即抢占 CPU。
由于线程被唤醒后不一定立即执行,当它重新获得锁时,环境条件可能已发生变化,因此必须在一个 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 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
- Entry = 入口:这是线程想要进入管程(Monitor)的入口排队区
- 还没进入:线程确实还没有进入管程内部(没有拿到锁)
- 准备进入:但它们已经在管程的"门口"排队了
对比 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 = 去洗手间的顾客(暂时离开座位,回来要重新排队)
关键理解
-
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 |
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 |
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) → Owner
ReentrantLock: Condition Queue → Sync Queue (WAITING) → Owner
2. Entry Set(锁竞争)的超时行为
synchronized 不支持超时,不会有 TIMED_WAITING
ReentrantLock.lock() 不支持超时,不会有 TIMED_WAITING
tryLock(timeout) 会进入 AQS Sync Queue(调用 doAcquireNanos → addWaiter → parkNanos),线程状态为 TIMED_WAITING
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(),这并非硬性约束——POSIX pthread 的入口函数签名 void* (*)(void*) 其实有返回值——而是 JVM 的设计选择:直接映射到 OS 线程 API 的最简模型,避免引入跨线程的返回值传递机制。后来 Java 5 引入的 Callable<V> 和 Future<V> 通过包装模式(FutureTask 包含 Callable)在更高层次绕过了这一限制。
Java Thread 类的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Thread implements Runnable { private Runnable target; public Thread(Runnable target) { this.target = target; } @Override public void run() { if (target != null) { target.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
| JVM_ENTRY(void, JVM_StartThread(JNIEnv* jni, jobject jthread)) JVMWrapper("JVM_StartThread"); JavaThread* native_thread = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)); if (native_thread->is_being_ext_suspended()) { native_thread->set_terminated_before_start(true); } if (native_thread != NULL) { if (!native_thread->has_been_started()) { os::create_thread(native_thread, thr_type, stack_size); native_thread->set_state(INITIALIZED); os::start_thread(native_thread); } } JVM_END
bool os::create_thread(Thread* thread, ThreadType thr_type) { pthread_attr_t attr; pthread_attr_init(&attr); size_t stack_size = ...; pthread_attr_setstacksize(&attr, stack_size); pthread_t tid; int ret = pthread_create(&tid, &attr, thread_native_entry, thread); if (ret == 0) { thread->set_thread_id(tid); return true; } return false; }
static void* thread_native_entry(Thread* thread) { thread->set_state(RUNNABLE); thread->run(); return NULL; }
class JavaThread: public Thread { private: oop _threadObj; OSThread* _osthread; volatile JavaThreadState _state; public: void run() { this->thread_main_inner(); } void thread_main_inner() { if (has_java_lang_thread()) { JavaCalls::call_virtual( &result, klass, method, threadObj, CHECK ); } } };
static void call_run_method(JNIEnv* env, jobject jthread) { jclass threadClass = env->FindClass("java/lang/Thread"); jmethodID runMethod = env->GetMethodID(threadClass, "run", "()V"); 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 三位一体设计

线程启动时序

这一调用链清晰展示了 run() 无返回值的设计根源:JVM 选择直接映射到 POSIX pthread 的最简模型,不使用入口函数的返回值。这是"高级抽象要基于底层系统"的又一例证。
Java 线程状态
synchronized vs ReentrantLock 的等待-通知队列模型对比

关键差异: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 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;
public void doWait() { synchronized (lock) { System.out.println("Thread-A: Acquired lock, checking condition...");
while (!conditionSatisfied) { try { System.out.println("Thread-A: Condition false, calling wait()..."); lock.wait();
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."); } }
public void doNotify() { synchronized (lock) { System.out.println("Thread-B: Acquired lock, changing condition..."); conditionSatisfied = true; lock.notify(); System.out.println("Thread-B: Notified, but STILL holding lock."); try { Thread.sleep(1000); } catch (InterruptedException e) {} } } }
|
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 { private final ReentrantLock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private boolean conditionSatisfied = false;
public void doAwait() { lock.lock(); try {
System.out.println("Thread-A: Acquired lock, checking condition...");
while (!conditionSatisfied) { try { System.out.println("Thread-A: Condition false, calling await()..."); condition.await();
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 { lock.unlock(); } }
public void doSignal() { lock.lock(); try { System.out.println("Thread-B: Acquired lock, changing condition..."); conditionSatisfied = true; condition.signal(); System.out.println("Thread-B: Signaled, but STILL holding lock."); try { Thread.sleep(1000); } catch (InterruptedException e) {} } finally { System.out.println("Thread-B: Releasing lock..."); lock.unlock(); } } }
|
线程状态转换全景图
前文的 java-thread-state.png 展示了六种 Java 线程状态及其转换关系,下面的 状态图再次呈现,随后详细解释每种状态的含义和触发条件。

线程状态详解
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 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 { queue.put(1); 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"); 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(); while (t2.isAlive()) { System.out.println(t2.getState()); } }
|
对这个程序进行 thread dump,可以看出 ReentrantLock 就是依赖于 park 导致的 waiting:


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

所以 waiting 可能是在条件变量上等待,也可能是在 synchronizer 本身上等待,不可一概而论。
按照 jvisualvm 的分类方法,线程还可以分为:
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 可能同时发生。
线程间方法的设计哲学

- 通常:
- 静态方法 = “我要操作当前线程”(self-operation)。static 相当于 per thread,一个好记的例子是通常 ThreadLocal 设置为 static 的,这样每个线程可以分到一个它的实例,而不是每个线程在每个对象里分到它的实例。
- 实例方法 = “我要操作指定线程”(cross-thread operation)。
- 这背后的逻辑是:
- 每个线程操作自己是比较安全的,static 可以默认在不指定对象的情况下操作自己。
- 而跨线程操作其他线程是比较危险的,因为其他线程的生死如果不是自然发展和结束的,很可能导致锁不释放,条件变量不正确设置,通知没有正确发出。这也就意味着系统可能死锁。
- 主动控制线程何时开始是安全的。
- 主动控制线程何时结束是危险的,因为你不能替它释放资源——这是禁止使用 stop、suspend 等 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 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
| class JavaThread : public Thread { volatile bool _interrupted;
public: void interrupt() { _interrupted = true; if (is_blocked_on_park()) { unpark(); } if (is_blocked_on_monitor()) { notify_monitor(); } if (is_blocked_on_sleep()) { wakeup_sleep(); } }
bool is_interrupted(bool clear_interrupted) { bool old = _interrupted; if (clear_interrupted) { _interrupted = false; } return old; } };
|
注意:_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() { this._interrupted = true; if (is_blocked_on_park()) { unpark(); } if (is_blocked_on_monitor()) { notify_monitor(); } if (is_blocked_on_sleep()) { wakeup_sleep(); } }
|
关键差异点:
unpark() 不是唯一唤醒方式
| 阻塞方式 |
唤醒机制 |
是否抛异常 |
标志位处理 |
LockSupport.park() |
Unsafe.unpark() |
❌ 不抛异常 |
✅ 保留标志 |
Object.wait() |
ObjectMonitor 通知 |
✅ 抛 InterruptedException |
❌ 清除标志 |
Thread.sleep() |
OS 信号唤醒 |
✅ 抛 InterruptedException |
❌ 清除标志 |
synchronized 等待 |
无法唤醒 |
- |
✅ 保留标志 |
unpark() 的特殊语义
LockSupport.unpark() 有一个重要特性:
1 2 3 4
|
LockSupport.unpark(thread); LockSupport.park();
|
这就是为什么 AQS 在 acquireQueued() 中必须用 Thread.interrupted()(清除版本):
1 2 3 4
| private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
|
synchronized 的特殊处理
这是最关键的区别:
1 2 3 4 5 6
| synchronized (lock) { }
|
原因:JVM 的 Monitor 实现基于操作系统的 mutex(如 Linux 的 pthread_mutex),而 OS mutex 不支持可中断的锁获取。这是"高级抽象要基于底层系统"的边界——Java 无法实现操作系统层面不支持的语义。
而 ReentrantLock.lockInterruptibly() 可以响应中断:
1 2 3 4 5 6
| try { reentrantLock.lockInterruptibly(); } catch (InterruptedException e) { }
|
总结:
Thread.interrupt() = 设置中断标志位 + 根据阻塞方式选择唤醒机制(unpark/OS信号/无法唤醒)
这也是为什么"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 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 的机制不同:

三个 interrupt 相关方法的精确语义
Java 提供了三个与中断相关的方法,它们的行为差异是大量 bug 的根源:
| 方法 |
类型 |
行为 |
是否清除标志 |
典型用途 |
interrupt() |
实例方法 |
设置目标线程的中断标志为 true;若目标线程正在阻塞,则唤醒它 |
— |
跨线程发送中断信号 |
isInterrupted() |
实例方法 |
返回目标线程的中断状态 |
否 |
非破坏性地查询中断状态 |
Thread.interrupted() |
静态方法 |
返回当前线程的中断状态 |
是 |
检查并消费中断信号 |
1 2 3 4 5 6 7 8 9 10 11 12
| Thread targetThread = new Thread(() -> { targetThread.isInterrupted(); targetThread.isInterrupted(); Thread.interrupted(); Thread.interrupted(); });
|
Thread.interrupted() 的清除行为是设计上的双刃剑:它实现了"检查并消费"的语义——一次中断信号只能被消费一次。这在单层代码中是合理的,但在嵌套调用中会导致"中断标志泄露"问题(后文详述)。
阻塞方法对 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 27 28 29 30 31 32 33 34 35
|
try { Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
while (!Thread.currentThread().isInterrupted()) { doWork(); }
LockSupport.park();
if (Thread.interrupted()) { interrupted = true; }
synchronized (lock) { if (Thread.currentThread().isInterrupted()) { } }
|
[PATTERN] 中断响应的三种模式:
| 模式 |
代表方法 |
行为 |
标志位处理 |
| 抛异常 + 清除 |
wait(), sleep(), join() |
立即抛出 InterruptedException |
自动清除,需手动恢复 |
| 静默返回 + 保留 |
LockSupport.park() |
方法返回,不抛异常 |
保留标志,由调用者处理 |
| 完全忽略 |
synchronized 等待 |
无法响应,继续等待 |
保留标志,获取锁后才能检查 |
中断恢复:为什么必须 Thread.currentThread().interrupt()
当 InterruptedException 被捕获后,中断标志已被清除。如果当前代码只是"路过"(比如中间层、包装层),不是中断的最终处理者,就必须恢复中断标志,否则上层调用者将永远无法感知到中断的发生:

标准的中断处理模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public void doWork() throws InterruptedException { Thread.sleep(1000); }
public void doWork() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Task interrupted", e); } }
public void doWork() { try { Thread.sleep(1000); } catch (InterruptedException e) { } }
|
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
|
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
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()) interrupted = true; } }
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
|

为什么 AQS 要用 Thread.interrupted()(清除版本)而不是 isInterrupted()?
因为 LockSupport.park() 有一个关键特性:如果中断标志为 true,park() 会立即返回而不阻塞。如果不清除标志,线程将陷入"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
| 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)) { 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(); else if (interruptMode == REINTERRUPT) selfInterrupt(); }
|
interrupt 与线程状态转换的关系
interrupt 可以触发多种线程状态转换,但其影响取决于线程当前所处的状态。完整的线程状态转换图见前文线程状态转换全景图,此处重点分析 interrupt 对各状态的影响:
interrupt 对不同状态线程的影响:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ┌─────────────────────────────────────────────────────────────────────┐ │ interrupt() 对不同状态的影响 │ ├─────────────────────────────────────────────────────────────────────┤ │ 当前状态 │ interrupt() 行为 │ 状态变化 │ ├───────────────────┼─────────────────────────┼───────────────────────┤ │ RUNNABLE │ 仅设置标志位 │ RUNNABLE → RUNNABLE │ │ BLOCKED │ 仅设置标志位,无法唤醒 │ BLOCKED → BLOCKED │ │ │ (必须等获取锁后检查) │ │ │ WAITING(park) │ park()返回,保留标志 │ WAITING → RUNNABLE │ │ WAITING(wait) │ 抛IE,清除标志 │ WAITING → RUNNABLE │ │ 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 (lock) { }
try { reentrantLock.lockInterruptibly(); } 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);
|
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) { log.error("Error", e); } }
while (!Thread.currentThread().isInterrupted()) { badLibraryMethod(); }
|
缺陷三:中断标志泄露
这是最隐蔽的问题。当子任务操作了中断标志(特别是使用 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
| class Worker implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { try { Task task = getNextTask(); task.run(); } catch (InterruptedException e) { break; } } } }
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 ├── 子任务 1:DownloadVideo │ ├── 子子任务 1.1:DownloadSegment(0) │ ├── 子子任务 1.2:DownloadSegment(1) │ └── 子子任务 1.3:DownloadSegment(2) ├── 子任务 2:ExtractAudio └── 子任务 3:MergeFiles
|
当用户点击"取消"时,中断信号应该如何传播?使用 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); File audio = extractAudio(video); merge(video, audio); } catch (InterruptedException e) { 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) {} } } }
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
| 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) { 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
| 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
| 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(); } } } }
|
决策树
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 的上层封装:
- 拒绝策略处理的是"提交阶段"的过载
- 中断机制处理的是"执行阶段"的取消
- 两者共同构成线程池的流量控制和生命周期管理
最佳实践总结
- 不要依赖强制中断来停止任务,任务应该支持检查中断标志并优雅退出
- 优先使用 cancel(false),让正在执行的任务自然完成
- 合理选择拒绝策略,根据业务对任务丢失的容忍度选择
- 始终为周期任务捕获异常,避免任务静默停止
这些实践与前面 interrupt 最佳实践章节的原则一致,体现了从底层机制到上层应用的一致性设计思想。
interrupt 的历史演进
核心教训:
- 隐式状态是万恶之源:
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() 只消费一次。其语义类似于二值信号量(binary semaphore)——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/unpark
Thread.sleep() 不使用 park,是独立的 native 实现
synchronized 块的阻塞由 JVM 监视器实现,通过 Parker 最终调用 OS 原语(详见《线程安全与锁优化》)
wait
这个方法是对 object 用的。
从 wait 中醒来会有伪唤醒的 case,所以醒来的时候一定要先检查唤醒条件是否已经得到满足。伪唤醒的根本原因在于操作系统层面的条件变量实现(如 Linux 的 pthread_cond_wait 基于 futex)不保证每次唤醒都对应一次 signal/notify,详见本文 Mesa 模型 一节的分析
join
Thread.join() 执行流程

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
|
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()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
|
- 线程的 join 相当于当前线程在另一个会死亡的线程对象上等待,类似于把这个线程当作一个condition,由死亡 signal:在 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 传统平台线程: ┌─────────────────────────────────────────────────────────────────────┐ │ synchronized (lock) { │ │ lock.wait(); │ } └─────────────────────────────────────────────────────────────────────┘
虚拟线程(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、锁等内容,请参阅《线程安全与锁优化》。