juc.png
juc.xmind

写在前面的话

并发编程最早的实践都在操作系统里。

管程

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

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 是为了告诉你,不要依赖它们的唤醒顺序。

模型映射

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 -

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 不可见

Java 线程状态

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();
}
}
}

线程状态列举

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 可能同时发生。

线程间方法的设计哲学

  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 的温和写入,但不必然强制操作形成对比。

特别的切换方法

LockSupport.park

也就是线程挂起。

condition 的 await 底层调用的是 LockSupport.park。这个方法的参数是一个用作 monitor 的对象,会被设置到 Object 的特定 Offset 上。

park 只能带来 waiting。所以 sync 和 conditionObject 其实都让 thread waiting ,只不过代表 thread 的 node 处在的队列不一样而已-线程 node 在 sync queue 和 condition queue 都是 waiting。

wait

这个方法是对 object 用的。

从 wait 中醒来会有伪唤醒的 case,所以醒来的时候一定要先检查唤醒条件是否已经得到满足。原理见《为什么条件锁会产生虚假唤醒现象(spurious wakeup)?》

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
/**
* 等待此线程终止,最多等待 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()(协作式逻辑)
*
* ============================================================
* 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 由程序员自己控制是不一样的。

关于 JMM、volatile、JUC、锁等内容,请参阅《线程安全与锁优化》