# 第 30 章 JMM、volatile 与 Synchronized


> 同一段并发代码在 x86 上正确、在 ARM 上出错，说明"正确性"不取决于机器而取决于规约，本章讲这份规约与实现它的两把工具。


## 学习目标

读完本章，你应当能够：

1. 辨析 JMM 规约的三件事（原子性、可见性、有序性），说出重排序的三种来源与 `as-if-serial` 的边界；
2. 逐条解释 `happens-before` 八条规则的含义与用途，说清它与"墙上时间先后"的本质区别；
3. 推出四种内存屏障的约束关系，说明 `volatile` 读写各插什么屏障、x86 上为什么只剩 `StoreLoad`；
4. 完整推导双重检查锁定（double-checked locking, DCL）单例为什么必须加 `volatile`，并写出两种正确写法；
5. 画出 64 位 Mark Word 在四种锁状态下的位域图，说清锁升级每一级的触发条件与成本；
6. 推导 `ThreadLocal` 内存泄漏的引用链，解释弱引用为何救不了 value、为什么线程池下必须 `remove`；
7. 用"不可变 → 无状态 → 同步"的判断流程回答"这段代码要不要加锁"。

### 前置知识检查

| 你需要知道 | 在哪一节补齐 |
|---|---|
| 线程与进程的本质区别 | 见第 4 章；本章只讲内存可见性 |
| 原子指令与 `CAS` 的硬件实现 | 见第 6 章；本章 30.4、30.11 节只讲 Java 侧语义封装 |
| 多核缓存一致性与 `MESI` | 见第 1、6 章；本章只用它解释"写为何要立即对其他核可见" |
| JVM 运行时数据区与栈帧结构 | 见第 32 章 32.2 节；本章只用到"锁记录在栈上"这一事实 |

## 30.1 为什么需要 JMM：一份"允许看到什么"的规约

### 30.1.1 同一段代码为什么在不同平台行为不同

并发程序的行为由三层共同决定：**编译器优化的重排 → CPU 乱序执行的发射顺序 → 多核缓存与写缓冲的传播顺序**。这三层在 x86 与 ARM 上的宽松程度完全不同，于是同一份字节码跑出不同结果。

| 平台 | 内存模型 | 允许的重排序 | 屏障指令 |
|---|---|---|---|
| x86-64 | 强序 TSO（total store order） | 只允许 Store 之后的 Load 提前（StoreLoad） | `mfence`、`lock` 前缀指令 |
| ARMv8 | 弱序（weak memory model） | 四类重排基本都允许 | `dmb ish` / `dmb ishst` / `dmb ishld` |
| JMM | 抽象规约 | **不承诺任何具体重排**，只承诺 `happens-before` | 由 JVM 翻译为目标平台的真实屏障 |

> [!NOTE]
> x86 的强序来自两条硬件事实：写缓冲（Store Buffer）让写不立即进 cache，失效队列（invalidate queue）让 Load 可能越过未完成的 Store，所以 `StoreLoad` 是唯一必须显式拦的一类。ARM 连 Store-Store 都不保证，同样的 `volatile` 写要翻译成更重的屏障。

### 30.1.2 JMM 到底规约了什么

JMM（Java Memory Model）定义在 [JLS 第 17 章](https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html)，它**不描述硬件怎么执行**，而是规定**一次合法执行允许读到哪些值**：输入是程序中所有动作（读、写、锁、`volatile` 读写、线程启停）；规则是① 每个读看到的值必须来自某个"允许的"写，而不是"最新的"写，② 筛选"允许的写"的唯一条件就是 `happens-before`，③ 禁止因果循环（out-of-thin-air）；输出是一个或多个"合法执行"，读到的值可以不止一种组合。**正确性的举证责任在程序员一侧**：只要程序里有一个不受 `happens-before` 约束的数据竞争（data race），JMM 就允许它读到任意"陈旧但合法"的值，甚至允许出现真实硬件上不会出现的组合。

> [!CAUTION]
> 答"JMM 保证了硬件不会重排序"是方向性错误。JMM 只保证**遵守 `happens-before` 的程序得到确定的可见性结果**；它不禁止硬件重排，而是反过来要求 JVM 插入足够的屏障来"补上"被硬件吃掉的那部分顺序。

## 30.2 三条性质与重排序

### 30.2.1 原子性：哪些操作天然原子

| 操作 | 原子性 | 依据与坑 |
|---|---|---|
| 除 `long`/`double` 外的 32 位及以下基本类型、引用类型的读写 | 原子 | JLS 17.6；但引用读写各算一次，两次之间无顺序保证 |
| `long`/`double` 非 `volatile` 读写 | **规范不保证** | JLS 允许拆成两次 32 位访问 |
| `volatile` 的 `long`/`double` | 原子 | JLS 17.7 明确要求 |
| `i++`、`i += 1` | 非原子 | 字节码是 `getstatic` → `iconst_1` → `iadd` → `putstatic` 四步 |
| `synchronized` 块内 | 由锁保证 | 互斥性使临界区整体成为一个原子单位 |

> [!WARNING]
> `long` 的"半写"在 64 位 HotSpot 上**实际不会发生**（64 位对齐读写天然原子），但 JLS 允许它发生。面试正确答法是："规范层面不保证，HotSpot 64 位实现上实际原子，所以不能依赖规范不保证的东西"——这句分层表述比背"long 不安全"更值钱。

### 30.2.2 重排序的三种类型与 `as-if-serial`

**可见性（visibility）** 指一个线程的写何时、以何种顺序对另一个线程的读可见，物理障碍是写缓冲与失效队列。**有序性（ordering）** 指动作允许以什么顺序被观察到，只在跨线程视角下有讨论意义。

| 类型 | 发生在哪 | 典型例子 | 拦截手段 |
|---|---|---|---|
| 编译器重排序 | 源码→字节码 / 字节码→机器码 | JIT 调换两个无依赖的写；循环不变量读提到循环外 | `volatile`、`synchronized`、`final`、`VarHandle` 的 acquire/release |
| 指令级并行重排序 | CPU 乱序执行引擎 | 无依赖指令同拍发射；写缓冲让 Store 延后可见 | 硬件屏障（`mfence`/`dmb`） |
| 内存系统重排序 | cache 与写缓冲 | 核 A 已写 `x` 但核 B 仍读到旧值，先写的 `y` 反而先可见 | 缓存一致性协议 + 屏障 |

