1. synchronized 底层实现机制剖析
在 Java 并发编程中,synchronized 关键字是最基础的线程同步工具。但很多人只停留在"知道怎么用"的层面,对其底层实现原理一知半解。今天我们就来彻底拆解 HotSpot 虚拟机中 synchronized 的实现细节,包括对象头结构、锁升级全过程以及各阶段的性能差异。
1.1 对象内存布局与 Mark Word
每个 Java 对象在堆内存中的存储布局分为三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
其中对象头又包含:
- Mark Word(标记字段):存储对象自身的运行时数据
- Klass Pointer(类型指针):指向类元数据的指针
重点来看 Mark Word 的结构(以 64 位 JVM 为例):
| 锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit(偏向锁) | 2bit(锁标志) |
|---|---|---|---|---|---|---|
| 无锁 | 未使用 | 对象的 hashCode | 分代年龄 | 锁标志位 | 0 | 01 |
| 偏向锁 | 线程ID + Epoch | 时间戳 | 分代年龄 | 1 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
| 重量级锁 | 指向互斥量(Monitor)的指针 | 10 | ||||
| GC标记 | 空 | 11 |
这个结构会随着锁状态的变化而动态改变。通过最后2位的锁标志位,可以快速判断当前对象的锁状态。
1.2 Monitor 机制解析
当锁升级到重量级时,会使用 Monitor 对象(也称为管程或监视器锁)来实现同步。每个 Java 对象都关联一个 Monitor,但这个 Monitor 是在需要时才会创建(延迟初始化)。
Monitor 的主要组成部分:
- _owner:指向持有锁的线程
- _EntryList:存放等待锁的线程(阻塞状态)
- _WaitSet:存放调用 wait() 方法的线程(等待状态)
获取锁的流程:
- 通过 CAS 操作尝试将 _owner 设置为当前线程
- 如果成功则获取锁
- 如果失败则进入 _EntryList 等待
释放锁时会将 _owner 置为 null 并唤醒 _EntryList 中的线程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 锁升级全过程详解
HotSpot 虚拟机中 synchronized 的锁状态会按照以下路径升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
这个升级过程是不可逆的,称为锁膨胀。
2.1 偏向锁阶段
设计初衷:在无竞争情况下减少同步开销。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。
获取流程:
- 检查 Mark Word 中的线程ID是否指向当前线程
- 如果是,直接执行同步代码
- 如果不是,尝试通过 CAS 将 Mark Word 的线程ID替换为当前线程ID
- 成功:获取偏向锁
- 失败:说明存在竞争,开始撤销偏向锁
注意:偏向锁在 Java 15 后已废弃,需要通过 -XX:-UseBiasedLocking 禁用
2.2 轻量级锁阶段
当偏向锁撤销后,会升级为轻量级锁。适用于线程交替执行同步块的场景。
加锁过程:
- 在当前线程的栈帧中创建锁记录(Lock Record)
- 将对象头中的 Mark Word 复制到锁记录中(Displaced Mark Word)
- 尝试用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针
- 成功:获取轻量级锁
- 失败:检查是否指向当前线程的栈帧
- 是:直接执行(重入)
- 否:说明存在竞争,开始膨胀为重量级锁
解锁过程:
- 用 CAS 将 Displaced Mark Word 替换回对象头
- 成功:同步完成
- 失败:说明已膨胀为重量级锁,进入重量级锁释放流程
2.3 重量级锁阶段
当多个线程同时竞争同一锁时,会升级为重量级锁。此时会使用操作系统的互斥量来实现同步,涉及到用户态到内核态的切换,性能开销最大。
特点:
- 线程阻塞和唤醒需要操作系统介入
- 存在上下文切换的开销
- 适用于高并发竞争场景
3. 锁升级实战案例分析
3.1 查看对象头工具
使用 JOL (Java Object Layout) 工具可以查看对象内存布局:
java复制// 添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
// 使用示例
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
输出示例(64位 JVM):
code复制java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
3.2 锁竞争性能测试
对比不同锁状态下的性能差异:
java复制public class LockBenchmark {
private static final int THREADS = 4;
private static final int ITERATIONS = 10000000;
public static void main(String[] args) throws Exception {
// 测试无竞争场景(偏向锁)
testLock(false);
// 测试轻度竞争场景(轻量级锁)
testLock(true);
}
private static void testLock(boolean contention) throws Exception {
Object lock = new Object();
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
synchronized (lock) {
if (contention && j % 100 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
System.out.printf("%s: %dms\n",
contention ? "Contended" : "No contention",
System.currentTimeMillis() - start);
}
}
典型输出:
code复制No contention: 482ms // 偏向锁/轻量级锁
Contended: 3247ms // 重量级锁
4. 常见问题与优化建议
4.1 锁升级过程中的性能陷阱
- 偏向锁延迟问题
- 新创建的对象默认是可偏向的,但偏向锁的启用有约4秒延迟
- 可以通过 -XX:BiasedLockingStartupDelay=0 取消延迟
- 批量重偏向与撤销
- 当一类对象的偏向锁被撤销超过20次时,会触发批量重偏向
- 当一类对象的偏向锁被撤销超过40次时,会禁用该类的偏向锁
- 自旋优化
- 轻量级锁失败后,线程会自旋尝试获取锁(自适应自旋)
- 自旋次数由JVM自动调整(-XX:PreBlockSpin已废弃)
4.2 锁优化实践
- 减少锁粒度
- 将大锁拆分为多个小锁(如ConcurrentHashMap的分段锁)
- 锁分离
- 读写分离(ReentrantReadWriteLock)
- 分离热点数据
- 无锁编程
- 使用CAS操作(AtomicInteger等)
- LongAdder替代AtomicLong
- 避免锁升级
- 对于明确会有竞争的锁,直接使用重量级锁
- 使用 -XX:-UseBiasedLocking 禁用偏向锁
4.3 锁诊断工具
- jstack
- 查看线程堆栈和锁持有情况
- 检测死锁
- JFR (Java Flight Recorder)
- 记录锁竞争事件
- 分析等待时间
- async-profiler
- 低开销的锁分析工具
- 可视化展示锁竞争热点
5. 现代JVM的锁优化趋势
随着Java版本的演进,锁机制也在不断优化:
- Java 15:默认禁用偏向锁
- 由于现代应用大多是多线程环境,偏向锁的收益降低
- 通过 -XX:+UseBiasedLocking 仍可启用
- Java 16:弹性元空间
- 减少元数据的内存占用
- 间接影响Monitor的创建开销
- Loom项目:虚拟线程
- 轻量级线程减少上下文切换开销
- 可能改变现有的锁竞争模式
在实际开发中,建议:
- 优先考虑无锁数据结构
- 必要时使用显式锁(ReentrantLock)
- 用并发工具类替代低级的同步块
- 通过压测确定最佳同步策略
