synchronized的锁升级过程是怎样的?
核心概念
synchronized 的锁升级是 JVM 的自适应优化策略,根据锁竞争程度动态调整锁的实现方式,从低开销的偏向锁逐步升级到重量级锁,实现性能与线程安全的平衡。
锁升级路径
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
特点:单向升级,不可降级(JDK 6-14,JDK 15后默认禁用偏向锁)
详细升级过程
阶段1:无锁状态
Mark Word 结构(64位JVM)
|------------------------------------------------------|
| 未使用(25bit) | hashcode(31bit) | 分代年龄(4bit) | 01 |
|------------------------------------------------------|
特征
- 对象刚创建时的初始状态
- 标志位:
01,且无偏向标记 - 没有线程访问
阶段2:偏向锁(Biased Lock)
触发条件
- 第一个线程访问 synchronized 代码块
- JVM 参数
-XX:+UseBiasedLocking开启(JDK 6-14 默认开启) - 对象未禁用偏向(hashCode 未计算)
Mark Word 结构
|--------------------------------------------------------|
| 线程ID(54bit) | Epoch(2bit) | 分代年龄(4bit) | 偏向标记1 | 01 |
|--------------------------------------------------------|
加锁流程
synchronized(obj) {
// 步骤1:检查 Mark Word 是否为偏向模式
if (是偏向模式 && 线程ID == 当前线程ID) {
// 直接进入,无需任何操作(最快路径)
return;
}
// 步骤2:CAS 尝试将线程ID写入 Mark Word
if (CAS成功) {
// 偏向成功
return;
}
// 步骤3:CAS 失败,说明有竞争,撤销偏向锁
revokeBias(); // 升级为轻量级锁
}
偏向锁撤销
触发场景:
- 其他线程尝试获取锁
- 调用
wait()/notify() - 调用
System.identityHashCode()
撤销流程:
1. 暂停拥有偏向锁的线程(STW)
2. 检查线程是否还在执行同步代码
- 是 → 升级为轻量级锁
- 否 → 恢复为无锁状态
3. 唤醒线程
阶段3:轻量级锁(Lightweight Lock)
触发条件
- 偏向锁撤销后
- 有其他线程竞争,但竞争不激烈
- 锁持有时间短
Mark Word 结构
|------------------------------------------------------|
| 指向栈中Lock Record的指针 (62bit) | 00 |
|------------------------------------------------------|
加锁流程
synchronized(obj) {
// 步骤1:在当前线程栈帧中创建 Lock Record
LockRecord record = new LockRecord();
// 步骤2:将对象的 Mark Word 复制到 Lock Record
record.displacedMarkWord = obj.markWord;
// 步骤3:CAS 将对象 Mark Word 替换为指向 Lock Record 的指针
if (CAS(obj.markWord, &record)) {
// 加锁成功
return;
}
// 步骤4:CAS 失败,进入自旋等待
spinAcquire();
}
自旋等待
void spinAcquire() {
int spinCount = 自适应自旋次数; // 默认10次左右
while (spinCount-- > 0) {
if (CAS(obj.markWord, &record)) {
return; // 自旋成功
}
// 空转等待
}
// 自旋失败,膨胀为重量级锁
inflate();
}
解锁流程
// 通过 CAS 将 Lock Record 中的 Mark Word 还原到对象头
if (CAS(obj.markWord, record.displacedMarkWord)) {
// 解锁成功
return;
}
// CAS 失败,说明有其他线程在自旋或已膨胀,走重量级锁释放流程
阶段4:重量级锁(Heavyweight Lock)
触发条件
- 自旋失败(自旋次数超过阈值)
- 等待线程超过 CPU 核心数的一半
- 自旋线程超过 1 个
- 调用
wait()/notify()
Mark Word 结构
|------------------------------------------------------|
| 指向 Monitor 对象的指针 (62bit) | 10 |
|------------------------------------------------------|
Monitor 对象结构
ObjectMonitor {
_owner = null; // 持有锁的线程
_WaitSet = []; // 调用 wait() 的线程队列
_EntryList = []; // 等待获取锁的线程队列
_recursions = 0; // 重入次数
_count = 0; // 计数器
}
加锁流程
monitorenter(obj) {
Monitor* mon = obj.monitor;
if (mon._owner == null) {
// 无人持有,直接获取
mon._owner = currentThread;
return;
}
if (mon._owner == currentThread) {
// 重入
mon._recursions++;
return;
}
// 加入 _EntryList 阻塞等待
mon._EntryList.add(currentThread);
park(currentThread); // 操作系统层面阻塞
}
解锁流程
monitorexit(obj) {
Monitor* mon = obj.monitor;
if (--mon._recursions > 0) {
// 还有重入,不释放
return;
}
mon._owner = null;
// 唤醒 _EntryList 中的一个线程(非公平)
Thread t = mon._EntryList.removeFirst();
unpark(t);
}
升级条件总结表
| 当前状态 | 升级条件 | 目标状态 |
|---|---|---|
| 无锁 | 第一次访问 | 偏向锁 |
| 偏向锁 | 其他线程竞争 | 轻量级锁 |
| 轻量级锁 | 自旋失败/竞争激烈/调用wait() | 重量级锁 |
实际案例分析
案例1:单线程场景
Object lock = new Object();
// 状态:无锁(01)
synchronized(lock) {
// 第一次:升级为偏向锁(01 + 偏向标记)
// 线程ID写入 Mark Word
}
synchronized(lock) {
// 第二次:检查线程ID,直接进入(无 CAS)
}
性能:最优,几乎无额外开销
案例2:两个线程交替访问
// 线程1
synchronized(lock) {
// 偏向线程1
}
// 线程2
synchronized(lock) {
// 偏向撤销 → 升级为轻量级锁(00)
// CAS 竞争锁
}
// 线程1
synchronized(lock) {
// 轻量级锁,CAS 尝试获取
}
性能:较好,通过 CAS 避免阻塞
案例3:多线程高竞争
// 10个线程同时竞争
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized(lock) {
// 1. 第一个线程:偏向锁
// 2. 第二个线程:升级为轻量级锁
// 3. 自旋失败:膨胀为重量级锁(10)
// 4. 后续线程:直接阻塞在 _EntryList
}
}).start();
}
性能:下降,涉及线程阻塞/唤醒开销
关键代码位置(HotSpot)
// 偏向锁撤销
BiasedLocking::revoke_and_rebias()
// 轻量级锁加锁
ObjectSynchronizer::slow_enter()
// 膨胀为重量级锁
ObjectSynchronizer::inflate()
// 重量级锁加锁
ObjectMonitor::enter()
答题总结
synchronized 锁升级的完整流程:
- 无锁 → 偏向锁:
- 首次访问通过 CAS 写入线程 ID
- 后续重入无需任何操作(最快)
- 偏向锁 → 轻量级锁:
- 其他线程竞争时触发偏向撤销
- 在栈帧中创建 Lock Record,通过 CAS 竞争
- 轻量级锁 → 重量级锁:
- 自旋失败或竞争激烈时触发膨胀
- 依赖 Monitor 对象和操作系统互斥量
- 线程阻塞在
_EntryList
设计思想:用空间换时间,根据竞争程度自适应调整,在性能和功能之间取得平衡。