**`as-if-serial` 语义**：无论怎么重排，**单线程**程序的执行结果必须与严格顺序执行一致。它给了编译器和 CPU 极大自由度，代价是这份保证只在单线程内成立。于是并发 bug 的特征就很清楚：**单线程下永远正确，只在多线程交错时出错**，这正是它难以复现的原因。经典反例是 `a = 0; flag = false;` 时线程 A 写 `a = 1; flag = true;`、线程 B 轮询 `while (!flag) {}` 后读 `a`：单线程视角 A 的两行看似有序、`r` 应为 1，但 A 与 B 之间没有 `happens-before`，`flag` 的写可能先于 `a` 的写可见，于是 B 读到 `flag = true` 而 `a = 0`——把 `flag` 声明为 `volatile` 即可修复（见 30.3 节）。

## 30.3 `happens-before` 八条规则

`happens-before` 是 JMM 的核心关系，记作 `hb(A, B)`，含义是 **A 的结果对 B 可见，且 A 在 B 之前被观察到**。它不是"时间上先发生"，而是"允许被看到的顺序"的许可。

| # | 规则 | 含义 | 用途 |
|---|---|---|---|
| 1 | 程序顺序规则 | 同一线程内，前面的动作 `hb` 后面的动作 | 单线程内无需同步即可看到自己的写 |
| 2 | 监视器锁规则 | 对同一把锁的 `unlock` `hb` 后续的 `lock` | `synchronized` 同时给出互斥与可见性 |
| 3 | `volatile` 变量规则 | 对 `volatile` 字段的写 `hb` 后续对它的读 | 建立"一写多读"的轻量发布/订阅 |
| 4 | 传递性（transitivity） | `hb(A,B)` 且 `hb(B,C)` ⟹ `hb(A,C)` | 让"锁→volatile→锁"这类链条串起来 |
| 5 | `Thread.start()` 规则 | `start()` 之前的动作 `hb` 新线程的第一条动作 | 新线程能看到构造它时写入的字段 |
| 6 | `Thread.join()` 规则 | 被 `join` 线程的所有动作 `hb` `join()` 成功返回 | `join` 返回后可安全读取子线程结果 |
| 7 | 线程中断规则 | `interrupt()` 的调用 `hb` 被中断线程检测到中断 | 保证 `isInterrupted()` 能看到标志 |
| 8 | 对象终结规则 | 对象构造完成 `hb` 其 `finalize()` 开始 | 终结器能看到已初始化对象的状态（机制已废弃） |

> [!IMPORTANT]
> `happens-before` 与"时间先后"的区别必须一句话说清：**`hb` 是可见性保证，不是时间顺序保证**。两个动作在物理时间上完全可能乱序执行；`hb` 只保证"若 A `hb` B，则 B 一定看得到 A 的效果"。反过来，"A 在墙钟上早于 B"**不产生**任何 `hb`，这正是 30.2.2 那个例子的病根。
>
> 规则 3 与规则 4 联动是最常被追问的组合：T1 写 `x = 42` 再写 `v = true`（`v` 为 `volatile`），T2 读到 `v == true` 再写 `b = x`，则由规则 1、3、4 得 `x = 42 hb b = x`——**用 `volatile` 发布一个对象，其内部所有普通字段也跟着安全发布**，这是"不可变对象 + 一次性发布"模式的全部依据。

## 30.4 内存屏障的四种类型与插入策略

### 30.4.1 四种屏障的约束关系

| 屏障 | 约束 | 直觉 | 相对成本 |
|---|---|---|---|
| `LoadLoad` | 禁止前后两个 Load 重排 | 先读完前一个再读后一个 | 低（x86 通常无需指令） |
| `StoreStore` | 禁止前后两个 Store 重排 | 前一个写必须先全局可见 | 低（x86 通常无需指令） |
| `LoadStore` | 禁止 Load 与它后面的 Store 重排 | 读不能跑到写后面去 | 低 |
| `StoreLoad` | 禁止 Store 与它后面的 Load 重排 | 写必须全系统可见后才能继续读 | **最贵**：排空写缓冲，x86 上是 `mfence` 或 `lock` 前缀 |

`StoreLoad` 最贵的原因是**它要求"全局可见"这一强条件**：其他三类只约束本核流水线与本核视角的顺序，`StoreLoad` 却要等到写缓冲清空、所有核都承认这次写，后续 Load 才能发射。一次 `mfence` 在 x86 上约 20~30 个时钟周期，接近其余三类之和。

### 30.4.2 `volatile` 写读的屏障插入位置

| 操作 | 前置屏障 | 后置屏障 | 一句话记忆 |
|---|---|---|---|
| `volatile` 写 | `StoreStore` | `StoreLoad` | 写之前把前面的写都推出去，写之后不许抢跑 |
| `volatile` 读 | `LoadLoad` | `LoadStore` | 读之后不许把后面的读写提前 |
| 普通读写 | 无 | 无 | 无任何约束 |

```
  volatile 写 = 前 StoreStore + 后 StoreLoad
   [StoreStore] ← 保证 v 之前的普通写（如 x = 42）都已进入可见路径
      store v = true        ← volatile 写本身
   [StoreLoad]  ← 禁止 v 之后的读提前，排空写缓冲，代价最高
  ⟹ A 线程 x=42; v=true 与 B 线程 if(v) use(x) 之间形成 hb(A, B)
```

### 30.4.3 x86 上为什么只剩 `StoreLoad`

x86 的 TSO 本身保证 `LoadLoad`、`StoreStore`、`LoadStore` 三类顺序，只有 `StoreLoad` 会重排，因此 HotSpot 的 x86 后端把 `volatile` 写编译成一条带 `lock` 前缀的指令（如 `lock addl $0x0, (%rsp)`，效果等价于 `mfence` 且更短），而 `volatile` 读**不需要额外指令**——只需从内存重新加载而不能复用寄存器。

> [!NOTE]
> 这解释了一个常见疑问："为什么 `volatile` 读看起来没有开销？"x86 上确实几乎没有；ARM 上读要插 `dmb ishld`、写要插 `dmb ish`，两边代价差一个量级。JDK 9 之后更推荐用 `VarHandle` 的 `getAcquire`/`setRelease`（见 30.11.2 节）表达"我只需要单向屏障"，它比 `volatile` 少一半屏障。

## 30.5 `volatile` 的两条语义与不保证原子性

