synchronized与锁升级
# 31.synchronized与锁升级
# 目录介绍
- 8.1 开篇疑问
- 8.2 synchronized的使用方式
- 8.3 对象头与锁状态
- 8.4 锁升级的完整过程
- 8.5 锁优化技术
- 8.6 synchronized的可重入性原理
- 8.7 wait/notify机制与Monitor的关系
- 8.8 synchronized vs ReentrantLock深度对比
- 8.9 synchronized的版本演进
- 8.10 生产环境中的synchronized最佳实践
- 8.11 总结与核心要点
# 8.1 开篇疑问
疑惑:synchronized 是 Java 最常用的同步关键字,但它真的是"重量级锁"吗?经常听说"偏向锁、轻量级锁、重量级锁",它们到底是什么?锁是怎么存在对象头里的?synchronized 和 ReentrantLock 该怎么选?JDK 15 为什么要废弃偏向锁?
答疑:JDK 6 对 synchronized 做了革命性优化,引入了锁升级机制。在无竞争或轻度竞争下,synchronized 的性能与无锁几乎一样。理解锁升级,就能理解为什么现代 Java 中 synchronized 不再是性能杀手。
# 8.2 synchronized的使用方式
# 8.2.1 三种加锁方式
public class SyncDemo {
private final Object lock = new Object();
// 1. 修饰实例方法——锁是 this 对象
public synchronized void instanceMethod() {
// 等价于 synchronized(this) { ... }
}
// 2. 修饰静态方法——锁是 Class 对象
public static synchronized void staticMethod() {
// 等价于 synchronized(SyncDemo.class) { ... }
}
// 3. 修饰代码块——锁是指定对象
public void blockMethod() {
synchronized (lock) {
// 临界区,只有持有 lock 的线程才能进入
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
最佳实践:优先使用代码块方式,锁对象使用 private final 修饰,避免外部访问导致的意外同步:
// 不推荐:锁 this,外部代码也可能 synchronized(obj) 导致竞争
public synchronized void method() { }
// 推荐:私有锁对象,外部无法干扰
private final Object lock = new Object();
public void method() {
synchronized (lock) { }
}
2
3
4
5
6
7
8
# 8.2.2 底层字节码分析
同步代码块通过 monitorenter 和 monitorexit 字节码指令实现:
public void syncBlock() {
synchronized (lock) {
doSomething();
}
}
2
3
4
5
编译后的字节码:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // 获取 lock 的 monitor
7: aload_0
8: invokevirtual #4 // doSomething()
11: aload_1
12: monitorexit // 正常退出时释放 monitor
13: goto 21
16: astore_2
17: aload_1
18: monitorexit // 异常退出时也释放 monitor(保证不会死锁)
19: aload_2
20: athrow
21: return
Exception table:
from to target type
7 13 16 any
16 19 16 any
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
关键点:
- 编译器会生成两条 monitorexit:一条用于正常退出,一条用于异常退出
- 异常表确保即使发生异常,monitor 也一定会被释放
- 同步方法则在方法的
access_flags中设置ACC_SYNCHRONIZED标志,JVM 在方法调用时自动获取/释放 monitor
# 8.2.3 Monitor监视器的本质
每个 Java 对象都关联一个 Monitor(监视器/管程),这是操作系统的概念。Monitor 是实现互斥与协作的基本构件。
ObjectMonitor 结构(HotSpot C++ 实现):
┌─────────────────────────────────────┐
│ _header : Mark Word 的备份 │
│ _object : 关联的 Java 对象 │
│ _owner : 当前持有锁的线程 │
│ _recursions : 重入计数 │
│ _count : monitor 的进入次数 │
│ _WaitSet : 调用 wait() 的线程集 │ ← wait/notify 协作
│ _cxq : 最近到达的竞争线程队列 │ ← 单向链表
│ _EntryList : 等待获取锁的线程集 │ ← 双向链表
│ _SpinFreq : 自旋频率 │
│ _SpinClock : 自旋周期 │
└─────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
Monitor 的工作流程:
线程请求锁
→ _owner == null?
→ 是:设为 _owner,获取成功
→ 否:进入 _cxq 或 _EntryList 等待
→ 被唤醒后重新竞争 _owner
线程调用 wait()
→ 释放 _owner,进入 _WaitSet 等待
→ 被 notify() 唤醒后,移到 _EntryList 重新竞争
线程释放锁
→ 清除 _owner
→ 唤醒 _EntryList 或 _cxq 中的线程
2
3
4
5
6
7
8
9
10
11
12
13
# 8.3 对象头与锁状态
# 8.3.1 Mark Word结构详解
锁信息存储在对象头的 Mark Word 中。64 位 JVM 的 Mark Word 布局:
┌──────────────────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├─────────────────────────────────────────────┬──────┬─────┬──────────┤
│ 具体内容(取决于锁状态) │ 分代 │偏向 │ 锁标志 │
│ │ 年龄 │标志 │ 位 │
│ │(4bit)│(1bit)│(2bit) │
├─────────────────────────────────────────────┼──────┼─────┼──────────┤
│ 无锁: unused(25) | hashcode(31) │ age │ 0 │ 01 │
│ 偏向锁: thread(54) | epoch(2) │ age │ 1 │ 01 │
│ 轻量级锁: Lock Record 指针 (62) │ │ │ 00 │
│ 重量级锁: ObjectMonitor 指针 (62) │ │ │ 10 │
│ GC 标记: │ │ │ 11 │
└─────────────────────────────────────────────┴──────┴─────┴──────────┘
2
3
4
5
6
7
8
9
10
11
12
13
关键细节:
- 无锁状态存储了
identity hashCode(调用Object.hashCode()后生成) - 一旦进入偏向锁,hashcode 字段被 threadId 替换。如果已经计算过 hashCode 的对象无法进入偏向锁
- GC 分代年龄用 4 bit 表示,最大值 15,这就是
MaxTenuringThreshold默认为 15 的原因
# 8.3.2 四种锁状态概览
| 锁状态 | 标志位 | 适用场景 | 性能 | Mark Word 存储 |
|---|---|---|---|---|
| 无锁 | 01(偏向=0) | 没有同步需求 | 最快 | hashCode + 分代年龄 |
| 偏向锁 | 01(偏向=1) | 只有一个线程访问 | 接近无锁 | 线程ID + epoch |
| 轻量级锁 | 00 | 两个线程交替访问 | CAS 操作 | Lock Record 指针 |
| 重量级锁 | 10 | 多线程竞争激烈 | 内核态切换 | Monitor 指针 |
核心原则:锁只能升级不能降级。无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
# 8.3.3 使用JOL工具观察对象头
Java Object Layout(JOL)工具可以直接查看对象的内存布局:
<!-- Maven 依赖 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
2
3
4
5
6
import org.openjdk.jol.info.ClassLayout;
public class MarkWordDemo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
// 查看无锁状态
System.out.println("无锁状态:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 加 synchronized 后查看
synchronized (obj) {
System.out.println("加锁状态:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 计算 hashCode 后查看
System.out.println("hashCode: " + Integer.toHexString(obj.hashCode()));
System.out.println("计算hashCode后:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输出示例:
无锁状态:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (偏向锁可偏向状态)
4 4 (object header) 00 00 00 00
8 4 (object header) ...
加锁状态:
0 4 (object header) 05 f0 01 03 (偏向锁,存储线程ID)
2
3
4
5
6
7
8
# 8.4 锁升级的完整过程
# 8.4.1 偏向锁原理
设计动机:HotSpot 团队研究发现,大部分锁在整个生命周期中只被一个线程持有。偏向锁让这个线程以后每次进入同步块时无需做任何同步操作。
获取过程:
线程A 首次进入 synchronized 块
│
↓ 检查 Mark Word 是否为可偏向状态(偏向标志=1,线程ID=0)
│
↓ CAS 将线程A 的 ID 写入 Mark Word
│
↓ 成功 → 获得偏向锁
│
线程A 后续进入同一个 synchronized 块
│
↓ 只需检查 Mark Word 中的线程 ID == 线程A 的 ID?
│
↓ 是 → 直接进入,无需任何 CAS 或同步操作
│ 这就是"偏向"的含义——偏向于第一个获取它的线程
2
3
4
5
6
7
8
9
10
11
12
13
14
性能数据:偏向锁在无竞争场景下,进入同步块的开销约为 1~2 纳秒,几乎与无锁相同。
# 8.4.2 偏向锁的撤销过程
当另一个线程尝试获取偏向锁时,触发撤销:
线程B 尝试获取偏向锁
│
↓ 发现 Mark Word 中的线程 ID ≠ 线程B
│
↓ 需要撤销偏向锁(必须等到安全点 Safe Point)
│
↓ 暂停持有偏向锁的线程A(STW)
│
↓ 检查线程A 的状态:
│
├─ 线程A 已退出同步块 → 撤销偏向,Mark Word 恢复为无锁
│ → 线程B 通过 CAS 竞争轻量级锁
│
└─ 线程A 仍在同步块中 → 升级为轻量级锁
→ 在线程A 的栈帧中创建 Lock Record
→ Mark Word 指向 Lock Record
→ 线程B 也创建 Lock Record 并竞争
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关键问题:偏向锁的撤销需要 STW(暂停所有线程到安全点),这在高并发场景下开销不可忽视。
# 8.4.3 偏向锁的批量重偏向与批量撤销
JVM 对偏向锁的撤销做了优化:
批量重偏向(Bulk Rebias):
- 当同一个类的对象的偏向锁被频繁撤销(默认阈值 20 次),JVM 认为该类的锁"确实会被多个线程使用"
- 此后新创建的该类对象的偏向锁会偏向最近的线程(通过 epoch 机制实现)
批量撤销(Bulk Revoke):
- 当同一个类的撤销次数达到更高阈值(默认 40 次),JVM 彻底放弃该类的偏向锁
- 该类所有新旧对象都不再使用偏向锁
# 相关 JVM 参数
-XX:BiasedLockingBulkRebiasThreshold=20 # 批量重偏向阈值
-XX:BiasedLockingBulkRevokeThreshold=40 # 批量撤销阈值
-XX:BiasedLockingDecayTime=25000 # 衰退时间(ms)
2
3
4
# 8.4.4 轻量级锁原理
设计动机:多个线程在不同时段交替获取锁(没有真正的竞争),用 CAS 代替重量级的互斥量。
获取过程详解:
线程A 尝试获取轻量级锁
│
↓ 步骤1:在线程A 的栈帧中创建 Lock Record
│ Lock Record 包含:
│ - Displaced Mark Word(Mark Word 的备份)
│ - owner(指向锁对象的指针)
│
↓ 步骤2:CAS 尝试将对象 Mark Word 替换为指向 Lock Record 的指针
│
↓ 步骤3:
│
├─ CAS 成功 → 获得轻量级锁,Mark Word 锁标志位变为 00
│
└─ CAS 失败 → 检查 Mark Word 是否指向当前线程的栈帧
│
├─ 是 → 重入(在栈帧中再创建一个 Lock Record,Displaced Mark Word = null)
│
└─ 否 → 有竞争!开始自旋
│
├─ 自旋成功(对方释放了锁)→ 获得轻量级锁
│
└─ 自旋失败(超过阈值)→ 膨胀为重量级锁
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
线程栈帧: 对象头:
┌──────────────┐ ┌──────────────┐
│ Lock Record │←─────────│ Mark Word │
│ ┌──────────┐ │ CAS替换 │ (指向锁记录) │
│ │Displaced │ │ │ 锁标志: 00 │
│ │Mark Word │ │ └──────────────┘
│ └──────────┘ │
│ owner ───────┼──────────→ 锁对象
└──────────────┘
重入时:栈中有多个 Lock Record 指向同一个对象
┌──────────────┐
│ Lock Record 3│ Displaced = null(重入标记)
│ Lock Record 2│ Displaced = null
│ Lock Record 1│ Displaced = 原始 Mark Word
└──────────────┘
释放时从上往下弹出,最后一个恢复 Mark Word
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 8.4.5 轻量级锁的膨胀过程
轻量级锁膨胀为重量级锁
│
↓ 步骤1:分配 ObjectMonitor 对象(C++ 层面,从全局 Monitor 缓存池中获取)
│
↓ 步骤2:将 ObjectMonitor 的 _header 设为原始 Mark Word
│ 将 ObjectMonitor 的 _object 指向锁对象
│
↓ 步骤3:CAS 将对象 Mark Word 替换为指向 ObjectMonitor 的指针
│ 锁标志位变为 10
│
↓ 步骤4:当前竞争失败的线程进入 ObjectMonitor 的 _EntryList
│ 操作系统层面阻塞(park/unpark)
2
3
4
5
6
7
8
9
10
11
12
# 8.4.6 重量级锁原理
重量级锁依赖操作系统的 Mutex Lock(互斥量),涉及用户态到内核态的切换(上下文切换约 10~20 微秒):
ObjectMonitor 的工作流程:
┌─────────────────────────────────────────┐
│ │
│ 请求锁 → _owner == null? │
│ YES → 设为 _owner │
│ NO → 进入 _cxq 队列 │
│ ↓ │
│ 自旋尝试(_SpinFreq 次) │
│ ↓ 自旋失败 │
│ park() 阻塞当前线程 │
│ │
│ 释放锁 → _owner = null │
│ → 唤醒 _EntryList 或 _cxq │
│ 中的一个线程 unpark() │
│ │
│ wait() → 释放 _owner │
│ → 进入 _WaitSet │
│ → park() │
│ │
│ notify() → 从 _WaitSet 取出一个 │
│ → 放入 _EntryList │
│ │
└─────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
性能开销分析:
- 线程阻塞(park):用户态 → 内核态切换,约 10 微秒
- 线程唤醒(unpark):内核态 → 用户态切换,约 10 微秒
- 上下文切换:保存/恢复 CPU 寄存器,约 5~10 微秒
- 总计一次锁竞争:约 20~40 微秒(相比偏向锁的 1~2 纳秒差了万倍)
# 8.4.7 锁升级全景图
新创建的对象
│
↓ JVM 启动后 4 秒(BiasedLockingStartupDelay)
│ 4秒内创建的对象 → 直接无锁(不可偏向)
│ 4秒后创建的对象 → 匿名偏向(偏向标志=1,线程ID=0)
│
↓ 线程A 首次加锁
偏向锁(Mark Word 存储线程A 的 ID)
│
↓ 线程B 尝试获取
撤销偏向锁(到达安全点 STW)
│
├─ 线程A 已退出同步块 → 撤销偏向,CAS 竞争
└─ 线程A 仍在同步块 → 升级
│
↓ CAS 替换 Mark Word
轻量级锁(Mark Word 指向栈中 Lock Record)
│
↓ CAS 自旋失败(竞争激烈)
膨胀为重量级锁(Mark Word 指向 ObjectMonitor)
│
↓ 所有线程释放锁
│ Mark Word 恢复
无锁状态
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
特殊路径:
- 如果对象已调用
hashCode(),无法进入偏向锁(偏向锁的 Mark Word 没有空间存 hashCode) - 调用
wait()/notify()会直接膨胀为重量级锁(这些方法依赖 ObjectMonitor 的 _WaitSet)
# 8.5 锁优化技术
# 8.5.1 锁消除
JIT 编译器通过逃逸分析发现锁对象不会被其他线程访问,直接消除锁操作:
// 场景1:局部变量上的锁
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // sb 不会逃逸出方法
sb.append(s1); // StringBuffer.append 是 synchronized 的
sb.append(s2);
return sb.toString();
}
// JIT 优化后:消除 StringBuffer 内部的所有 synchronized
// 场景2:循环中的局部锁
public void process() {
Object lock = new Object(); // 每次调用创建新的 lock
synchronized (lock) { // 这个锁对象不可能被其他线程访问
doWork();
}
}
// JIT 优化后:完全消除 synchronized
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 相关 JVM 参数
-XX:+DoEscapeAnalysis # 开启逃逸分析(默认开启)
-XX:+EliminateLocks # 开启锁消除(默认开启)
2
3
# 8.5.2 锁粗化
将连续的加锁解锁合并为一次更大范围的锁:
// 优化前:连续多次加锁解锁
public void addAll(List<String> items) {
for (String item : items) {
synchronized (lock) {
list.add(item);
}
}
}
// 每次循环都要 monitorenter/monitorexit
// JIT 优化后:粗化为一次加锁
public void addAll(List<String> items) {
synchronized (lock) {
for (String item : items) {
list.add(item);
}
}
}
// 只需一次 monitorenter/monitorexit
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:锁粗化也有副作用——锁持有时间变长,可能增加其他线程的等待时间。JIT 会权衡利弊。
# 8.5.3 自适应自旋
轻量级锁 CAS 失败后,线程不会立即阻塞(阻塞/唤醒开销大),而是自旋(忙等待,反复 CAS 尝试):
// 自旋的本质(伪代码)
while (!cas(lockAddr, expected, myLockRecord)) {
// CAS 失败,继续尝试
spinCount++;
if (spinCount > adaptiveSpinLimit) {
// 自旋超过阈值,放弃自旋,膨胀为重量级锁
inflate();
break;
}
}
2
3
4
5
6
7
8
9
10
自适应的含义:
- 如果在同一个锁上,上次自旋成功获取了锁 → 认为自旋很可能成功 → 增加自旋次数
- 如果上次自旋失败了 → 认为自旋大概率浪费 → 减少自旋次数甚至跳过自旋
JVM 通过运行时统计数据动态调整,无需手动配置。
# 8.5.4 逃逸分析与锁优化的关系
逃逸分析是 JIT 编译器的一项关键优化技术,它分析对象的动态作用域:
逃逸分析结果 → 可能的优化
─────────────────────────────────
不逃逸(方法内局部)→ 锁消除、栈上分配、标量替换
线程逃逸(多线程访问)→ 不做优化
2
3
4
// 不逃逸
public void demo() {
Object lock = new Object(); // lock 不逃逸
synchronized (lock) { } // 锁消除
}
// 线程逃逸
private Object sharedLock = new Object();
public void demo() {
synchronized (sharedLock) { } // 可能被多线程访问,不能消除
}
2
3
4
5
6
7
8
9
10
11
# 8.6 synchronized的可重入性原理
synchronized 是可重入锁——同一个线程可以多次获取同一把锁:
public class ReentrantDemo {
public synchronized void methodA() {
methodB(); // 在持有 this 锁的情况下调用另一个 synchronized 方法
}
public synchronized void methodB() {
// 如果不可重入,这里会死锁!
// 线程在 methodA 中持有 this 锁,进入 methodB 又需要 this 锁
}
}
2
3
4
5
6
7
8
9
10
实现原理:
偏向锁的可重入:Mark Word 中的线程 ID 等于当前线程,直接进入。
轻量级锁的可重入:每次重入在栈帧中压入一个新的 Lock Record(Displaced Mark Word 为 null 作为重入标记)。释放时从上往下弹出 Lock Record,最后一个(Displaced Mark Word 非 null)才真正解锁。
重量级锁的可重入:ObjectMonitor 的 _recursions 计数器。每次重入加 1,退出减 1,减到 0 时释放锁。
// 重入深度为 3 时的栈和 Monitor 状态
// 轻量级锁:
// 栈帧: LR3(null) | LR2(null) | LR1(原始MW)
// 释放顺序: LR3→LR2→LR1(最后恢复 Mark Word)
// 重量级锁:
// Monitor._owner = 当前线程
// Monitor._recursions = 3
// 释放时 _recursions-- 直到 0,才真正释放 _owner
2
3
4
5
6
7
8
9
# 8.7 wait/notify机制与Monitor的关系
# 8.7.1 wait和notify的底层原理
wait() / notify() / notifyAll() 是 Object 的方法,底层依赖 ObjectMonitor:
调用 obj.wait():
1. 当前线程必须是 obj 的 Monitor 的 _owner
2. 释放 Monitor(_owner = null, _recursions = 0)
3. 当前线程进入 _WaitSet(双向链表)
4. 线程被阻塞(park)
调用 obj.notify():
1. 从 _WaitSet 中取出一个线程
2. 将其移到 _EntryList 或 _cxq
3. 被移出的线程等待重新竞争 Monitor
4. 注意:notify() 不会立即释放锁!
调用 notify() 的线程继续执行,直到退出 synchronized 块
调用 obj.notifyAll():
1. 将 _WaitSet 中的所有线程移到 _EntryList
2. 所有线程竞争 Monitor,只有一个能获取
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 8.7.2 为什么wait必须在synchronized块中调用
// 如果不在 synchronized 中调用 wait()
// 会抛出 IllegalMonitorStateException
// 原因:wait() 需要释放 Monitor,如果没有持有 Monitor 怎么释放?
// 更重要的是防止 lost wake-up(丢失唤醒)问题:
// 如果 wait 不需要同步:
// 线程A 检查条件为 false → 准备 wait()
// 但在执行 wait() 之前,线程B 修改条件为 true 并调用 notify()
// 线程A 执行 wait(),但 notify 已经过去了
// 线程A 永远等待 → 丢失唤醒!
// synchronized 保证了"检查条件"和"进入等待"是原子操作
synchronized (lock) {
while (!condition) { // 检查条件
lock.wait(); // 在同一个锁保护下等待
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8.7.3 虚假唤醒与正确使用姿势
// 错误:使用 if 检查条件
synchronized (lock) {
if (!condition) {
lock.wait(); // 虚假唤醒后直接往下执行,条件可能仍然不满足!
}
// 处理...
}
// 正确:使用 while 循环检查条件
synchronized (lock) {
while (!condition) { // 被唤醒后重新检查条件
lock.wait();
}
// 条件确实满足了,安全处理
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
虚假唤醒的来源:
- 操作系统底层:pthread_cond_wait 在某些系统上可能无原因返回
- notifyAll:唤醒所有等待线程,但只有一个能满足条件
- 条件变化:被唤醒时,条件可能已被其他线程改变
# 8.8 synchronized vs ReentrantLock深度对比
# 8.8.1 功能对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置(字节码 + C++) | Java API(基于 AQS) |
| 释放方式 | 自动释放(退出同步块/异常) | 必须手动 unlock()(try-finally) |
| 可中断获取 | 不可中断 | lockInterruptibly() |
| 超时获取 | 不支持 | tryLock(timeout, unit) |
| 非阻塞获取 | 不支持 | tryLock() |
| 公平锁 | 非公平 | 可选公平/非公平 |
| 条件变量 | 只有一个条件(wait/notify) | 多个 Condition |
| 锁升级 | 偏向→轻量级→重量级 | 无升级概念(直接 CAS + park) |
| 性能 | JDK 6+ 优化后与 Lock 接近 | 稳定 |
# 8.8.2 AQS框架原理
ReentrantLock 基于 AQS(AbstractQueuedSynchronizer) 实现:
AQS 核心结构:
┌───────────────────────────────┐
│ state: int(锁状态) │ 0=未锁定, >0=锁定(重入次数)
│ head → Node → Node → tail │ CLH 变体队列(双向链表)
│ (等待获取锁的线程队列) │
└───────────────────────────────┘
获取锁过程(非公平模式):
1. CAS 尝试将 state 从 0 改为 1
→ 成功:获取锁,设为 exclusiveOwnerThread
→ 失败:进入队列
2. 入队后自旋 + park 等待
3. 前驱节点释放锁时 unpark 后继节点
释放锁过程:
1. state - 1(支持重入)
2. state == 0 时真正释放
3. unpark 头节点的后继节点
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8.8.3 选择建议与使用场景
优先使用 synchronized:
- 代码更简洁,不用担心忘记 unlock
- JVM 可以做锁升级、锁消除等优化
- 未来 JVM 版本可能进一步优化
- 适合大多数简单同步场景
选择 ReentrantLock 的场景:
// 1. 需要超时获取锁
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try { doWork(); }
finally { lock.unlock(); }
} else {
handleTimeout();
}
// 2. 需要可中断获取锁
try {
lock.lockInterruptibly();
try { doWork(); }
finally { lock.unlock(); }
} catch (InterruptedException e) {
handleInterruption();
}
// 3. 需要多个条件变量(生产者-消费者)
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者等待 notFull,唤醒 notEmpty
// 消费者等待 notEmpty,唤醒 notFull
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 8.9 synchronized的版本演进
| 版本 | 变化 | 影响 |
|---|---|---|
| JDK 1.0 | synchronized 诞生 | 纯重量级锁实现 |
| JDK 1.4 | 引入自旋锁 | 减少线程阻塞开销 |
| JDK 1.6 | 锁升级(偏向锁+轻量级锁) | 性能大幅提升 |
| JDK 1.6 | 锁消除、锁粗化 | 编译器级别优化 |
| JDK 1.6 | 自适应自旋 | 智能调整自旋次数 |
| JDK 15 | 默认关闭偏向锁 | -XX:-UseBiasedLocking |
| JDK 18 | 废弃偏向锁 | 偏向锁代码维护成本高,现代应用中收益递减 |
JDK 15 废弃偏向锁的原因:
- 偏向锁的撤销需要 STW,在高并发应用中反而有害
- 偏向锁的代码复杂度高(占 HotSpot 同步代码的 ~10%),维护成本大
- 现代应用中单线程访问锁的场景减少(微服务、响应式编程)
- 轻量级锁的 CAS 开销在现代 CPU 上已经很小
JDK 21 虚拟线程对 synchronized 的影响:
// 虚拟线程中使用 synchronized 会导致"线程钉扎"(Pinning)
// 即虚拟线程被钉在载体线程上,无法切换
// ❌ 虚拟线程中使用 synchronized 会阻塞载体线程
Thread.startVirtualThread(() -> {
synchronized (lock) {
// 虚拟线程被钉在载体线程上
// 如果这里有 IO 操作,载体线程也被阻塞
blockingIO();
}
});
// ✅ 虚拟线程中推荐使用 ReentrantLock
Thread.startVirtualThread(() -> {
lock.lock();
try {
blockingIO(); // 虚拟线程可以正常切换
} finally {
lock.unlock();
}
});
// JDK 24 正在改进 synchronized 对虚拟线程的支持
// 未来可能不再有钉扎问题
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
核心启示:synchronized 在 JVM 中持续演进。理解其底层原理,才能在不同版本、不同场景下做出最优选择。
# 8.10 生产环境中的synchronized最佳实践
1. 锁对象的选择:
// 不推荐:锁 this
public synchronized void method() { }
// 外部可以 synchronized(obj) 导致意外竞争
// 不推荐:锁 Class
public static synchronized void method() { }
// 所有实例共享同一把锁
// 推荐:专用锁对象
private final Object lock = new Object();
// 或更好的选择(JDK 14+)
// private final Object lock = new Object() {}; // 匿名内部类,有独特的类名,便于排查
2
3
4
5
6
7
8
9
10
11
12
2. 减小锁粒度:
// 不推荐:方法级别的锁
public synchronized void process(Request req) {
validate(req); // 不需要同步
compute(req); // 不需要同步
updateCache(req); // 需要同步
}
// 推荐:只锁必要的部分
public void process(Request req) {
validate(req);
compute(req);
synchronized (cacheLock) {
updateCache(req);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 避免死锁:
// 死锁场景
// 线程A: synchronized(lockA) { synchronized(lockB) { } }
// 线程B: synchronized(lockB) { synchronized(lockA) { } }
// 解决方案1:固定加锁顺序
// 总是先锁 lockA 再锁 lockB
// 解决方案2:使用 tryLock 超时
// lock.tryLock(timeout) 避免永久等待
// 解决方案3:使用专门的并发工具
// ConcurrentHashMap、CopyOnWriteArrayList 等
2
3
4
5
6
7
8
9
10
11
12
# 8.11 总结与核心要点
锁升级的设计哲学:
- 乐观到悲观:偏向锁假设无竞争,轻量级锁假设轻度竞争,重量级锁应对激烈竞争
- 按需升级:从零开销的偏向锁开始,只在真正需要时才付出更大代价
- 空间换时间:Mark Word 复用不同状态的位,用极少空间支持四种锁状态
- 持续演进:从全程 STW 到偏向锁到废弃偏向锁,JVM 团队持续根据真实工作负载调优
核心要点速查:
| 锁状态 | Mark Word 存储 | 加锁方式 | 开销 | 适用场景 |
|---|---|---|---|---|
| 偏向锁 | 线程 ID | 比较线程 ID | ~1ns | 单线程反复进入 |
| 轻量级锁 | Lock Record 指针 | CAS 操作 | ~10ns | 线程交替执行 |
| 重量级锁 | Monitor 指针 | Mutex + 内核切换 | ~10μs | 多线程激烈竞争 |
面试核心回答框架:
Q: synchronized 的实现原理?
A: 1) 字节码层面:monitorenter/monitorexit
2) 对象头层面:Mark Word 存储锁状态
3) JVM 层面:锁升级机制(偏向→轻量级→重量级)
4) 优化层面:锁消除、锁粗化、自适应自旋
5) 底层实现:ObjectMonitor(C++),依赖 Mutex
2
3
4
5
6
理解锁升级,就能理解为什么 synchronized 在现代 JVM 中性能已经足够好,以及什么场景下应该选择 ReentrantLock。