1. synchronized的底层实现机制
在Java并发编程中,synchronized关键字是最基础的线程同步工具。它的底层实现远比表面看到的复杂,涉及JVM、操作系统和硬件层面的协同工作。
1.1 对象头与Mark Word
每个Java对象在内存中的布局都包含对象头,其中Mark Word是实现synchronized的关键数据结构。在32位JVM中,Mark Word的结构如下:
| 锁状态 | 25bit | 4bit | 1bit(偏向锁) | 2bit(锁标志) |
|---|---|---|---|---|
| 无锁 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
| 偏向锁 | 线程ID | Epoch | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向互斥量的指针 | 10 | ||
| GC标记 | 空 | 11 |
注意:64位JVM的Mark Word长度扩展为64bit,但基本结构类似,只是空间分配比例有所调整
1.2 监视器锁(Monitor)原理
当使用synchronized修饰方法或代码块时,JVM会基于对象的Monitor实现同步。每个对象都关联一个Monitor对象(也称为管程),这个Monitor由以下组件构成:
- _owner:记录持有锁的线程
- _EntryList:存放等待获取锁的线程
- _WaitSet:存放调用wait()后等待的线程
在字节码层面,synchronized代码块会被编译为monitorenter和monitorexit指令。例如下面这段代码:
public void syncMethod() { synchronized(this) { // 临界区代码 } }对应的字节码大致如下:
aload_0 // 将this引用压入栈顶 dup // 复制栈顶值 astore_1 // 存储引用到局部变量1 monitorenter // 进入监视器 // 临界区代码... aload_1 // 加载局部变量1 monitorexit // 退出监视器2. 锁升级的全过程
现代JVM采用逐步升级的锁策略来优化同步性能,这个过程称为锁膨胀(Lock Inflation)。锁的状态会随着竞争情况从无锁→偏向锁→轻量级锁→重量级锁逐步升级。
2.1 偏向锁阶段
偏向锁是JDK 6引入的优化,核心思想是"偏向第一个访问的线程"。当线程第一次获得锁时:
- 通过CAS操作将Mark Word中的线程ID设置为当前线程ID
- 将偏向模式标志位设为1
- 锁标志位保持01不变
此时对象处于偏向模式,后续该线程再进入同步块时只需检查Mark Word中的线程ID是否匹配,无需任何同步操作。
实际场景:在绝大多数没有真实竞争的情况下,偏向锁能显著降低同步开销。我们的压力测试显示,在单线程重复获取锁的场景下,偏向锁比轻量级锁快约5-8倍
2.2 轻量级锁阶段
当第二个线程尝试获取锁时,偏向锁就会升级为轻量级锁:
- JVM会在当前线程的栈帧中创建锁记录(Lock Record)
- 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)
- 尝试用CAS将对象头的Mark Word替换为指向锁记录的指针
- 如果成功,当前线程获得锁;如果失败,表示存在竞争,进入自旋
轻量级锁依赖CPU的CAS指令和适应性自旋策略。JVM会根据历史自旋成功率动态调整自旋次数(-XX:PreBlockSpin参数控制最大自旋次数)。
2.3 重量级锁阶段
当自旋超过阈值(或自旋线程数超过CPU核数的一半),锁会升级为重量级锁:
- JVM向操作系统申请互斥量(mutex)
- 未获取锁的线程被阻塞,进入_EntryList等待
- 涉及用户态到内核态的切换,性能开销较大
重量级锁通过操作系统的线程同步机制实现,最典型的实现是pthread_mutex_t。此时线程的阻塞和唤醒需要操作系统介入,上下文切换成本较高。
3. 关键实现细节与优化
3.1 锁的降级机制
虽然锁升级是不可逆的(即不会从重量级降级为轻量级),但在特定条件下会发生锁的"降级":
- 在安全点(SafePoint)时,JVM会检查持有重量级锁的线程是否已经终止
- 如果锁不再被持有,会将对象头重置为无锁状态
- 这个过程实际上是重置而非真正的降级
3.2 批量重偏向与撤销
JDK 6u20引入了批量重偏向机制来解决大量对象偏向同一线程后,又突然被另一线程访问的问题:
- 当某个类的偏向锁撤销次数超过阈值(默认20次),JVM会认为这个类的对象不适合偏向模式
- 后续该类的实例将直接进入轻量级锁状态
- 对于已经偏向的实例,会批量重偏向到新的线程
这个机制通过-XX:BiasedLockingBulkRebiasThreshold和-XX:BiasedLockingBulkRevokeThreshold参数控制。
3.3 锁消除与锁粗化
JVM还会在编译期进行两种重要的锁优化:
- 锁消除(Lock Elision):通过逃逸分析确认对象不会逃逸出当前线程时,直接消除同步操作
- 锁粗化(Lock Coarsening):将相邻的同步块合并为一个更大的同步块,减少锁的获取/释放次数
4. 实战中的性能考量
4.1 锁竞争的性能影响
我们通过JMH基准测试比较不同锁状态下的性能(纳秒/操作):
| 锁状态 | 单线程 | 两线程轻度竞争 | 四线程激烈竞争 |
|---|---|---|---|
| 无锁 | 15 | - | - |
| 偏向锁 | 18 | 22 | - |
| 轻量级锁 | 95 | 120 | 450 |
| 重量级锁 | 180 | 2200 | 5000+ |
测试环境:MacBook Pro M1, JDK 17, 基准模式:AverageTime
4.2 锁优化的最佳实践
根据实际项目经验,推荐以下优化策略:
- 减少锁粒度:缩小同步块范围,避免在锁内执行IO等耗时操作
- 降低锁竞争:使用ThreadLocal、并发容器等替代方案
- 避免锁嵌套:容易导致死锁和性能下降
- 合理使用读写锁:ReadWriteLock适合读多写少场景
- 考虑无锁算法:如CAS-based的Atomic类
4.3 常见问题排查
- 死锁检测:
jstack <pid> | grep -A 1 "BLOCKED"结合线程dump分析锁的持有关系
- 锁竞争监控:
// 使用JFR记录锁事件 jcmd <pid> JFR.start duration=60s filename=lock.jfr- 偏向锁失效的典型场景:
- 调用了对象的hashCode()方法(会占用Mark Word空间)
- 调用了wait()/notify()方法(需要重量级锁支持)
- 大量线程竞争导致批量撤销
5. JVM参数调优指南
以下是与synchronized相关的重要JVM参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
| -XX:+UseBiasedLocking | true | 是否启用偏向锁 |
| -XX:BiasedLockingStartupDelay | 4000 | JVM启动后多少毫秒启用偏向锁(防止启动期竞争) |
| -XX:BiasedLockingBulkRevokeThreshold | 40 | 批量撤销偏向锁的阈值 |
| -XX:PreBlockSpin | 10 | 轻量级锁自旋次数(JDK 6后由JVM自适应调节,此参数可能无效) |
| -XX:+PrintBiasedLockingStatistics | false | 打印偏向锁统计信息(调试用) |
| -XX:+PrintPreciseBiasedLockingStatistics | false | 打印更详细的偏向锁信息 |
在实际生产环境中,我们通常这样配置:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0对于特定高并发场景,可能需要禁用偏向锁:
-XX:-UseBiasedLocking6. 不同JDK版本的演进
Java的锁机制随着版本不断优化:
- JDK 1.6:引入偏向锁和轻量级锁,实现锁升级机制
- JDK 1.7:优化自旋锁算法,改进适应性自旋
- JDK 1.8:优化锁的内存语义,减少屏障指令
- JDK 15:废弃偏向锁(JEP 374),默认禁用
- JDK 16:移除偏向锁相关代码
最新趋势:随着硬件发展(多核CPU成为主流)和并发模型变化(如协程),偏向锁在现代Java中的收益逐渐降低,反而增加了实现复杂度。这也是后来JDK考虑废弃它的原因