`volatile` 的全部语义只有两条。**语义一，可见性**：对 `volatile` 字段的写会立即对其他核可见，其他线程的后续读必然读到本次写及其之前的所有普通写。**语义二，禁止重排序**：由 30.4.2 的屏障组合实现，它让 `volatile` 写之前的所有普通写与这个 `volatile` 写构成一个整体发布单元。

> [!CAUTION]
> `volatile` **不保证原子性**。`i++` 是"读—改—写"三步，`volatile` 只保证每一步的读与写各自原子且有序，**不保证三步合起来原子**。两个线程各做 1 万次 `i++`，结果大概率小于 2 万。需要复合原子性只有三条路：`synchronized`、`AtomicInteger`（`CAS` 循环）、`LongAdder`（分段累加）。

### 30.5.1 DCL 单例为什么必须加 `volatile`

对象创建在字节码层面展开为三步：**① 分配内存并得到引用 ② 执行构造器初始化字段 ③ 把引用赋给静态字段**。其中 **② 与 ③ 之间没有数据依赖**，编译器与 CPU 都可以把它们重排为 `① → ③ → ②`。第 ① 步分配出来的内存是"全零"的，所以重排后其他线程看到的引用**非 `null` 但字段全是零值**。

```
  没有 volatile 时可能出现的交错（② 与 ③ 重排）
  ── 线程 A（构造） ──────    ── 线程 B（首次取用） ──────
  ① instance = 分配内存（零值）
  ③ INSTANCE = instance（提前赋值！）
                              ④ 检查 INSTANCE != null → 通过
                              ⑤ 使用 obj.field → 读到 0 / null ★
  ② 执行构造器，把 field 赋真值
  ⟹ 根因：B 的④⑤与 A 的①②③之间没有 happens-before
  ⟹ 若 INSTANCE 是 volatile：③ 是 volatile 写、④ 是 volatile 读，
     由规则 3 + 4 得①②都 hb ⑤，B 必然看到已完成初始化的对象
```

```java
public final class SafeSingleton {
    // volatile 让"引用赋值"与"构造器内的字段写"之间建立 happens-before，
    // 否则第二次检查可能看到引用非 null、字段却还是零值的半初始化对象
    private static volatile SafeSingleton instance;
    public static SafeSingleton getInstance() {
        if (instance == null) {                 // 第一次检查：常态下不进临界区，省掉每次同步开销
            synchronized (SafeSingleton.class) {
                if (instance == null) {         // 第二次检查：并发进入时只有第一个线程该构造
                    instance = new SafeSingleton();
                }
            }
        }
        return instance;
    }
}
```

注意 `value` 是 `final`：`final` 域本身有安全发布语义（见 30.9 节），但它保护的是"字段写完先于构造器返回"，与 `volatile` 保护的"引用赋值不被提前暴露"不是同一件事，所以两者都要有。

> [!TIP]
> 更优写法是**静态内部类持有（initialization-on-demand holder）**：私有的静态内部类 `Holder` 里放一个 `static final HolderSingleton INSTANCE = new HolderSingleton();`，`getInstance()` 直接返回它。它利用 JVM 类初始化的同步机制（`<clinit>` 由 JVM 保证只执行一次、其他线程阻塞等待），既天然延迟加载（类只在首次主动使用时初始化）又不需要 `volatile` 与 `synchronized`。单元素 `enum` 更简洁，还额外获得**反序列化安全**与**反射攻击防护**，Effective Java 首推它；代价是不能继承其他类。

## 30.6 `synchronized` 的字节码、锁对象与 Mark Word

### 30.6.1 字节码层面发生了什么

```text
  字节码骨架：astore_1（锁对象存入局部变量槽，供异常路径复用）→ monitorenter →
  临界区 → monitorexit → goto；异常处理器入口再 aload_1 → monitorexit → athrow
  Exception table: from 4 to 13, target 16, type any ← 覆盖所有异常
```

**为什么编译器要复制一遍 `monitorexit`**：JVM 要求 `monitorenter` 与 `monitorexit` 数量严格配对，且释放必须发生在**任何**退出路径上。编译器于是用 `Exception table` 的 `any` 类型兜住全部异常并补上释放。这也意味着 **`synchronized` 不会因异常泄漏锁**，而手工 `Lock` 必须写在 `finally` 里。**同步方法**没有 `monitorenter`，而是在方法 `access_flags` 上置 `ACC_SYNCHRONIZED`，由 JVM 在方法调用与返回时隐式加解锁。

### 30.6.2 锁的是哪个对象

| 写法 | 锁对象 | 需要注意 |
|---|---|---|
| `synchronized (obj) { }` | `obj` 这个实例 | 必须是所有线程共享的同一引用；`synchronized (new Object())` 毫无意义 |
| `synchronized (this)` / 实例同步方法 | 当前实例 | 不同实例之间不互斥；单例上等价于全局锁 |
| `static` 同步方法 | 该类的 `Class` 对象 | 与实例方法锁**不是同一把锁**，两者可以并发 |
| `synchronized (SomeConst)` | 那个常量对象 | 锁 `String` 字面量、`Integer` 缓存对象极危险：会被无关代码共享 |

> [!WARNING]
> 常见错误是"同步方法锁的是方法"。锁的永远是**对象**，方法只决定了隐含对象是 `this` 还是 `Class`。另一个高频坑是**锁对象被替换**：`synchronized (lock)` 中若 `lock` 是非 `final` 字段且在临界区内被重新赋值，其他线程就锁到了新对象，互斥直接失效。

### 30.6.3 Mark Word 位域图与锁状态

HotSpot 把锁状态与运行时元数据塞进对象头的 Mark Word（64 位 JVM 下 8 字节）。**它的内容随锁状态整体改变含义**，这是理解锁升级的前提。

```
  64 位 Mark Word 的四种状态（bit 从右往左编号，右侧为低位）
  无锁（Normal，biased_lock=0）:
    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 = 01
  偏向锁（Biased）:
    | thread_id:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 = 01
  轻量级锁: | 指向栈中 Lock Record 的指针:62 | lock:2 = 00
  重量级锁: | 指向 ObjectMonitor 的指针:62  | lock:2 = 10
  GC 标记:  | 空（转发指针或占位）            | lock:2 = 11
  对象头布局: | Mark Word (8 字节) | Klass Pointer (8 字节) |
    ← 开 -XX:+UseCompressedOops 时 Klass 压到 4 字节；故普通对象至少 16 字节
```

