一、对象头(Object Header)与Mark Word
每个Java对象在内存中的布局包含对象头,其中存储了与锁相关的关键信息。对象头由两部分组成:
- Mark Word:存储对象的运行时数据(如哈希码、GC分代年龄、锁状态等)。
- Klass Pointer:指向对象所属类的元数据(Class对象)。
Mark Word的结构
Mark Word的长度为32位或64位(取决于JVM是32位还是64位)。其内容随锁状态动态变化:
锁状态 | 存储内容(简化为通用描述) | 标志位(最后2-3位) |
---|---|---|
无锁 | 哈希码、分代年龄等 | 01 |
偏向锁 | 持有锁的线程ID、偏向时间戳(epoch) | 101 |
轻量级锁 | 指向线程栈中锁记录(Lock Record)的指针 | 00 |
重量级锁 | 指向Monitor对象(ObjectMonitor)的指针 | 10 |
示例(64位JVM的Mark Word布局):
- 无锁状态:
[unused:25bit][identity_hashcode:31bit][unused:1bit][age:4bit][biased_lock:1bit][lock:2bit]
- 偏向锁:
[thread_id:54bit][epoch:2bit][age:4bit][biased_lock:1bit][lock:2bit]
二、Monitor(管程)与ObjectMonitor
当锁升级为重量级锁 时,HotSpot会通过ObjectMonitor
对象实现线程同步。每个Java对象在需要时(首次竞争升级时)会关联一个ObjectMonitor
实例。
ObjectMonitor的核心字段
cpp
// HotSpot源码中的ObjectMonitor结构(简化)
class ObjectMonitor {
void* _header; // 存储对象的Mark Word(用于恢复无锁状态)
void* _owner; // 持有锁的线程(如线程A)
ObjectWaiter* _EntryList; // 阻塞等待锁的线程队列
ObjectWaiter* _WaitSet; // 调用wait()后进入等待状态的线程队列
volatile int _count; // 记录重入次数(如线程A多次加锁)
volatile int _waiters; // 等待队列中的线程数
// 其他字段省略...
};
Monitor的工作流程
- 线程竞争锁 :
- 若
_owner
为null
,线程通过CAS操作将自己设为_owner
,获取锁。 - 若
_owner
非空,线程进入_EntryList
队列,并阻塞(通过操作系统互斥量)。
- 若
- 释放锁 :
- 线程将
_owner
置为null
,并唤醒_EntryList
中的线程。
- 线程将
- 调用wait() :
- 持有锁的线程释放锁,进入
_WaitSet
队列,等待其他线程调用notify()
。
- 持有锁的线程释放锁,进入
三、锁的实现机制
1. 偏向锁(Biased Locking)
- 目标:优化无竞争场景,避免同步操作。
- 实现 :
- 对象初始化时,Mark Word设置为无锁状态。
- 当第一个线程(线程A)获取锁时,JVM将Mark Word的线程ID设置为A的ID,并标记为偏向锁(
101
)。 - 后续线程A进入同步块时,直接检查线程ID是否匹配,无需CAS操作。
- 撤销偏向锁 :
- 当线程B尝试获取锁时,JVM触发偏向锁撤销:
- 在全局安全点(所有线程暂停)检查线程A是否仍在同步块中。
- 若已退出,将Mark Word恢复为无锁状态;若未退出,升级为轻量级锁。
- 当线程B尝试获取锁时,JVM触发偏向锁撤销:
2. 轻量级锁(Lightweight Lock)
- 目标:优化低竞争场景,避免线程阻塞。
- 实现 :
- 线程在栈帧中创建
Lock Record
,拷贝对象头的Mark Word到其中。 - 通过CAS操作将对象头的Mark Word替换为指向
Lock Record
的指针(锁标志位00
)。 - 成功:线程获得锁。
- 失败(其他线程已竞争):触发自旋,超过阈值则升级为重量级锁。
- 线程在栈帧中创建
3. 重量级锁(Heavyweight Lock)
- 目标:处理高竞争场景,确保线程安全。
- 实现 :
- 对象头的Mark Word指向关联的
ObjectMonitor
实例(锁标志位10
)。 - 未获取锁的线程进入
_EntryList
队列,依赖操作系统的互斥量(Mutex)阻塞。
- 对象头的Mark Word指向关联的
四、字节码层面的支持
synchronized
关键字在编译后生成以下字节码指令:
-
同步代码块:
monitorenter
:尝试获取锁。monitorexit
:释放锁(确保在异常时也能释放)。
javasynchronized(obj) { // 代码块 } // 编译后: monitorenter try { // 代码块 } finally { monitorexit }
-
同步方法:
- 方法访问标志设置为
ACC_SYNCHRONIZED
。 - JVM在方法调用和返回时自动处理锁的获取与释放。
javapublic synchronized void method() { // 方法体 }
- 方法访问标志设置为
五、HotSpot的优化策略
- 自适应自旋(Adaptive Spinning) :
- JVM根据最近的自旋成功率动态调整自旋次数,避免CPU空转。
- 批量重偏向(Bulk Rebiasing) :
- 若一个类的偏向锁被多个不同线程获取,JVM会将该类的后续对象偏向新的线程。
- 批量撤销(Bulk Revocation) :
- 若偏向锁被频繁撤销,JVM会禁用该类的偏向锁,后续直接使用轻量级锁。
六、为什么锁降级不被支持?
- 性能考量 :
重量级锁的降级需要复杂的状态回退(如重建Mark Word、释放Monitor),而高竞争场景可能很快再次升级,得不偿失。 - 实现复杂度 :
锁降级需处理线程安全、状态同步等问题,HotSpot选择简化设计,仅支持锁释放后重置为无锁状态。
七、示例:从代码到底层的映射
java
public class SyncExample {
private final Object lock = new Object();
public void doSomething() {
synchronized (lock) { // 1. 尝试获取lock的锁
// 同步代码块
} // 2. 释放锁
}
}
- 首次执行 :
lock
对象处于无锁状态,线程获取偏向锁。
- 竞争出现 :
- 其他线程尝试获取锁,触发偏向锁撤销,升级为轻量级锁。
- 自旋失败 :
- 竞争激烈时,最终升级为重量级锁,线程进入阻塞队列。
总结
从HotSpot角度看,synchronized
的底层实现是对象头、Monitor和锁状态机的精密协作:
- 对象头动态存储锁状态,实现快速状态切换。
- Monitor管理线程竞争与阻塞,提供最终的安全性保障。
- 锁升级在性能与安全间动态权衡,适应不同并发场景。
理解这些机制,不仅能优化高并发代码,还能深入掌握JVM的并发设计哲学。