1. 从对象头到锁状态:Java并发的基础设施
在HotSpot虚拟机中,每个对象都拥有一个对象头(Object Header),这个看似简单的数据结构却是整个Java并发体系的基石。对象头主要由两部分组成:Mark Word和类型指针。其中Mark Word的设计尤为精妙,它就像一个变色龙,会根据对象的状态改变自己的形态。
Mark Word在32位JVM中占32位,在64位JVM中占64位。它的结构可以理解为一种"复用空间"的设计——同一块内存区域在不同状态下会存储不同的内容。当对象处于不同锁状态时,Mark Word的位模式会发生变化,这种变化正是锁状态演变的基础。
提示:可以通过JOL(Java Object Layout)工具查看对象内存布局,这是分析锁状态的利器。添加jol-core依赖后,调用ClassLayout.parseInstance(obj).toPrintable()即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 无锁到偏向锁:性能优化的第一步
2.1 无锁状态(001)
新创建的对象默认处于无锁状态。此时Mark Word存储的是对象的哈希码(调用hashCode()方法后生成)和分代年龄信息。在无竞争的场景下,对象会保持这种状态。
有趣的是,哈希码的存储本身就是一个权衡的结果。当对象计算过identity hash code后,这个值会永远保存在Mark Word中(除非发生锁升级)。这意味着一旦对象进入偏向锁状态,就无法再保存哈希码——因为偏向锁会覆盖这部分空间。
2.2 偏向锁(101)
偏向锁是JDK 6引入的重要优化,基于"大多数锁在生命周期内只会被一个线程获取"的观察。当线程第一次获取锁时,JVM会通过CAS操作将线程ID写入Mark Word,同时将锁标志位改为101。
偏向锁的妙处在于后续的获取操作变成了简单的比较:如果Mark Word中的线程ID与当前线程相同,就直接执行同步代码,完全避开了操作系统层面的互斥操作。这种优化对于几乎没有竞争的同步块能带来显著的性能提升。
注意:偏向锁有约4秒的延迟生效时间(默认-XX:BiasedLockingStartupDelay=4000)。这是因为JVM启动时会有大量竞争,此时启用偏向锁反而会降低性能。
3. 轻量级锁:应对短时间竞争的方案
3.1 轻量级锁的加锁过程(00)
当第二个线程尝试获取偏向锁时,锁就会升级为轻量级锁。这个过程相当精妙:
- JVM会在当前线程的栈帧中创建锁记录(Lock Record)空间
- 将Mark Word复制到锁记录中(称为Displaced Mark Word)
- 尝试用CAS将Mark Word替换为指向锁记录的指针
- 如果成功,线程获得锁;如果失败,说明存在竞争,进入自旋
轻量级锁的核心思想是:通过线程局部的锁记录和CAS操作避免操作系统层面的互斥量操作。这种设计对于短时间的锁竞争非常有效,因为自旋的代价通常小于线程挂起的代价。
3.2 自旋优化的边界
轻量级锁的自旋不是无限制的。JDK采用了适应性自旋(Adaptive Spinning)策略,会根据之前自旋的成功率动态调整自旋次数。在单核CPU上,JVM甚至会直接禁用自旋,因为这种情况下自旋纯粹是浪费CPU资源。
自旋优化的黄金法则是:锁持有时间应小于线程上下文切换的时间。根据经验,这个临界点大约在10-100纳秒之间。超过这个时间,自旋反而会降低整体吞吐量。
4. 重量级锁:最终的安全网
4.1 升级为重量级锁(10)
当自旋超过阈值(或自旋失败),轻量级锁就会升级为重量级锁。此时JVM会向操作系统申请互斥量(mutex),并将Mark Word指向一个监视器(Monitor)对象。未能获取锁的线程会被挂起,进入阻塞队列。
重量级锁的Monitor对象包含几个关键组件:
- _owner:指向持有锁的线程
- _EntryList:等待获取锁的线程队列
- _WaitSet:调用了wait()的线程队列
这种设计虽然带来了线程挂起和唤醒的开销,但确保了在激烈竞争下的公平性和系统稳定性。
4.2 锁降级的特殊情况
虽然锁升级是不可逆的,但在某些特殊场景下会发生锁降级。最典型的就是在G1垃圾收集器的并发标记阶段,为了不STW(Stop-The-World),JVM会先将锁降级,完成标记后再恢复。这种操作需要JVM的特别处理,普通开发中几乎不会遇到。
5. 锁消除与锁粗化:JVM的智能优化
5.1 锁消除(Lock Elision)
JVM通过逃逸分析(Escape Analysis)判断同步块是否真的需要加锁。如果确定对象不会逃逸出当前线程,就会直接消除锁操作。例如StringBuffer的同步操作在局部使用时经常被消除。
5.2 锁粗化(Lock Coarsening)
当JVM检测到一连串对同一对象的加锁解锁操作时,会将多个同步块合并为一个更大的同步块。这种优化减少了锁的获取释放次数,特别适用于循环体内的同步操作。
6. 实战中的锁状态监控
6.1 使用工具观察锁状态
除了前面提到的JOL工具,还可以通过以下方式监控锁状态:
- jstack:查看线程的锁持有情况
- JFR(Java Flight Recorder):记录锁竞争事件
- VisualVM:图形化展示锁竞争情况
6.2 性能优化的经验法则
根据锁状态演变的特点,可以总结出一些优化原则:
- 减少同步块范围:只在必要处同步
- 降低锁粒度:用多个小锁代替一个大锁
- 避免热点锁:如ConcurrentHashMap的分段设计
- 读写分离:读多写少时使用ReadWriteLock
- 无锁数据结构:如AtomicInteger、LongAdder
我在实际项目中曾遇到一个典型案例:一个统计服务使用synchronized修饰整个方法,在高并发下性能极差。通过分析发现99%的调用都是读操作,于是改用ReadWriteLock,吞吐量立即提升了20倍。这个案例生动说明了理解锁机制的重要性。
