⬅️ 03-volatile深度解析 | ➡️ 05-CAS与原子类
1. 是什么:管程(Monitor)模型(⭐⭐)
1.1 语义
synchronized 提供 互斥性 + 可见性 + 有序性:
- 互斥:同一时刻只有一个线程持有监视器锁
- 可见 :
unlockhb 后续同锁的lock→ 锁内写对后续同锁内读可见 - 有序:临界区内的操作不会被重排到临界区外
1.2 三种用法
java
// 1. 实例方法 → 锁 this
synchronized void method() { ... }
// 2. 静态方法 → 锁 Class 对象
static synchronized void method() { ... }
// 3. 代码块 → 锁任意对象
synchronized (lockObj) { ... }
1.3 字节码视角
arduino
monitorenter ← 获取锁
// 临界区
monitorexit ← 释放锁
monitorexit ← 异常路径也保证释放(编译器自动插入)
2. 为什么需要 synchronized(⭐⭐)
volatile 只能保证可见性 ,不能保证互斥 和复合操作的原子性:
java
volatile int count = 0;
count++; // ❌ 不安全
synchronized (this) {
count++; // ✅ 安全:读-改-写在临界区内原子执行
}
3. 底层实现:对象头、锁升级与 CPU 指令(⭐⭐⭐)
3.1 对象头与 Mark Word
Java 对象的前 8 字节(64 位 JVM)是 Mark Word,存储锁状态、hashCode、GC 年龄:
css
┌──────────────────────────────────────┬──────┬───┐
│ Mark Word (62 bits) │ age │ 状态│
├──────────────────────────────────────┼──────┼───┤
│ unused:25 | hashCode:31 | age:4 | 0 │ 01 │无锁│
│ threadId:54 | epoch:2 | age:4 | 1 │ 01 │偏向│
│ Lock Record 指针:62 │ 00 │轻量│
│ ObjectMonitor 指针:62 │ 10 │重量│
│ (GC) │ 11 │ GC │
└──────────────────────────────────────┴──────┴───┘
3.2 锁升级路径
flowchart LR
A["无锁"] --> B["偏向锁
同线程零开销重入"] B -->|"其他线程竞争"| C["轻量级锁
CAS 自旋"] C -->|"自旋失败/竞争激烈"| D["重量级锁
OS mutex / futex"] style A fill:#e8f5e9 style B fill:#fff3e0 style C fill:#fce4ec style D fill:#f44336,color:#fff
同线程零开销重入"] B -->|"其他线程竞争"| C["轻量级锁
CAS 自旋"] C -->|"自旋失败/竞争激烈"| D["重量级锁
OS mutex / futex"] style A fill:#e8f5e9 style B fill:#fff3e0 style C fill:#fce4ec style D fill:#f44336,color:#fff
注意 :升级是单向的(偏向→轻量→重量),不会降级(重量→偏向)。JDK 15+ 默认关闭偏向锁。
3.3 偏向锁指令级
asm
; 快速路径:检查 Mark Word 中的线程 ID
mov rax, QWORD PTR [rdi] ; 读 Mark Word
and rax, biased_lock_mask ; 提取偏向标志
cmp rax, current_thread_id ; 是否偏向当前线程?
je already_biased ; ✅ 同线程重入:无原子操作,~1 ns
; ❌ 否则 → 偏向撤销(可能触发 safepoint STW)
撤销触发点:
- 另一个线程尝试获取
- 批量重偏向/撤销阈值(默认 20/40)
- 调用
hashCode()(某些实现)
3.4 轻量级锁指令级
asm
; 栈上分配 Lock Record,CAS 替换 Mark Word
lea rdx, [rsp+lock_record_offset] ; Lock Record 地址
mov rax, QWORD PTR [rdi] ; 读当前 Mark Word
mov QWORD PTR [rdx], rax ; 保存旧 Mark Word
lock cmpxchg QWORD PTR [rdi], rdx ; CAS:Mark Word → Lock Record 指针
jne slow_path ; 失败 → 自旋或膨胀到重量级
3.5 重量级锁与 OS 路径
asm
; 膨胀(inflate)后的 ObjectMonitor::enter
; 快速 CAS → 失败 → 自旋 → 失败 →
; 最终调用 OS 挂起:
syscall ; futex(addr, FUTEX_WAIT, ...)
; ↑ 用户态 → 内核态上下文切换
3.6 成本量化
| 锁状态 | 指令数 | 上下文切换 | 延迟 |
|---|---|---|---|
| 偏向(同线程) | 3-5 | 无 | ~1 ns |
| 轻量(CAS 成功) | 10-20 | 无 | ~10-30 ns |
| 轻量(自旋) | 百-千 | 无 | ~100-1000 ns |
| 重量(futex) | N/A | 用户↔内核 | ~5000-15000 ns |
4. JIT 锁优化(⭐⭐)
4.1 锁消除
java
void method() {
Object lock = new Object(); // 逃逸分析:lock 不逃逸
synchronized (lock) { // JIT 直接删除锁!
// ...
}
}
4.2 锁粗化
java
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // JIT 合并为一次加锁
list.add(i);
}
}
// → 优化为一次 synchronized + 循环
4.3 自适应自旋
JVM 根据上次自旋成功率动态调整自旋次数------上次成功则多转几圈,上次失败则直接挂起。
5. wait/notify 与 Condition(⭐⭐)
5.1 wait/notify 三件套
java
synchronized (lock) {
while (!condition) { // 必须用 while(防伪唤醒)
lock.wait(); // 释放锁 + 挂起
}
// 条件满足,处理
}
synchronized (lock) {
condition = true;
lock.notify(); // 唤醒一个等待线程(不释放锁)
}
5.2 与 Condition 对比
| 维度 | wait/notify |
Condition |
|---|---|---|
| 绑定 | 任何对象的监视器 | 绑定 Lock |
| 条件队列 | 1 个 wait-set | 多个 Condition |
| 可中断等待 | 不支持 | awaitUninterruptibly() / await() |
| 限时等待 | wait(timeout) |
await(time, unit) + awaitNanos |
| 公平性 | 无保证 | 随 Lock 策略 |
6. 面试题精选(⭐⭐ ~ ⭐⭐⭐)
Q1 🟦 字节:「对象头 64 位里有哪些状态?偏向锁撤销触发点?」(⭐⭐⭐)
答:
Mark Word 编码 5 种状态(无锁/偏向/轻量/重量/GC),由最低 2-3 位区分。
偏向撤销触发点:
- 另一个线程尝试偏向同一对象
- 批量重偏向阈值(默认 20 次)/ 批量撤销阈值(40 次)
- 调用
hashCode()(部分路径会撤销偏向,因为 hashCode 需要占据 Mark Word 空间)
追问 :为什么 wait() 让重偏向失效?------ wait 导致 monitor inflate(膨胀为重量级),之后偏向位被清除。
Q2 🟧 阿里:「线上 CPU 不高但 RT 抖动,怀疑偏向锁,怎么验证与止血?」(⭐⭐⭐)
答:
- 取证 :火焰图 + JFR
jdk.JavaMonitorEnter事件 /-XX:+PrintBiasedLocking(旧版本) - 原理 :多线程轻微竞争 → 偏向撤销进 safepoint → P99 尖刺(均值 CPU 不高)
- 止血 :灰度
-XX:-UseBiasedLocking,监控 P99 变化 - 案例:某网关 JDK8,撤销峰值 12k/s → P99 从 18ms 飙到 120ms;关闭偏向后 P99 回 22ms,CPU P95 仅 +6%
追问:JDK 17 默认策略?------ 偏向锁默认关闭(JEP 374),面试要确认目标 JDK 版本。
Q3 🟧 阿里:「static synchronized 和实例 synchronized 的区别?」(⭐⭐)
答:
static synchronized:锁的是Class对象- 实例
synchronized:锁的是this实例 - 两者不互斥(不同监视器对象)
- 死锁风险:如果同时使用,注意锁排序统一
Q4 🟡 美团:「用 synchronized 还是 ReentrantLock?」(⭐⭐)
答:
| 维度 | synchronized |
ReentrantLock |
|---|---|---|
| 条件队列 | 1 个 | 多个 Condition |
| 可中断获取 | ❌ | ✅ lockInterruptibly |
| 限时获取 | ❌ | ✅ tryLock(timeout) |
| 公平锁 | ❌ | ✅ 构造参数 |
| 释放保证 | 字节码保证 | 必须 finally |
| 性能 | JDK6+ 接近 | 略好(极高竞争下) |
原则 :简单同步用 synchronized;需要高级特性(中断/限时/多条件/公平)用 ReentrantLock。
Q5 🟢 腾讯:「synchronized ("LOCK") 有什么问题?」(⭐)
答 :字符串字面量 intern → 全 JVM 共享同一对象 → 可能与不相关的代码意外互斥 → 性能或死锁。永远用 private final Object lock = new Object() 做监视器。
7. 快问快答(⭐)
- synchronized 可重入吗?------ 可。
- synchronized 释放锁后其他线程能立即看到修改吗?------ 遵循 hb:同锁的 unlock hb 后续 lock。
- Thread.sleep 释放锁吗?------ 不释放。
- wait 释放锁吗?------ 释放(被唤醒后重新获取)。
- notify 和 notifyAll 区别?------ notify 唤一个(随机),notifyAll 唤全部但仍串行获取锁。
- 锁消除什么时候触发?------ 逃逸分析证明锁对象不逃逸。
- synchronized 和 volatile 能替换吗?------ 不能互换:volatile 无互斥,synchronized 更重。
- 字节码保证释放是怎么做到的?------ 编译器在异常路径插入额外
monitorexit。 - Thread.holdsLock(obj) 作用?------ 断言当前线程持有 obj 的锁。
- 嵌套 synchronized 有死锁风险吗?------ 有,如果锁顺序不一致。
8. 本章小结
flowchart TD
A["语义
互斥+可见+有序"] --> B["对象头
Mark Word 编码"] B --> C["锁升级
偏向→轻量→重量"] C --> D["CPU 指令
CAS / futex"] D --> E["JIT 优化
消除/粗化/自旋"] E --> F["面试落地
选型 + 事故排查"]
互斥+可见+有序"] --> B["对象头
Mark Word 编码"] B --> C["锁升级
偏向→轻量→重量"] C --> D["CPU 指令
CAS / futex"] D --> E["JIT 优化
消除/粗化/自旋"] E --> F["面试落地
选型 + 事故排查"]
下一步:synchronized 的互斥靠 锁 ,那有没有不用锁 的方式实现原子性?→ 05-CAS与原子类