1. 为什么需要深入理解synchronized
在Java并发编程中,synchronized是最基础也是最常用的同步机制。很多开发者仅仅停留在"知道怎么用"的层面,但当遇到性能瓶颈、死锁问题或者需要调优时,对底层机制的理解就显得尤为重要。
我曾在生产环境遇到过一个典型场景:某个使用synchronized的接口在高并发时性能急剧下降。通过jstack查看线程状态,发现大量线程处于BLOCKED状态。如果不理解对象头、Monitor这些底层机制,就很难定位到是锁竞争导致的性能问题。
2. 对象头:锁信息的存储基石
2.1 对象内存布局
每个Java对象在内存中的存储布局可以分为三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头又包含:
- Mark Word:存储对象的运行时数据
- 类型指针:指向类元数据的指针
- 数组长度(如果是数组)
2.2 Mark Word的奥秘
Mark Word是理解锁机制的关键,它在32位和64位JVM中的长度不同:
| 位数 | 内容 |
|---|---|
| 32位 | 25bit HashCode + 4bit 分代年龄 + 1bit 偏向锁标志 + 2bit 锁标志位 |
| 64位 | 56bit HashCode + 4bit 分代年龄 + 1bit 偏向锁标志 + 2bit 锁标志位 |
锁标志位的不同值代表不同状态:
| 锁标志位 | 状态 |
|---|---|
| 01 | 无锁/偏向锁 |
| 00 | 轻量级锁 |
| 10 | 重量级锁 |
| 11 | GC标记 |
注意:在偏向锁启用时,锁标志位为01;禁用时,无锁状态为001
3. Monitor:锁的实现核心
3.1 Monitor的基本结构
Monitor是synchronized的底层实现机制,每个Java对象都关联一个Monitor。在HotSpot虚拟机中,Monitor由ObjectMonitor实现,主要包含以下关键字段:
- _owner:指向持有锁的线程
- _EntryList:存放等待锁的线程
- _WaitSet:存放调用了wait()的线程
- _recursions:重入次数
- _count:计数器
3.2 锁的获取流程
当线程尝试获取锁时,会经历以下步骤:
- 通过CAS操作尝试将Mark Word替换为指向当前线程的指针
- 如果成功,获取锁
- 如果失败,进入_EntryList等待
- 当锁释放时,_EntryList中的线程会竞争获取锁
4. 锁升级:从偏向锁到重量级锁
4.1 锁的四种状态
Java中的锁会随着竞争情况从低到高升级:
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
4.2 偏向锁
偏向锁的设计初衷是减少无竞争情况下的同步开销。当一个线程第一次获取锁时:
- 通过CAS操作将线程ID写入Mark Word
- 设置偏向锁标志位为1
- 之后该线程再获取锁时,只需检查线程ID是否匹配
实际经验:偏向锁在明确知道会有多线程竞争的场景下应该禁用,可以通过-XX:-UseBiasedLocking关闭
4.3 轻量级锁
当第二个线程尝试获取锁时,偏向锁会升级为轻量级锁:
- JVM会在当前线程的栈帧中创建锁记录(Lock Record)
- 将对象头的Mark Word复制到锁记录中(Displaced Mark Word)
- 尝试通过CAS将对象头的Mark Word替换为指向锁记录的指针
- 如果成功,获取锁;如果失败,表示有竞争,开始自旋
4.4 重量级锁
当自旋超过一定次数(默认10次,可通过-XX:PreBlockSpin调整),轻量级锁会升级为重量级锁:
- 向操作系统申请互斥量(mutex)
- 线程进入阻塞状态
- 需要操作系统介入进行线程调度
5. 锁的优化实践
5.1 减少锁的粒度
一个常见的优化方法是缩小同步代码块的范围:
java复制// 不推荐
public synchronized void process() {
// 大量不需要同步的代码
// ...
// 少量需要同步的代码
}
// 推荐
public void process() {
// 大量不需要同步的代码
// ...
synchronized(this) {
// 少量需要同步的代码
}
}
5.2 锁分离技术
根据不同的操作使用不同的锁:
java复制class SeparateLockExample {
private final Object readLock = new Object();
private final Object writeLock = new Object();
public void read() {
synchronized(readLock) {
// 读操作
}
}
public void write() {
synchronized(writeLock) {
// 写操作
}
}
}
5.3 避免死锁
死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
避免死锁的策略:
- 按固定顺序获取锁
- 设置锁超时时间
- 使用tryLock()
6. 常见问题排查
6.1 锁竞争问题定位
使用jstack查看线程状态:
code复制"Thread-1" #11 prio=5 os_prio=0 tid=0x00007f48740e8000 nid=0x5e1e waiting for monitor entry [0x00007f486b7f6000]
java.lang.Thread.State: BLOCKED (on object monitor)
6.2 锁膨胀问题
通过-XX:+PrintFlagsFinal查看锁相关参数:
code复制intx BiasedLockingBulkRebiasThreshold = 20
intx BiasedLockingBulkRevokeThreshold = 40
6.3 性能调优参数
常用JVM锁参数:
| 参数 | 说明 |
|---|---|
| -XX:+UseBiasedLocking | 启用偏向锁(默认true) |
| -XX:BiasedLockingStartupDelay | 偏向锁启动延迟(默认4000ms) |
| -XX:+UseSpinning | 启用自旋(默认true) |
| -XX:PreBlockSpin | 自旋次数(默认10) |
7. 实际案例分析
7.1 高并发下单系统优化
在一个电商系统中,最初的实现是这样的:
java复制public synchronized void createOrder(Order order) {
// 订单创建逻辑
}
在高并发场景下,这个实现会导致大量线程阻塞。优化方案:
- 使用分段锁,按用户ID哈希分配不同的锁
- 将同步范围缩小到最小必要部分
- 对于库存扣减,使用乐观锁替代
7.2 缓存更新问题
考虑一个缓存更新场景:
java复制public synchronized void updateCache(String key, Object value) {
// 更新缓存
cache.put(key, value);
// 记录日志
log.info("Cache updated: {}", key);
}
问题在于日志记录也被包含在同步块中。优化方案:
java复制public void updateCache(String key, Object value) {
synchronized(this) {
cache.put(key, value);
}
log.info("Cache updated: {}", key);
}
8. 从字节码看synchronized
8.1 同步方法
同步方法会在方法访问标志中添加ACC_SYNCHRONIZED:
code复制public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
8.2 同步代码块
同步代码块会使用monitorenter和monitorexit指令:
code复制public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
9. 锁的替代方案
虽然synchronized是Java内置的同步机制,但在某些场景下,其他方案可能更合适:
9.1 ReentrantLock
特点:
- 可中断
- 公平锁选项
- 尝试获取锁
- 条件变量
9.2 StampedLock
适用于读多写少的场景,提供乐观读模式。
9.3 并发容器
如ConcurrentHashMap、CopyOnWriteArrayList等。
10. 性能对比与选型建议
10.1 不同锁的性能对比
| 锁类型 | 适用场景 | 特点 |
|---|---|---|
| 偏向锁 | 单线程访问 | 开销最小 |
| 轻量级锁 | 低竞争 | 自旋消耗CPU |
| 重量级锁 | 高竞争 | 线程阻塞开销大 |
| ReentrantLock | 需要高级功能 | 灵活但复杂 |
10.2 选型建议
- 明确并发场景:读多写少?写多读少?
- 评估竞争程度:低竞争考虑轻量级方案
- 考虑功能需求:是否需要可中断、公平性等
- 进行基准测试:用JMH进行性能测试
在实际项目中,我通常会先用synchronized实现功能,在性能测试阶段再根据结果决定是否需要优化。过早优化往往会导致代码复杂度增加,而收益不明显。