`lock` 两位是状态机的选择子（`01` 无锁/偏向、`00` 轻量级、`10` 重量级、`11` GC 标记）；`biased_lock` 位区分同为 `01` 的"无锁"与"偏向"。偏向锁状态下 **54 位线程 ID 占了原 `hashCode` 的位置**，这就是"调用 `hashCode()`（或 `System.identityHashCode`）会**撤销偏向锁**"的原因——两者抢同一块位域。

## 30.7 锁升级路径

```
  无锁 ─首次同步、偏向未关闭→ 偏向锁 ─第二个线程竞争→ 轻量级锁 ─CAS 自旋失败→ 重量级锁
  成本: 零      一次 CAS 置线程 ID     一次 CAS + 自旋        内核互斥量 + park/unpark
  备注: —       撤销要 STW 到安全点    失败即膨胀、不可逆      一次上下文切换约 1~5 μs
```

| 状态 | Mark Word 内容 | 获取成本 | 触发/退出条件 |
|---|---|---|---|
| 无锁 | `hash` + `age` + `01` | 0 | 初始状态 |
| 偏向锁 | 线程 ID + `epoch` + `01` | 一次 `CAS` 写入自身线程 ID | 只有**一个**线程反复进入；第二个线程尝试获取即触发**撤销** |
| 轻量级锁 | 栈上 Lock Record 指针 + `00` | 一次 `CAS` + 若干次自旋 | 有竞争但临界区极短；自旋失败或竞争加剧即**膨胀** |
| 重量级锁 | `ObjectMonitor` 指针 + `10` | 内核互斥量，可能阻塞线程 | 竞争激烈或临界区长；解锁后唤醒等待者 |

### 30.7.1 偏向锁、轻量级锁与自旋

**偏向锁**由 JDK 6 引入、`-XX:+UseBiasedLocking` 控制，它赌的是**大多数锁在整个生命周期内只被一个线程持有**，于是把 `CAS` 成本也省掉：第一次获取用一次 `CAS` 把线程 ID 写进 Mark Word，此后该线程再进入只需比对 ID，**连 `CAS` 都不用**。代价在撤销：**撤销偏向锁必须把线程停在安全点（safepoint）**，检查它是否还在临界区内，再改写 Mark Word——这是一次 STW，虽然通常只有微秒级，但"为省一次 `CAS` 而引入 STW"在偏向锁命中率很低（大量使用 `java.util.concurrent`）的代码里就不划算了。

> [!NOTE]
> JDK 15 起 `-XX:+UseBiasedLocking` **默认关闭**（JEP 374），并在后续版本移除了该实现。所以答"锁升级一定经过偏向锁"并不准确：**默认配置下现在是"无锁 → 轻量级 → 重量级"三态**，偏向锁属于"历史上存在、需要知道为什么被移除"的知识点。

**轻量级锁**的核心动作是"把 Mark Word 搬走"：① 在当前线程的栈帧里分配 Lock Record，把 Mark Word 当前值复制进它的 `displaced_header` 字段；② 用 `CAS` 把对象头的 Mark Word 改成"指向这个 Lock Record 的指针"。成功即加锁成功、锁状态位变 `00`；失败说明有竞争，先判断是否自己重入（是则 `displaced_header` 置 `null`、重入计数 +1），否则自旋，自旋用尽即膨胀为重量级锁。

```
  栈帧（线程私有）                   堆上的对象
  ┌────────────────────┐        ┌───────────────────────┐
  │ Lock Record        │        │ Mark Word             │
  │  displaced_header ─┼─备份─▶ │  (原 hash/age/01)     │
  └─────────▲──────────┘        │ Mark Word 现在＝ ─────┼─┐
            └───────────────────┴───────────────────────┘ │
               指向 Lock Record 的指针 ◀──────────────────┘
```
**解锁**就是把 `displaced_header` 反向 `CAS` 回对象头；若 `CAS` 失败，说明等待期间已膨胀为重量级锁，必须走重量级解锁路径（唤醒 `_EntryList` 中的线程）。
**自旋（spin）的判据很简单：临界区执行时间 < 一次线程挂起/唤醒的成本时，忙等比阻塞划算。** 数量级对比是：一次上下文切换约 1~5 μs，而一次空循环极便宜，所以只要临界区能在微秒级内结束，自旋就有正收益。`-XX:PreBlockSpin`（JDK 6 之前）是**固定**自旋次数、默认 10，问题是固定次数无法适应不同临界区；JDK 6 起改为**自适应自旋（adaptive spinning）**：由 JVM 根据**同一个锁上最近一次自旋的成功率**动态决定——上次成功则允许更久，上次失败则直接跳过自旋去阻塞。于是 `-XX:PreBlockSpin` 实际失效（参数仍在，通常不生效）。
**锁粗化（lock coarsening）** 指 JIT 发现一串相邻操作反复对同一对象加解锁（典型是循环体内的 `synchronized`，或 `StringBuffer.append` 连续调用）时，把锁扩展到整个操作序列外，判断依据是"加解锁本身的开销大于合并后增加的临界区持有时间"。**锁消除（lock elision）** 基于**逃逸分析（escape analysis）**：若 JIT 证明锁对象**不会逃逸出当前线程**（如方法内 `new` 出的 `StringBuffer` 且未被返回或存入共享字段），该锁不可能是共享资源，直接删除。这与栈上分配、标量替换（见第 32 章 32.2 节）是同一份逃逸分析结果的三个用途。

## 30.8 `ObjectMonitor` 与 `wait`/`notify` 的队列迁移

```
  HotSpot 的 ObjectMonitor 结构（c++ 层，对应重量级锁）
  ┌──────────────┐  _owner     = 当前持有锁的线程（重入计数 _recursions）
  │ ObjectMonitor│  _cxq       = 竞争队列：新来的竞争者 CAS 入队（单向链表）
  │              │  _EntryList = 入口队列：从 _cxq 搬运过来的"等锁"线程
  │              │  _WaitSet   = 等待集：调用过 wait() 的线程（双向循环链表）
  └──────────────┘
  wait()     ⟹ 释放锁（_owner 置空、_recursions 归零），把自身包成 ObjectWaiter
                挂入 _WaitSet，线程状态变 WAITING
  notify()   ⟹ 从 _WaitSet 取一个 ObjectWaiter，按策略迁到 _EntryList 或 _cxq；
                ★ 被唤醒者不立刻持锁，而是变 BLOCKED 重新抢锁
  notifyAll()⟹ _WaitSet 全部迁移；持锁线程退出 ⟹ 从 _EntryList 选一个（默认非公平）
```

