synchronized 锁的可重入性实现(偏向锁、轻量级锁、重量级锁)
在 Java 中,synchronized 通过 对象头(Object Header) 中的锁状态标志位(Mark Word)管理锁的升级(偏向锁 → 轻量级锁 → 重量级锁)。其可重入性在不同锁状态下有不同的实现方式,但核心思想是 通过计数器记录锁的持有次数。以下是详细分析:
一、可重入性的通用原理
无论处于哪种锁状态,synchronized 的可重入性都依赖以下机制:
- 计数器(Recursions) : • 每次线程进入同步块时,计数器递增。 • 每次退出同步块时,计数器递减。 • 计数器归零时,锁完全释放。
- 线程绑定: • 锁必须记录当前持有锁的线程(
_owner字段)。
二、偏向锁(Biased Lock)下的可重入性
偏向锁是 JVM 针对 单线程重复获取锁 场景的优化,避免 CAS 操作的开销。
1. 对象头结构
|-------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|--------------------|
| thread_id(54 bits) | epoch(2 bits) | age(4 bits) | Biased |
|-------------------------------------------------------|--------------------|
• thread_id:持有偏向锁的线程 ID。 • epoch:偏向锁的时间戳(用于批量撤销)。 • age:对象分代年龄。
2. 可重入实现
• 首次获取锁: • 通过 CAS 将线程 ID 写入对象头,锁标志位设为偏向锁(101)。 • 计数器初始化为 1。 • 重入锁: • 检查对象头的 thread_id 是否为当前线程。 • 无需任何同步操作,直接递增计数器(记录在线程栈的锁记录中)。 • 计数器不存储在对象头,而是通过线程私有数据隐式维护。
3. 示例
public void methodA() {
synchronized (this) { // 偏向锁:首次获取锁,thread_id 写入对象头
methodB(); // 重入时仅递增计数器(无需 CAS)
}
}
public void methodB() {
synchronized (this) { // 重入:检查 thread_id 一致,计数器递增至 2
// ...
} // 退出:计数器递减至 1
} // 退出:计数器归零,释放锁
三、轻量级锁(Lightweight Lock)下的可重入性
轻量级锁通过 CAS 自旋 处理多线程轻度竞争场景,适用于 低并发、短临界区 的代码。
1. 对象头结构
|-------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record(62 bits) | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
• ptr_to_lock_record:指向线程栈中锁记录(Lock Record)的指针。
2. 可重入实现
• 首次获取锁:
- 在栈帧中创建锁记录(Lock Record),存储对象头的 Mark Word 副本。
- 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针(锁标志位
00)。 - 计数器初始化为 1。 • 重入锁:
- 检查对象头的指针是否指向当前线程的锁记录。
- 新增一个锁记录(如
Lock Record 2)存储当前对象头状态(计数器隐式递增)。 - 每个退出操作对应一个锁记录的弹出,直到最后一个锁记录释放锁。
3. 示例
public void methodA() {
synchronized (this) { // 轻量级锁:CAS 成功,创建 Lock Record 1
methodB(); // 重入:创建 Lock Record 2
}
}
public void methodB() {
synchronized (this) { // 重入:检查锁记录指针一致
// ...
} // 退出:弹出 Lock Record 2,计数器递减
} // 退出:弹出 Lock Record 1,CAS 恢复对象头,释放锁
四、重量级锁(Heavyweight Lock)下的可重入性
重量级锁通过操作系统互斥量(Mutex)和监视器(Monitor)实现,适用于 高并发、长临界区 场景。
1. 对象头结构
|-------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|-------------------------------------------------------|--------------------|
| ptr_to_monitor(62 bits) | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
• ptr_to_monitor:指向 Monitor 对象的指针。
2. Monitor 结构(C++ 实现)
class ObjectMonitor {
Thread* _owner; // 持有锁的线程
intptr_t _recursions; // 重入次数计数器
EntryList _EntryList; // 等待锁的线程队列
WaitSet _WaitSet; // 调用 wait() 的线程队列
};
3. 可重入实现
• 首次获取锁:
- 通过 CAS 或操作系统互斥量获取锁,
_owner设为当前线程,_recursions = 1。 • 重入锁: - 检查
_owner是否为当前线程。 - 直接递增
_recursions,无需竞争锁。 • 释放锁: - 递减
_recursions,若归零则释放锁并唤醒等待线程。
4. 示例
public void methodA() {
synchronized (this) { // 重量级锁:_owner 设为当前线程,_recursions=1
methodB(); // 重入:_recursions 递增至 2
}
}
public void methodB() {
synchronized (this) { // 重入:检查 _owner 一致,_recursions=2
// ...
} // 退出:_recursions 递减至 1
} // 退出:_recursions 归零,释放锁
五、锁升级与可重入性
当锁从 偏向锁 升级为 轻量级锁 或 重量级锁 时,可重入性仍然有效:
- 偏向锁 → 轻量级锁: • 当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁。 • 原线程的重入次数通过锁记录隐式维护。
- 轻量级锁 → 重量级锁: • 当自旋失败次数超过阈值(默认 10 次),升级为重量级锁。 • 原线程的重入次数由 Monitor 的
_recursions字段接管。
六、总结
| 锁状态 | 可重入实现方式 |
|---|---|
| 偏向锁 | 隐式计数器(无需 CAS),线程 ID 匹配时直接重入。 |
| 轻量级锁 | 通过栈帧锁记录隐式维护计数器,每次重入新增一个锁记录。 |
| 重量级锁 | 通过 Monitor 的 _recursions 字段显式记录重入次数。 |
synchronized 的可重入性通过 计数器 + 线程绑定 实现,且在不同锁状态下有不同的优化策略: • 偏向锁:单线程无竞争时零开销重入。 • 轻量级锁:低竞争时通过锁记录隐式维护。 • 重量级锁:高竞争时依赖操作系统级别的计数器管理。
理解这一机制有助于编写高效且线程安全的代码,避免死锁并优化性能。