1. Java对象头:锁状态的物理载体
在Java虚拟机中,每个对象都有一块神秘的区域叫做对象头(Object Header),它就像对象的身份证,存储着关键的元数据信息。对于理解synchronized锁机制来说,对象头中的Mark Word区域尤为重要——它用64位(在64位JVM中)的空间记录了对象的哈希码、GC年龄、锁状态等核心信息。
我曾在排查一个线上死锁问题时,通过分析对象内存布局发现:当锁处于不同状态时,Mark Word的结构会动态变化。比如无锁状态下,25位存储哈希码,4位存储GC年龄;而重量级锁状态下,62位直接存储指向Monitor对象的指针。这种设计就像变色龙一样,根据场景动态调整内存布局,既节省空间又提升效率。
// 查看对象内存布局的JOL工具示例 public class ObjectLayoutDemo { public static void main(String[] args) { Object obj = new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); } }运行这段代码你会看到类似如下的输出(以64位JVM为例):
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION 0 4 (object header) // Mark Word开始 4 4 (object header) // Mark Word继续 8 4 (object padding) Instance size: 16 bytes2. Monitor锁的物理实现
2.1 从对象头到Monitor指针
当线程首次获取synchronized锁时,JVM会将对象头中的锁标志位从01(无锁/偏向锁)变为10(重量级锁),同时将Mark Word部分替换为指向ObjectMonitor的指针。这个转换过程就像把普通门锁升级为保险柜——Monitor提供了更复杂的线程管理机制。
我在性能调优时发现一个关键细节:这个转换并非立即发生。JVM会先尝试偏向锁(避免同步开销),失败后再升级为轻量级锁(CAS自旋),最终才会膨胀为重量级锁。这种锁升级策略正是HotSpot的优化精髓。
2.2 ObjectMonitor的核心结构
在HotSpot源码中,ObjectMonitor这个C++类定义了Monitor的核心结构。几个关键字段值得注意:
- _owner:指向持有锁的线程,相当于"锁的主人"
- _EntryList:存储阻塞等待锁的线程(竞争失败的选手)
- _WaitSet:存储调用wait()的线程(主动休息的选手)
- _recursions:记录锁重入次数(主人重复进门的次数)
// HotSpot虚拟机中的ObjectMonitor片段 class ObjectMonitor { void* _header; // 存储对象头 intptr_t _count; // 锁计数器 void* _owner; // 持有线程 ObjectWaiter* _EntryList; // 阻塞线程队列 ObjectWaiter* _WaitSet; // 等待线程队列 // ...其他字段 };3. synchronized的锁竞争流程
3.1 从字节码到机器指令
编译synchronized代码块时,JVM会在字节码中插入monitorenter和monitorexit指令。但实际执行时,这些指令会被JIT编译为更底层的机器码。通过-XX:+PrintAssembly参数可以看到,最终调用的是ObjectMonitor的enter和exit方法。
我在压测时观察到:当竞争激烈时,90%的CPU时间都消耗在cmpxchg(CAS指令)上。这说明锁竞争的本质就是多个线程通过CPU原子指令争抢_owner字段的写入权。
3.2 完整的锁竞争流程
- 快速路径(Fast Path):线程通过CAS尝试直接获取锁,成功则设置_owner
- 自旋优化:失败后不立即阻塞,而是循环尝试(避免线程切换开销)
- 入队等待:自旋超过阈值(XX:PreBlockSpin配置)后进入_EntryList
- 操作系统互斥:最终通过pthread_mutex_lock进入内核态阻塞
// 典型synchronized代码对应的字节码 public void syncMethod() { synchronized(this) { System.out.println("locked"); } }对应的字节码:
aload_0 // 加载this引用 dup astore_1 monitorenter // 进入同步块 ...方法体... aload_1 monitorexit // 退出同步块4. 锁优化的实战经验
4.1 偏向锁的失效场景
虽然偏向锁能减少同步开销,但在某些场景反而会成为性能杀手。比如使用线程池时,由于worker线程存活时间长,容易导致大量偏向锁撤销操作。通过-XX:-UseBiasedLocking关闭偏向锁后,我们某个服务的吞吐量反而提升了15%。
4.2 自适应自旋的智慧
JVM会根据历史成功率动态调整自旋次数(-XX:PreBlockSpin)。有次排查线上问题发现,当锁持有时间超过1ms时,自旋反而增加了CPU负载。最终我们通过-XX:PreBlockSpin=20将默认值从10调整为20,获得了更好的平衡。
4.3 对象头与锁的关系验证
通过以下实验可以直观看到锁状态变化:
- 新建对象:锁标志位为001(无锁)
- 首次synchronized:变为101(偏向锁)
- 第二个线程竞争:变为00(轻量级锁)
- 更多线程竞争:变为10(重量级锁)
// 验证锁状态变化的示例 public static void main(String[] args) throws Exception { Object obj = new Object(); System.out.println("初始状态:"); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); synchronized (obj) { System.out.println("第一个线程加锁:"); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); } new Thread(() -> { synchronized (obj) { System.out.println("第二个线程竞争:"); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); } }).start(); }5. 从设计思想看同步机制
无论是synchronized的ObjectMonitor还是AQS,其核心思想都包含三个关键点:
- 状态记录:通过volatile变量(_owner/state)快速判断锁状态
- 排队机制:竞争失败的线程进入队列等待(_EntryList/ConditionQueue)
- 线程唤醒:锁释放时精确唤醒等待线程(避免惊群效应)
这种设计就像医院挂号系统:挂号机相当于CAS操作,候诊区相当于_EntryList,而医生叫号就是线程唤醒机制。理解这个类比后,再回头看源码会有豁然开朗的感觉。
在实际编码中,我常遇到这样的误区:认为synchronized性能一定比Lock差。其实在低竞争场景下,经过锁消除、锁粗化等优化后,synchronized的性能可能更好,毕竟它作为内置锁享受了更多JVM优化特权。