synchronized实现原理详解

synchronized 关键字的实现原理

synchronized 是 Java 中最基础的同步机制,其实现原理涉及多层次的优化和设计。让我们深入探讨其实现机制:
image

一、JVM 层面的实现基础

1. Java 对象头 (Object Header)

每个 Java 对象在内存中由三部分组成:

  • 对象头 (Header):包含锁信息和 GC 信息
  • 实例数据 (Instance Data):对象字段值
  • 对齐填充 (Padding):内存对齐使用

其中对象头是实现 synchronized 的关键:
image

|-------------------------------------------------------|
|                  Mark Word (64 bits)                  |
|-------------------------------------------------------|
|      锁状态       |      61 bits 内容       | 锁标志位 |
|-------------------------------------------------------|
|   无锁状态      | unused|hashCode|分代年龄|     01   |
|   偏向锁        | 线程ID| Epoch |分代年龄|     01   |
|   轻量级锁      |         指向栈中锁记录的指针        | 00 |
|   重量级锁      |        指向Monitor的指针           | 10 |
|   GC标记        |              空                  | 11 |
|-------------------------------------------------------|

2. Monitor(管程)

每个 Java 对象都与一个 Monitor 关联:

  • 当线程进入 synchronized 代码块时,会尝试获取对象的 Monitor
  • Monitor 的主要结构:
    ObjectMonitor {
        void*   _header;      // 对象头
        void*   _owner;       // 持有锁的线程
        intptr_t _recursions; // 重入次数
        ObjectWaiter* _EntryList;  // 等待队列
        ObjectWaiter* _WaitSet;    // wait() 等待队列
        // ...
    }
    

二、锁升级过程(锁膨胀)

JVM 为减少锁操作开销,设计了锁升级机制

1. 无锁状态

  • 新创建的对象初始状态
  • 锁标志位:01

2. 偏向锁 (Biased Lock)

  • 适用场景:单线程重复访问同步块
  • 实现原理
    • 通过 CAS 将线程 ID 写入 Mark Word
    • 同一线程再次进入时只需检查线程 ID
    • 锁标志位:01(与无锁相同,但偏向位为1)
  • 优点:零成本加锁解锁
  • 缺点:有竞争时会升级

3. 轻量级锁 (Lightweight Lock)

  • 适用场景:多线程交替执行,无实际竞争
  • 实现原理
    1. 在栈帧创建 Lock Record
    2. 将对象 Mark Word 复制到 Lock Record(Displaced Mark Word)
    3. CAS 将对象头指向 Lock Record
    4. 成功:获取锁;失败:自旋或升级
  • 锁标志位:00

4. 重量级锁 (Heavyweight Lock)

  • 适用场景:多线程竞争激烈
  • 实现原理
    • 指向 Monitor 对象的指针
    • 未获取锁的线程进入阻塞状态
    • 依赖操作系统互斥量(mutex)
  • 锁标志位:10
  • 特点:线程阻塞/唤醒涉及用户态-内核态切换,开销大

image
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

三、锁升级过程示意图

sequenceDiagram participant T as 线程 participant OM as ObjectMonitor participant OS as 操作系统 T->>OM: 尝试通过CAS获取锁 alt 获取成功 OM-->>T: 设置_owner=当前线程 else 获取失败 OM->>OM: 自旋尝试 (轻量级锁) alt 自旋成功 OM-->>T: 获取锁 else 自旋失败 OM->>OM: 将线程加入_EntryList OM->>OS: 调用pthread_mutex_lock() OS->>OS: 线程进入阻塞状态 OS-->>OM: 当锁可用时唤醒 OM-->>T: 获取锁 end end

四、底层实现细节

1. 字节码层面

  • 同步方法:ACC_SYNCHRONIZED 标志
  • 同步块:monitorentermonitorexit 指令

2. 操作系统层面

  • 重量级锁使用 pthread 互斥锁:
    pthread_mutex_lock(&monitor->mutex);
    // 临界区代码
    pthread_mutex_unlock(&monitor->mutex);
    

3. 优化技术

  • 自旋锁:轻量级锁竞争时,线程执行忙循环(默认10次)
  • 适应性自旋:根据历史成功率动态调整自旋次数
  • 锁消除:JIT 编译器消除不可能存在竞争的锁
  • 锁粗化:合并连续的同步块减少锁操作

五、性能特点对比

锁类型 优点 缺点 适用场景
偏向锁 加解锁几乎无开销 撤销开销大 单线程重复访问
轻量级锁 避免线程阻塞 自旋消耗CPU 多线程交替执行
重量级锁 不消耗CPU等待 线程切换开销大 高竞争场景

六、最佳实践

  1. 优先使用 java.util.concurrent 包中的显式锁
  2. 减少同步块范围(锁粒度细化)
  3. 避免在循环内使用同步
  4. 考虑使用 ThreadLocal 避免共享
  5. 高并发场景慎用 synchronized

理解 synchronized 的实现原理有助于:

  • 编写高性能并发代码
  • 合理选择同步机制
  • 诊断死锁和性能问题
  • 深入理解 JVM 并发模型

🎯 总结

synchronized的实现原理包括:

  1. 对象头机制:基于Mark Word存储锁信息
  2. Monitor模型:重量级锁的实现基础
  3. 锁升级策略:从偏向锁→轻量级锁→重量级锁
  4. 字节码支持:monitorenter/monitorexit指令
  5. JVM优化:锁消除、锁粗化等优化策略
posted @ 2025-08-03 17:36  MuXinu  阅读(163)  评论(0)    收藏  举报