一、概述
synchronized是 Java 中实现线程同步的关键字,它提供了一种内置的锁机制,可以确保多个线程在访问共享资源时的互斥性。随着 JDK 版本的迭代,synchronized的性能得到了显著优化,其中最重要的优化就是锁升级机制。
二、底层实现基础
2.1 Java 对象头
在 JVM 中,每个 Java 对象在内存中都包含三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
synchronized的锁信息主要存储在对象头的 Mark Word中。
64位 JVM 下 Mark Word 结构:
| 锁状态 | 25 bit | 31 bit | 1 bit | 4 bit | 1 bit (偏向锁位) | 2 bit (锁标志位) |
|---|---|---|---|---|---|---|
| 无锁 | unused | 对象的 hashCode | 分代年龄 | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID(54bit) Epoch | 对象分代年龄 | 1 | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 (62 bit) | 00 | ||||
| 重量级锁 | 指向互斥量(Monitor)的指针 (62 bit) | 10 | ||||
| GC标记 | 与GC相关的信息(62 bit) | 11 |
注意:自 JDK 15 起,偏向锁被默认禁用,但理解其原理仍有重要意义。
2.2 监视器(Monitor)
Monitor 是实现同步的底层对象,每个 Java 对象都可以关联一个 Monitor:
synchronized代码块 →monitorenter和monitorexit指令synchronized方法 →ACC_SYNCHRONIZED方法访问标志
三、锁升级过程
锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
3.1 偏向锁(Biased Locking)
设计初衷:减少同一线程重复获取锁的开销。 工作流程:
- 第一个获取锁的线程将线程ID CAS 到对象头
- 该线程再次进入时直接检查线程ID,匹配则直接获取锁
- 其他线程竞争时,开始偏向锁撤销
优缺点:
- ✅ 无竞争时性能极佳
- ❌ 竞争激烈时撤销成本高
3.2 轻量级锁(Lightweight Locking)
设计初衷:当存在轻度锁竞争时,通过自旋避免线程阻塞。 加锁过程:
// 伪代码流程
1. 在栈帧中创建锁记录(Lock Record)
2. 拷贝对象头Mark Word到锁记录(Displaced Mark Word)
3. CAS将对象头指向锁记录指针
4. 成功则获得锁,失败则自旋重试
自适应自旋:JDK 6 引入,根据上次自旋结果动态调整自旋次数。
3.3 重量级锁(Heavyweight Locking)
触发条件:轻量级锁自旋失败后升级。 特点:
- 基于操作系统互斥量(mutex)实现
- 线程阻塞和唤醒需要内核态切换
- 开销最大但可保证公平性
四、锁升级流程图
flowchart TD
A[线程进入同步块] --> B{检查锁状态}
B -->|无锁(01)| C{偏向锁是否启用?}
C -->|是| D[CAS设置线程ID]
D -->|成功| E[获得偏向锁]
D -->|失败| F[偏向锁撤销]
C -->|否| F
B -->|偏向锁(01)| G{线程ID是否匹配?}
G -->|是| E
G -->|否| F
F --> H[升级为轻量级锁]
H --> I[创建锁记录拷贝Mark Word]
I --> J[CAS替换对象头]
J -->|成功| K[获得轻量级锁]
J -->|失败| L[自适应自旋]
L --> M{自旋成功?}
M -->|是| K
M -->|否| N[升级为重量级锁]
B -->|轻量级锁(00)| O[检查锁记录指针]
O -->|指向当前线程| K
O -->|重入| P[添加空锁记录]
B -->|重量级锁(10)| Q[进入等待队列阻塞]
E --> R[执行同步代码]
K --> R
P --> R
Q --> R
R --> S[退出同步块]
S --> T{当前锁状态}
T -->|偏向锁| U[什么都不做]
T -->|轻量级锁| V[CAS恢复Mark Word]
V -->|成功| W[解锁完成]
V -->|失败| X[已升级为重量级锁<br>唤醒等待线程]
T -->|重量级锁| Y[释放锁并唤醒线程]
五、各锁状态对比
| 锁状态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 无竞争时开销极小 | 竞争时撤销成本高 | 单线程重复访问 |
| 轻量级锁 | 线程不阻塞,响应快 | 自旋消耗CPU | 低竞争、同步块小 |
| 重量级锁 | 不消耗CPU,公平 | 线程切换开销大 | 高竞争、同步块大 |
六、实践建议
- 减少锁粒度:缩小同步代码块范围
- 避免锁竞争:使用并发容器、线程局部变量等
- 监控锁状态:借助JVM参数监控锁升级情况
- 考虑替代方案:在高并发场景下可考虑
ReentrantLock
七、总结
synchronized的锁升级机制体现了 JVM 在性能优化上的智慧:
- 从低开销锁开始,按需升级
- 在响应时间和吞吐量间寻求平衡
- 适应不同竞争程度的并发场景
理解这一机制有助于我们编写更高效的并发程序,并在出现性能问题时能够准确诊断和优化。
本文基于 JDK 8+ 版本编写,具体实现细节可能因 JVM 厂商和版本有所不同。