从HotSpot角度看synchronized的底层结构

一、对象头(Object Header)与Mark Word

每个Java对象在内存中的布局包含对象头,其中存储了与锁相关的关键信息。对象头由两部分组成:

  1. Mark Word:存储对象的运行时数据(如哈希码、GC分代年龄、锁状态等)。
  2. 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的工作流程

  1. 线程竞争锁
    • _ownernull,线程通过CAS操作将自己设为_owner,获取锁。
    • _owner非空,线程进入_EntryList队列,并阻塞(通过操作系统互斥量)。
  2. 释放锁
    • 线程将_owner置为null,并唤醒_EntryList中的线程。
  3. 调用wait()
    • 持有锁的线程释放锁,进入_WaitSet队列,等待其他线程调用notify()

三、锁的实现机制

1. 偏向锁(Biased Locking)

  • 目标:优化无竞争场景,避免同步操作。
  • 实现
    • 对象初始化时,Mark Word设置为无锁状态。
    • 当第一个线程(线程A)获取锁时,JVM将Mark Word的线程ID设置为A的ID,并标记为偏向锁(101)。
    • 后续线程A进入同步块时,直接检查线程ID是否匹配,无需CAS操作。
  • 撤销偏向锁
    • 当线程B尝试获取锁时,JVM触发偏向锁撤销:
      • 在全局安全点(所有线程暂停)检查线程A是否仍在同步块中。
      • 若已退出,将Mark Word恢复为无锁状态;若未退出,升级为轻量级锁。

2. 轻量级锁(Lightweight Lock)

  • 目标:优化低竞争场景,避免线程阻塞。
  • 实现
    • 线程在栈帧中创建Lock Record,拷贝对象头的Mark Word到其中。
    • 通过CAS操作将对象头的Mark Word替换为指向Lock Record的指针(锁标志位00)。
    • 成功:线程获得锁。
    • 失败(其他线程已竞争):触发自旋,超过阈值则升级为重量级锁。

3. 重量级锁(Heavyweight Lock)

  • 目标:处理高竞争场景,确保线程安全。
  • 实现
    • 对象头的Mark Word指向关联的ObjectMonitor实例(锁标志位10)。
    • 未获取锁的线程进入_EntryList队列,依赖操作系统的互斥量(Mutex)阻塞。

四、字节码层面的支持

synchronized关键字在编译后生成以下字节码指令:

  1. 同步代码块

    • monitorenter:尝试获取锁。
    • monitorexit:释放锁(确保在异常时也能释放)。
    java 复制代码
    synchronized(obj) {
        // 代码块
    }
    // 编译后:
    monitorenter
    try {
        // 代码块
    } finally {
        monitorexit
    }
  2. 同步方法

    • 方法访问标志设置为ACC_SYNCHRONIZED
    • JVM在方法调用和返回时自动处理锁的获取与释放。
    java 复制代码
    public synchronized void method() {
        // 方法体
    }

五、HotSpot的优化策略

  1. 自适应自旋(Adaptive Spinning)
    • JVM根据最近的自旋成功率动态调整自旋次数,避免CPU空转。
  2. 批量重偏向(Bulk Rebiasing)
    • 若一个类的偏向锁被多个不同线程获取,JVM会将该类的后续对象偏向新的线程。
  3. 批量撤销(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. 释放锁
    }
}
  1. 首次执行
    • lock对象处于无锁状态,线程获取偏向锁。
  2. 竞争出现
    • 其他线程尝试获取锁,触发偏向锁撤销,升级为轻量级锁。
  3. 自旋失败
    • 竞争激烈时,最终升级为重量级锁,线程进入阻塞队列。

总结

从HotSpot角度看,synchronized的底层实现是对象头、Monitor和锁状态机的精密协作:

  • 对象头动态存储锁状态,实现快速状态切换。
  • Monitor管理线程竞争与阻塞,提供最终的安全性保障。
  • 锁升级在性能与安全间动态权衡,适应不同并发场景。

理解这些机制,不仅能优化高并发代码,还能深入掌握JVM的并发设计哲学。

相关推荐
橘子青衫几秒前
Java并发编程利器:CyclicBarrier与CountDownLatch解析
java·后端·性能优化
天天摸鱼的java工程师12 分钟前
高考放榜夜,系统别崩!聊聊查分系统怎么设计,三张表足以?
java·后端·mysql
天天摸鱼的java工程师20 分钟前
深入理解 Spring 核心:IOC 与 AOP 的原理与实践
java·后端
漫步者TZ20 分钟前
【Netty系列】解决TCP粘包和拆包:LengthFieldBasedFrameDecoder
java·网络协议·tcp/ip·netty
愿你是阳光06071 小时前
Java-redis实现限时在线秒杀功能
java·redis·bootstrap
我爱Jack1 小时前
Spring Boot统一功能处理深度解析
java·spring boot·后端
苦学编程的谢1 小时前
Java网络编程API 1
java·开发语言·网络
寒山李白2 小时前
Java 依赖注入、控制反转与面向切面:面试深度解析
java·开发语言·面试·依赖注入·控制反转·面向切面
casual_clover2 小时前
Android 之 kotlin语言学习笔记三(Kotlin-Java 互操作)
android·java·kotlin
AA-代码批发V哥2 小时前
Java正则表达式完全指南
java·正则表达式