> [!IMPORTANT]
> **`wait` 必须在 `synchronized` 块内**，否则抛 `IllegalMonitorStateException`。两条理由缺一不可：(1) `wait` 操作的是对象监视器，必须先成为 `_owner` 才有权操作它的 `_WaitSet`；(2) 更本质的是**为了消除丢失唤醒（lost wakeup）的竞态**——判断条件（如队列非空）与进入等待必须原子，否则"判断为假 → 尚未 `wait` 时通知先到达 → 通知丢失 → 永久等待"。
>
> **`wait` 会释放锁**：它要停下来等别人改变条件，握着锁别人就进不了临界区、也就无法 `notify`（典型死锁）。这是 `wait` 与 `sleep` 最关键的差别：`sleep`/`park` 都不释放锁。另外被唤醒后线程变 `BLOCKED` 重新抢锁，中间条件可能已被改回不满足，且 JVM 允许**虚假唤醒（spurious wakeup）**，所以规范写法是 `while (!condition) { lock.wait(); }` 而非 `if`。

## 30.9 `final` 域的安全发布语义

JLS 17.5 规定：**对象构造器执行完毕时，如果没有对 `this` 的引用逃逸，那么 `final` 字段对其他线程可见，且看到的值至少是构造器里写入的值。** 实现手段是在 `final` 字段的写之后、构造器返回之前插入 **`StoreStore` 屏障**，禁止"把对象引用赋给共享变量"这一写越过 `final` 字段的写。

```java
public class FinalPublish {
    final int x;              // 构造器内一次性写入，之后不可改
    int y;                    // 非 final：没有安全发布保证
    static FinalPublish obj;  // 共享引用，不加 volatile，靠 final 语义兜住 x
    public FinalPublish() {
        x = 1;
        y = 2;   // 编译器在构造器返回前插入 StoreStore 屏障：保证 x=1 先于对象引用对外可见
    }
}
```
**对不可变对象的意义**：若一个类的字段全为 `final` 且指向的对象本身不可变（如 `String`），那么构造完成后的对象**天然线程安全、无需任何同步即可安全发布**。这就是不可变对象（immutable object）成为并发设计第一选择的根本原因——它把可见性问题在**构造阶段一次性解决**，之后不再需要锁。

> [!WARNING]
> `final` 语义有前提，被追问时必须说全：(1) **构造器内不能泄漏 `this`**（注册到监听器、存入静态集合、传给会回读字段的方法），否则其他线程能在字段赋值完成前拿到引用；(2) **`final` 只保护它自己那个字段的写**，构造器里对**非 `final`** 字段的写没有安全发布保证（上面例子的 `y = 2` 就不受保护）；(3) 反序列化产生的对象不走构造器，`final` 语义不覆盖。

## 30.10 `ThreadLocal` 的完整机制与内存泄漏根因

### 30.10.1 前提：`ThreadLocalMap` 是 `Thread` 的字段

**理解 `ThreadLocal` 的一切都要从这句话开始：`ThreadLocalMap` 挂在 `Thread` 对象上，而不是挂在 `ThreadLocal` 对象上。**

```
  引用链（面试必须能画出来的那张图）
  Thread ─(强引用 threadLocals 字段)→ ThreadLocalMap → Entry[] table
                       └─ Entry ◀─ extends WeakReference<ThreadLocal<?>>
                            ├─ referent ─(弱引用)→ ThreadLocal 对象 ★ key
                            └─ value ───(强引用)→ 业务对象      ★ value
  ① 外部对 ThreadLocal 的强引用消失 ⟹ 只剩 Entry 的弱引用指向它
  ② 下一次 GC ⟹ key 被回收，Entry 的 key 变成 null，但 value 仍被 Entry 强引用
  ③ Entry 被 ThreadLocalMap 强引用，ThreadLocalMap 被 Thread 强引用
  ④ 线程存活期间（★线程池的核心线程几乎永不退出）这部分 value 永远无法回收
  ⟹ "key 被回收但 value 无法回收"，泄漏量 = 全部失效 Entry 的 value 之和
```

> [!IMPORTANT]
> **为什么 key 用弱引用、value 用强引用？** key 用弱引用是为了给"忘记 `remove`"提供兜底：当外部再也无法访问该 `ThreadLocal` 时自动断开 key。value 若也用弱引用，则任何一次 GC 都能清掉正常业务值，语义直接崩掉。所以弱引用只加在 key 上，**代价就是 value 泄漏**，这个代价由 `get`/`set`/`remove` 的探测式清理来偿还。

### 30.10.2 探测式清理与 `remove` 的必要性

`ThreadLocalMap` 用**开放定址法**（线性探测）解决冲突，key 为 `null` 的 Entry 即"陈旧条目（stale entry）"。三个入口都会顺手清理：

| 方法 | 是否清理 | 清理策略 |
|---|---|---|
| `get()` | 是 | 探测到 key 相同者返回 value；途中遇到 `key == null` 则调用 `expungeStaleEntry` |
| `set()` | 是 | 写入前做启发式清理：至少扫 `log2(阈值)` 个槽，污染度高则全表扫描 |
| `remove()` | 是 | 定位后**先把 `value` 置 `null`**，再调 `expungeStaleEntry` 断开 Entry |

`expungeStaleEntry` 的关键动作有两步：**把 `value` 置 `null`（真正让对象可回收）**，以及**把该槽之后连续非空段里的有效 Entry 重新散列到正确位置**——开放定址法的探测链断了以后，后面的元素会查找不到，必须搬移。

> [!CAUTION]
> "有 `expungeStaleEntry` 兜底，所以不用显式 `remove`"是最危险的错误结论。清理只在**同一线程后续访问同一个 `ThreadLocalMap`** 时被动触发，且只覆盖探测路径上的局部区域。线程池场景下线程被复用、业务代码却不再碰这个 `ThreadLocal` 时，清理永不发生。**必须 `try { ... } finally { tl.remove(); }`**，尤其在把数据库连接、大对象、`SimpleDateFormat` 这类资源放进 `ThreadLocal` 时。

### 30.10.3 `InheritableThreadLocal` 与 `TransmittableThreadLocal`

| 类型 | 传递方向 | 快照时机 | 失效场景 |
|---|---|---|---|
| `ThreadLocal` | 不传递 | — | 子线程读不到父线程的值 |
| `InheritableThreadLocal` | 父 → 子 | `new Thread()` 时在 `Thread.init` 中复制 | **线程池复用线程**：副本在首次创建时固定，后续提交的任务不会刷新 |
| `TransmittableThreadLocal` | 提交方 → 执行线程 | 任务**提交时**抓快照、**执行前**回放、**执行后**还原 | 需要包裹线程池（`TtlExecutors`）或其增强线程池 |

