synchronized 锁的可重入性实现(偏向锁、轻量级锁、重量级锁)

209 阅读5分钟

synchronized 锁的可重入性实现(偏向锁、轻量级锁、重量级锁)

在 Java 中,synchronized 通过 对象头(Object Header) 中的锁状态标志位(Mark Word)管理锁的升级(偏向锁 → 轻量级锁 → 重量级锁)。其可重入性在不同锁状态下有不同的实现方式,但核心思想是 通过计数器记录锁的持有次数。以下是详细分析:


一、可重入性的通用原理

无论处于哪种锁状态,synchronized 的可重入性都依赖以下机制:

  1. 计数器(Recursions) : • 每次线程进入同步块时,计数器递增。 • 每次退出同步块时,计数器递减。 • 计数器归零时,锁完全释放。
  2. 线程绑定: • 锁必须记录当前持有锁的线程(_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. 可重入实现

首次获取锁

  1. 在栈帧中创建锁记录(Lock Record),存储对象头的 Mark Word 副本。
  2. 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针(锁标志位 00)。
  3. 计数器初始化为 1。 • 重入锁
  4. 检查对象头的指针是否指向当前线程的锁记录。
  5. 新增一个锁记录(如 Lock Record 2)存储当前对象头状态(计数器隐式递增)。
  6. 每个退出操作对应一个锁记录的弹出,直到最后一个锁记录释放锁。
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. 可重入实现

首次获取锁

  1. 通过 CAS 或操作系统互斥量获取锁,_owner 设为当前线程,_recursions = 1。 • 重入锁
  2. 检查 _owner 是否为当前线程。
  3. 直接递增 _recursions,无需竞争锁。 • 释放锁
  4. 递减 _recursions,若归零则释放锁并唤醒等待线程。
4. 示例
public void methodA() {
    synchronized (this) { // 重量级锁:_owner 设为当前线程,_recursions=1
        methodB();       // 重入:_recursions 递增至 2
    }
}
​
public void methodB() {
    synchronized (this) { // 重入:检查 _owner 一致,_recursions=2
        // ...
    } // 退出:_recursions 递减至 1
} // 退出:_recursions 归零,释放锁

五、锁升级与可重入性

当锁从 偏向锁 升级为 轻量级锁重量级锁 时,可重入性仍然有效:

  1. 偏向锁 → 轻量级锁: • 当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁。 • 原线程的重入次数通过锁记录隐式维护。
  2. 轻量级锁 → 重量级锁: • 当自旋失败次数超过阈值(默认 10 次),升级为重量级锁。 • 原线程的重入次数由 Monitor 的 _recursions 字段接管。

六、总结

锁状态可重入实现方式
偏向锁隐式计数器(无需 CAS),线程 ID 匹配时直接重入。
轻量级锁通过栈帧锁记录隐式维护计数器,每次重入新增一个锁记录。
重量级锁通过 Monitor 的 _recursions 字段显式记录重入次数。

synchronized 的可重入性通过 计数器 + 线程绑定 实现,且在不同锁状态下有不同的优化策略: • 偏向锁:单线程无竞争时零开销重入。 • 轻量级锁:低竞争时通过锁记录隐式维护。 • 重量级锁:高竞争时依赖操作系统级别的计数器管理。

理解这一机制有助于编写高效且线程安全的代码,避免死锁并优化性能。