三者的本质差别是**快照的抓取时机**：`InheritableThreadLocal` 在"线程创建"时抓，而线程池里线程创建与任务提交是解耦的，所以必然失效；TTL 把时机改到"任务提交时"，才对得上线程池的语义。

## 30.11 线程安全的三个层次与 `VarHandle`

### 30.11.1 "这段代码要不要加锁"的判断流程

```
  第 0 问：存在共享可变状态吗？
     └─ 不存在（全用局部变量、参数、不可变对象）⟹ 线程安全，不需任何同步 ★最常被忽略
  第 1 问：能改成不可变（字段全 final、无 setter、内部集合不外泄）吗？
     └─ 能 ⟹ 线程安全，靠 final 的安全发布（见 30.9 节）
  第 2 问：能改成"每线程一份"吗？⟹ 用 ThreadLocal，必须 remove（见 30.10 节）
  第 3 问：能换成并发容器或原子类吗？（单个操作原子 ≠ 复合操作原子，见第 29、31 章）
     └─ 能 ⟹ ConcurrentHashMap / AtomicInteger / LongAdder
  第 4 问：以上都不行 ⟹ 必须同步
     ├─ 只发不收的状态标志、单一变量更新 ⟹ volatile 足够
     └─ "检查后动作"或需保护多个变量的一致性 ⟹ synchronized 或 Lock
  ⟹ 三层次：不可变 > 无状态与局部变量 > 同步；同步是最后手段而非首选
```

判据的核心是**复合操作**：`if (map.get(k) == null) map.put(k, v)` 即使 `map` 是 `ConcurrentHashMap` 也不安全，因为"检查"与"动作"之间不原子，必须用 `putIfAbsent`/`computeIfAbsent` 或加锁。

### 30.11.2 `Unsafe` 与 `VarHandle` 的低层语义

| 方法族 | 语义 | 屏障强度 | 适用 |
|---|---|---|---|
| `compareAndSet` | 原子比较并交换 | 兼具 `volatile` 读写语义（full fence） | 无锁结构、计数器、`AQS` |
| `getAcquire` / `setRelease` | 单向语义：acquire 读、release 写 | release ≈ 前置 `StoreStore`+`LoadStore`；acquire ≈ 后置 `LoadLoad`+`LoadStore` | 发布/订阅，比 `volatile` 少一半屏障 |
| `getOpaque` / `setOpaque` | 不重排，但不保证跨线程可见 | 强于 plain、弱于 acquire/release | 抑制编译器优化（热循环里反复读字段） |
| `getPlain` / `setPlain` | 普通读写 | 无 | 显式表达"这里不需要内存语义" |

JDK 9 起 `Unsafe` 的内存访问方法由 `VarHandle` 取代，后者在编译期做类型检查、对 JIT 更友好，其相对 olatile 的价值在于**可以只声明单向屏障**。

```java
import java.lang.invoke.VarHandle;
public final class ReleasePublish {
    private int data;                                  // 普通字段：靠 release/acquire 配对建立可见性
    // 用标准 API 拿字段句柄，编译期即可校验类型，避免 Unsafe"签名一错就崩"
    private static final VarHandle DATA = MethodHandles.lookup()
            .findVarHandle(ReleasePublish.class, "data", int.class);
    // 发布方：setRelease 只要求"之前的写不能跑到它后面"，比 volatile 写便宜（x86 上省掉 StoreLoad）
    public void publish(int value) {
        DATA.setRelease(this, value);
    }
    // 消费方：getAcquire 只要求"之后的读写不能跑到它前面"，与 setRelease 配对即可安全看到 value
    public int consume() {
        return (int) DATA.getAcquire(this);
    }
}
```

## 本章小结

| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| JMM 的定位 | 它规约"允许看到什么"，不描述硬件怎么执行 | 答"JMM 禁止了硬件重排序" |
| 重排序三类 | 编译器、指令级并行、内存系统各重排一次 | 只答"CPU 乱序执行"一种 |
| `happens-before` | 是可见性保证，不是墙上时间顺序 | 用"代码写在前面"论证可见性 |
| 规则用法 | 规则 3 + 规则 4 是发布对象的常用组合 | 只背八条却不会用它推可见性 |
| 屏障 | 写 `StoreStore`+`StoreLoad`；读 `LoadLoad`+`LoadStore` | 答"读写各插一个屏障" |
| `volatile` 原子性 | 复合操作不原子，`i++` 必须加锁或原子类 | 答"volatile 是轻量级锁" |
| DCL | 必须 `volatile`，否则可能拿到半初始化对象 | 说"不加只是性能差一点" |
| 锁对象 | 同步方法锁 `this` 或 `Class`，二者不互斥 | 答"锁的是方法或代码块" |
| Mark Word | 内容随锁状态整体改变，`hashCode` 与偏向锁抢位域 | 答"对象头固定存哈希值" |
| 轻量级锁 | `CAS` 把 Mark Word 指向栈上 Lock Record | 答"轻量级锁就是不阻塞的锁" |
| `wait` | 必须在 `synchronized` 内、释放锁、用 `while` 包裹 | 答"wait 是让出 CPU 的 sleep" |

## 自测题
**Q1**（辨析）"`volatile` 就是轻量级的 `synchronized`"这句话对吗？请给出判据，并举一个 `volatile` 不够用的最小例子。
<details><summary>答案与解析</summary>
**只有一半对。** 相同点是两者都提供可见性、都禁止重排序。**关键差别只有一条：`volatile` 不提供互斥**，因此不提供原子性。判据是"操作是否单步、能否用一个 `volatile` 写完整表达状态"：能替代的是单写多读的状态标志、DCL 的引用发布、"写值 + 置标志"的发布模式；不能替代的是 `i++`、check-then-act、需保护多字段不变量的场景。
最小反例：`volatile int i` 上两个线程各做 1 万次 `i++`，结果小于 2 万。根因是 `i++` 的字节码为 `getstatic → iconst_1 → iadd → putstatic` 四步，`volatile` 只让 `getstatic`/`putstatic` 各自带屏障，**两个 `putstatic` 之间仍可交错**，于是丢失更新。修法是 `synchronized` 或 `AtomicInteger.incrementAndGet()`（`CAS` 循环重试）。
**面试官会追问**：(1) `volatile` 读在 x86 上几乎没有开销，只需重新从内存加载，写才要带 `lock` 前缀的指令（见 30.4.3 节）。(2) `AtomicInteger` 原子是因为 `CAS` 指令的"比较"与"写入"在硬件上不可分割，且带全屏障语义（见 30.11.2 节）。
</details>
**Q2**（推导）`int data = 0; boolean ready = false;`，线程 A 执行 `data = 42; ready = true;`，线程 B 执行 `if (ready) { println(data); }`。这段代码在 x86 上通常打印 42，在 ARM 上可能打印 0。请指出缺少哪条 `happens-before`，画出可能的重排，并说明加 `volatile` 后屏障如何补上缺口。
<details><summary>答案与解析</summary>
**缺少规则 3（`volatile` 变量规则）**：`ready` 只是普通字段，两线程之间不存在任何 `hb`，交错发生在两层——JIT 可调换 A 中两行无依赖的写；ARM 不保证 Store-Store 顺序，`ready = true` 可能先于 `data = 42` 对其他核可见，B 读到 `ready == true` 时 `data` 仍是 0。x86 因 TSO 保证 Store-Store 顺序所以"通常能对"，这正是"同一段代码在不同平台行为不同"的样本。
**修法**：把 `ready` 声明为 `volatile`。写侧插 `StoreStore`（保证 `data = 42` 先可见）+ `StoreLoad`（禁止写抢跑），读侧插 `LoadLoad`（后续读 `data` 不能提前）。于是由规则 1、3、4 得 `data = 42 hb ready = true hb if(ready)`，输出必然是 42。
**面试官会追问**：只给 `data` 加 `volatile` 不行，关键是**标志位**必须带 `volatile`；两个字段都用 `AtomicInteger` 可以但浪费（原子类方法是 full fence）；更省的做法是发布侧 `setRelease(ready)`、消费侧 `getAcquire(ready)`，比 `volatile` 少一半屏障（见 30.11.2 节）。
</details>
**Q3**（分析）有人写"不加 `volatile` 的 DCL"并辩称"`new` 是一整条指令，别的线程不可能看到半个对象"。请从字节码与屏障两个角度反驳，并说明静态内部类写法为什么天然不需要 `volatile`。
<details><summary>答案与解析</summary>
**反驳一（字节码）**：`new` 在字节码里是三条指令——`new`（分配并零值初始化）、`invokespecial <init>`（执行构造器）、`putstatic`（赋值引用）。**后两条无数据依赖**，JIT 与 CPU 都可把 `putstatic` 提前到 `<init>` 之前，而第 ① 步分配的内存全是零值，所以另一个线程通过首次检查后读到的是 `0`/`null`。**反驳二（屏障）与静态内部类为什么不需要**：不加 `volatile` 时 `putstatic` 前后**没有任何屏障**，缺少 `StoreStore` 意味着构造器内的字段写可能后于引用赋值对其他核可见；加 `volatile` 后前置 `StoreStore` 把构造器内的写压在它前面，读侧 `LoadLoad` 保证第二个检查通过后才允许读字段。静态内部类则依赖 **JVM 的类初始化锁**：`<clinit>` 由 JVM 保证"同一类只执行一次，其他尝试初始化的线程被阻塞到完成"（JLS 12.4.2），本身就是完整的 `hb` 链，且类只在首次主动使用时初始化，故既安全又延迟加载。**面试官会追问**：反射可以调私有构造器，需要额外防护，而 `enum` 单例天然免疫反射与反序列化；若构造器里把 `this` 传出去，就破坏了安全发布的前提，静态内部类也救不了（见 30.9 节）。
</details>
**Q4**（推导）某线程池服务用 `ThreadLocal<byte[]>` 缓存每请求约 1 MB 缓冲，代码只有 `set` 没有 `remove`，数小时后堆占用持续上涨且 `Full GC` 后不下降。请画出引用链，解释弱引用为何没能避免泄漏，并说明为什么显式 `remove` 是唯一解法。
<details><summary>答案与解析</summary>
**引用链**：`Thread`（线程池核心线程，存活期几乎等于进程）→ 强引用 `threadLocals` 字段 → `ThreadLocalMap` → `Entry[]` → `Entry` → **`value`（强引用）→ 那 1 MB 的 `byte[]`**；而 `Entry` 对 `ThreadLocal` 对象本身只有**弱引用**（`Entry extends WeakReference<ThreadLocal<?>>`）。弱引用为什么没救：它只加在 **key** 上，GC 回收 key 后 `Entry` 成为"key 为 `null` 的陈旧条目"，但 `value` 仍被 `Entry` 强引用、`Entry` 仍被 map 强引用、map 仍被那个**不会退出的线程**强引用，所以这 1 MB 不可达但不可回收，`Full GC` 清不掉。
**为什么必须显式 `remove`**：`expungeStaleEntry` 只在**同一线程再次访问该 map** 时被动触发（`get`/`set` 的探测路径或 `set` 的启发式清理）。线程池复用时若该请求类型的 `ThreadLocal` 后续不再被这个线程使用，清理路径永远走不到，泄漏是永久性的。`remove()` 会**先把 `value` 置 `null`** 再清 Entry，是唯一能立即断开强引用的动作。规范写法：`try { tl.set(buf); ... } finally { tl.remove(); }`。
**面试官会追问**：`value` 不能做成弱引用，否则正常业务值会在任何一次 GC 时被清空；map 也不能挂在 `ThreadLocal` 上，否则它就成了所有线程共享的 map，既需要并发控制又丢失"每线程一份副本"的隔离语义。线程池下要跨任务传递上下文，用 `TransmittableThreadLocal` 把快照时机从"线程创建"改到"任务提交"（见 30.10.3 节）。
</details>
**Q5**（设计）一段"检查后更新共享 map"的代码 `if (!map.containsKey(k)) map.put(k, load(k));` 中 `map` 是 `ConcurrentHashMap`，接口响应时间却抖动。请说明它为什么不安全，给出三种修复并比较代价，再说明用 `synchronized` 保护时要注意什么。
<details><summary>答案与解析</summary>
**为什么不安全**：`ConcurrentHashMap` 只保证**单个方法调用**原子。这里"判断存在性"与"写入"是两次独立调用，中间其他线程可以插入写入，于是 `load(k)` 可能被多个线程重复执行。若 `load` 有副作用或代价高，就是实打实的损失；`size()`/`isEmpty()` 之类在并发下也只是估计值，不能当判据。**三种修复**：(1) **`computeIfAbsent(k, this::load)`**：整段由 map 内部按桶加锁，最简洁；代价是**同一个桶上的其他 key 会被阻塞**，`load` 很慢会拖住该桶，且 `load` 内部递归操作同一 map 会抛异常。(2) **`putIfAbsent(k, load(k))`**：语义是"不存在才放"，但参数 `load(k)` **先被求值**，重复加载仍会发生，只在 `load` 便宜时可用。(3) **双检 + 锁**：`V v = map.get(k); if (v == null) { synchronized (lock) { v = map.get(k); if (v == null) { v = load(k); map.put(k, v); } } }`，把重复加载压到最小，代价是引入外部锁、不同 key 之间也会互相阻塞。
**用 `synchronized` 的注意事项与追问**：(a) 锁对象必须 `final` 且所有访问路径共用同一个（不要用 `this`，除非能保证单实例）；(b) **临界区内不要做 I/O 或耗时计算**，`load(k)` 要查库就放到锁外或用"占位 + 异步填充"；(c) `synchronized` 没有超时，若 `load` 可能永久阻塞，应改用 `ReentrantLock.tryLock`。`computeIfAbsent` 的坑是同一线程递归修改同一 map 会抛 `IllegalStateException`，在其内部访问**另一个** map 也可能形成锁顺序环；判断抖动究竟来自锁竞争还是 `load` 本身，必须看 `jstack` 里 `BLOCKED` 线程数与火焰图中 `load` 占比，不要凭猜测改代码。
</details>

## 面试高频问题与陷阱

| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应本章小节 |
|---|---|---|---|
| 既然硬件已有内存模型，Java 为什么还要自己定一套 JMM？它规定了什么？ | JMM 的抽象层次 | 错答"为了禁止硬件重排序"。加分点：JMM 是"允许读到哪些值"的合同，举证责任在程序员；x86 是 TSO、ARM 是弱序，JVM 负责把 `happens-before` 翻译成各自平台的屏障 | 30.1 |
| `happens-before` 的八条规则各是什么？它和时间上的"先发生"是一回事吗？ | 八条规则的记忆与语义 | 错答"就是时间上先发生"，或用"代码写在前面"论证可见性。加分点：它是可见性保证；并把规则 3 + 传递性组合成"用 volatile 发布对象、其内部普通字段也安全发布" | 30.3 |
| `volatile` 到底做了什么？它在 x86 和 ARM 上编译出的指令一样吗？ | 屏障插入策略与平台差异 | 错答"volatile 就是把值写回主存"或"读写各插一个屏障"。加分点：写 = `StoreStore`+`StoreLoad`，读 = `LoadLoad`+`LoadStore`；x86 只剩 `StoreLoad`（`lock` 前缀），读侧零指令 | 30.4 |
| `volatile` 能保证 `i++` 线程安全吗？为什么不能？ | 原子性与互斥的区分 | 错答"能，因为 volatile 保证了可见性"。加分点：`i++` 是 `getstatic → iconst_1 → iadd → putstatic` 四步，`volatile` 只保证每步各自语义，两个 `putstatic` 之间仍可交错（丢失更新） | 30.5 |
| 双重检查锁定单例为什么必须加 `volatile`？不加会怎样？ | 对象创建三步与 2/3 重排 | 错答"不加只是性能差一点"或"`new` 是原子操作不会出问题"。加分点：`new` → `invokespecial <init>` → `putstatic` 三步，后两步无依赖可重排，其他线程可能读到字段为默认值的半初始化对象 | 30.5.1 |
| `synchronized` 锁的到底是什么？修饰实例方法和静态方法锁的是同一把锁吗？ | 锁对象的确定 | 错答"锁的是方法"或"锁的是代码块"。加分点：实例方法锁 `this`、静态方法锁 `Class` 对象，二者不互斥；并指出锁 `String` 字面量或 `Integer` 缓存对象的危险 | 30.6.2 |
| 说说对象头里的 Mark Word，以及锁升级的完整路径与每级成本。 | Mark Word 位域与锁升级状态机 | 错答"锁升级一定经过偏向锁"（JDK 15 起默认禁用并已移除），或说不出 `lock` 两位的取值含义。加分点：画位域图，说明 `hashCode` 与偏向锁线程 ID 抢同一块位域、撤销偏向锁要 STW 到安全点 | 30.6.3、30.7 |
| `ThreadLocal` 会内存泄漏吗？为什么 `Entry` 的 key 用弱引用、value 用强引用？ | 引用链与清理时机 | 错答"用了弱引用所以不会泄漏"或"有 `expungeStaleEntry` 兜底所以不用 remove"。加分点：画出 `Thread → ThreadLocalMap → Entry → value` 强引用链；清理只在同线程再次访问该 map 时发生，线程池下必须 `try/finally remove` | 30.10 |

## 延伸阅读

- [JLS 第 17 章 Threads and Locks](https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html)：17.4 内存模型（`happens-before` 与因果性要求）、17.5 `final` 字段语义、17.6 字的拆分、17.7 `volatile` 的 64 位原子性。
- [JEP 374: Deprecate and Disable Biased Locking](https://openjdk.org/jeps/374)：偏向锁被默认关闭的理由与后续移除计划。
- OpenJDK 源码：`src/hotspot/share/runtime/synchronizer.cpp`（`ObjectMonitor`、`fast_enter`/`slow_enter`、`ObjectWaiter` 在 `_cxq`/`_EntryList`/`_WaitSet` 间的迁移）、`src/hotspot/share/oops/markWord.hpp`（Mark Word 位域与 `lock` 位取值）、`src/hotspot/share/opto/` 下的屏障节点与 `LockCoarsening`、`EliminateLocks` 优化。- `java.lang.invoke.VarHandle` 官方 API 文档（`getAcquire`/`setRelease`/`getOpaque` 的语义与 `compareAndSet` 的屏障强度）、`ThreadLocal`/`InheritableThreadLocal` 文档与 `ThreadLocalMap` 的 `expungeStaleEntry` 实现。- 见第 6 章（硬件层原子指令与内存屏障）、第 29 章（`ConcurrentHashMap` 与并发容器演进）、第 31 章（`AQS`、`ReentrantLock` 与线程池）、第 32 章 32.2 节（逃逸分析、栈上分配与标量替换）。


---

> 作者: [破戒僧](https://onlyblacktea.cn)  
> URL: https://onlyblacktea.cn/book/backend/ch30/  

