1. 引言
1.1 什么是synchronized
synchronized 是 Java 提供的一种内置的同步机制,用于解决多线程并发访问共享资源时的线程安全问题。在 JDK 1.6 之前,synchronized 是一个"重量级"锁,其性能较差,因为它直接依赖操作系统的互斥量(Mutex)来实现,涉及用户态和内核态的切换。
1.2 为什么需要锁升级
为了提升 synchronized 的性能,JDK 1.6 引入了锁升级机制 (也称为锁膨胀机制)。锁升级的核心思想是:根据竞争情况动态调整锁的实现策略,从轻量级到重量级逐步升级。
这种设计基于以下观察:
- 大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得
- 即使有竞争,竞争也往往是短暂的,线程自旋等待即可获得锁
通过锁升级机制,JVM 可以在不同的竞争场景下使用最合适的同步策略,从而显著提升性能。
2. Java对象内存布局
2.1 对象的内存结构
在 HotSpot 虚拟机中,每个 Java 对象在内存中的布局分为三个部分:

| 组成部分 | 说明 | 大小 |
|---|---|---|
| 对象头(Object Header) | 存储对象自身的运行时数据,包括 Mark Word 和类型指针 | 8/12/16 字节(取决于是否开启指针压缩) |
| 实例数据(Instance Data) | 对象真正存储的有效信息,即各个字段的内容 | 不固定 |
| 对齐填充(Padding) | 占位符,保证对象大小是 8 字节的整数倍 | 0-7 字节 |
2.2 对象头结构
对象头是实现 synchronized 锁的关键,它包含两部分:
- Mark Word(标记字):存储对象自身的运行时数据
- Klass Pointer(类型指针):指向对象的类元数据
对于数组对象,还会包含数组长度信息。
2.2.1 Mark Word 详解
Mark Word 是实现锁升级的核心数据结构。在 64 位 JVM 中,Mark Word 占 64 位(8 字节),在 32 位 JVM 中占 32 位(4 字节)。

32 位 JVM 的 Mark Word 结构:
| 锁状态 | 25 bit | 4 bit | 1 bit (是否偏向锁) | 2 bit (锁标志位) |
|---|---|---|---|---|
| 无锁 | 对象的 hashCode | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程 ID (23 bit) + Epoch (2 bit) | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||
| GC 标记 | 空 | 11 |

64 位 JVM 的 Mark Word 结构:
| 锁状态 | 56 bit | 1 bit | 4 bit | 1 bit (是否偏向锁) | 2 bit (锁标志位) |
|---|---|---|---|---|---|
| 无锁 | unused (25 bit) + hashCode (31 bit) | unused | 分代年龄 | 0 | 01 |
| 偏向锁 | 线程 ID (54 bit) + Epoch (2 bit) | unused | 分代年龄 | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 (62 bit) | 00 | |||
| 重量级锁 | 指向 ObjectMonitor 的指针 (62 bit) | 10 | |||
| GC 标记 | 空 | 11 |
锁标志位说明:
| 锁标志位 | 偏向锁位 | 锁状态 |
|---|---|---|
| 01 | 0 | 无锁状态(Normal) |
| 01 | 1 | 偏向锁状态(Biased) |
| 00 | - | 轻量级锁状态(Lightweight Locked) |
| 10 | - | 重量级锁状态(Heavyweight Locked) |
| 11 | - | GC 标记状态 |
注意:指针压缩的影响
以上 Mark Word 布局假设未启用指针压缩。在 64 位 JVM 中,默认启用指针压缩(
-XX:+UseCompressedOops),此时对象头的 Klass Pointer 会从 8 字节压缩为 4 字节。这会影响对象整体大小,但 Mark Word 本身的布局保持不变。使用 JOL 工具验证时,需注意指针压缩对对象布局的实际影响。
3. 锁的四种状态
synchronized 锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁。
3.1 锁状态对比表
| 锁状态 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 无锁 | 无同步开销 | 无法保证线程安全 | 单线程环境 |
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗 CPU | 追求响应时间,同步块执行速度非常快,线程交替执行同步块 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行时间较长 |
3.2 锁的演进方向
重要特性:锁可以升级但不能降级
scss
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
↑ ↑ ↑ ↑
(01,0) (01,1) (00) (10)
这种"只能升级不能降级"的策略是为了提高获得锁和释放锁的效率。
JDK 版本演进说明
自 JDK 15 起(JEP 374),偏向锁默认禁用,原因包括:维护成本高、现代应用多为高并发场景导致收益有限、与虚拟线程(JDK 21)存在兼容性问题。JDK 18+ 中相关 JVM 参数已标记为"obsolete"(过时),但截至 JDK 25,偏向锁代码仍保留在 JVM 中以兼容旧应用(尚未完全移除)。详见 [Section 9.3](#Section 9.3 "#93-jdk-%E7%89%88%E6%9C%AC%E5%B7%AE%E5%BC%82")。
4. 锁升级的完整流程
4.1 锁升级流程图
4.2 详细升级过程
4.2.1 无锁 → 偏向锁
- 检查对象头:线程访问同步块时,检查 Mark Word 的锁标志位和偏向锁标志
- 判断是否可偏向 :
- 如果是可偏向状态(偏向锁标志为 1)且 ThreadID 为空
- 使用 CAS 操作尝试将当前线程 ID 写入 Mark Word
- 获取偏向锁成功:CAS 成功后,以后该线程进入同步块时只需简单测试 Mark Word 中是否存储着指向当前线程的偏向锁
4.2.2 偏向锁 → 轻量级锁
当另一个线程尝试获取已被偏向的锁时:
- 检测到竞争:发现 Mark Word 中的线程 ID 不是自己
- 暂停偏向线程:到达全局安全点(Safepoint),暂停持有偏向锁的线程
- 检查偏向线程状态 :
- 如果线程已经退出同步块或不再存活:直接撤销偏向锁,变为无锁状态
- 如果线程仍在同步块中:将偏向锁升级为轻量级锁
- 恢复线程执行
4.2.3 轻量级锁 → 重量级锁
当自旋达到一定次数仍未获得锁时:
- 自旋失败:线程自旋等待锁的次数超过阈值(JDK 6+ 默认启用自适应自旋,由 JVM 动态调整)
- 膨胀为重量级锁:将轻量级锁膨胀为重量级锁
- 线程阻塞:未获得锁的线程进入阻塞状态,等待被唤醒
注意 :早期版本可通过
-XX:PreBlockSpin参数设置固定自旋次数(默认 10 次),但该参数在现代 JDK 中已被自适应自旋取代,不再生效。
5. 偏向锁的实现原理
5.1 偏向锁的设计理念
偏向锁的核心思想:锁不仅不存在多线程竞争,而且总是由同一线程多次获得。
在这种情况下,锁的获取和释放连 CAS 操作都不需要,只需简单地测试 Mark Word 中是否存储着指向当前线程的偏向锁,大大提高性能。
5.2 偏向锁的获取流程
偏向锁标志是否为 1?"} CheckBias --> |否| OtherLock["使用其他锁策略"] CheckBias --> |是| CheckThread{"ThreadID == 当前线程 ID?"} CheckThread --> |是| DirectExec["直接执行同步块"] CheckThread --> |否| CheckAnonymous{"ThreadID == 0
匿名偏向?"} CheckAnonymous --> |是| CASReplace["CAS 替换 ThreadID"] CheckAnonymous --> |否| Revoke["撤销偏向锁或锁升级"] CASReplace --> CASSuccess{"成功?"} CASSuccess --> |是| ExecSync["执行同步代码块"] CASSuccess --> |否| Upgrade["锁升级"]
5.3 偏向锁的撤销
偏向锁的撤销是一个相对昂贵的操作,需要等待全局安全点(Safepoint)。
撤销流程:
- 到达安全点:暂停持有偏向锁的线程
- 检查线程状态 :
- 线程未活动或已退出同步块:直接将 Mark Word 改为无锁状态(01,0)
- 线程仍在同步块中:将偏向锁升级为轻量级锁
- 恢复线程执行
批量重偏向和批量撤销:
为了优化偏向锁撤销的性能,JVM 引入了批量重偏向和批量撤销机制:
| 机制 | 触发条件 | 处理方式 |
|---|---|---|
| 批量重偏向 | 一个类的对象被多个线程访问,但不存在竞争 | 将该类的偏向锁指向新的线程 |
| 批量撤销 | 某个类的撤销次数达到阈值(默认 40) | 将该类的所有对象都改为不可偏向 |
5.4 相关 JVM 参数
| 参数 | 说明 | 默认值 | 状态 |
|---|---|---|---|
-XX:+UseBiasedLocking |
启用偏向锁 | JDK 6-14 默认启用,JDK 15+ 默认禁用 | JDK 18+ obsolete |
-XX:BiasedLockingStartupDelay |
偏向锁延迟启动时间(毫秒) | 4000ms(见下文说明) | JDK 18+ obsolete |
-XX:BiasedLockingBulkRebiasThreshold |
批量重偏向阈值 | 20 | JDK 18+ obsolete |
-XX:BiasedLockingBulkRevokeThreshold |
批量撤销阈值 | 40 | JDK 18+ obsolete |
JDK 18+ 参数状态说明
自 JDK 18 起,上述偏向锁相关参数已标记为 obsolete(过时),在命令行使用时会输出警告信息并被忽略。这是 JEP 374 废弃偏向锁计划的延续。
为什么偏向锁要延迟 4 秒启动?
JVM 在启动时会创建大量内部对象和线程(如 Finalizer 线程、Reference Handler 等),这些初始化操作会涉及很多同步操作。如果此时就启用偏向锁,反而会因为频繁的偏向锁撤销而降低性能。因此 JVM 默认延迟 4 秒后才启用偏向锁,此时应用程序已经完成初始化,可以正常享受偏向锁带来的性能提升。
5.5 使用 JOL 验证锁状态
可以使用 JOL(Java Object Layout)工具观察对象头的变化:
xml
<!-- Maven 依赖 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
java
import org.openjdk.jol.info.ClassLayout;
public class LockStateDemo {
public static void main(String[] args) throws InterruptedException {
// 等待偏向锁延迟启动(或使用 -XX:BiasedLockingStartupDelay=0)
Thread.sleep(5000);
Object lock = new Object();
// 1. 查看初始状态(可偏向/匿名偏向)
System.out.println("===== 初始状态 =====");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// 2. 加锁后查看(偏向锁)
synchronized (lock) {
System.out.println("===== 偏向锁状态 =====");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
// 3. 另一个线程竞争锁(升级为轻量级锁或重量级锁)
Thread t = new Thread(() -> {
synchronized (lock) {
System.out.println("===== 竞争后状态 =====");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
});
t.start();
t.join();
}
}
运行参数:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0:立即启用偏向锁-XX:-UseBiasedLocking:禁用偏向锁,直接使用轻量级锁
6. 轻量级锁的实现原理
6.1 轻量级锁的设计理念
轻量级锁的核心思想:对于绝大部分的锁,在整个同步周期内都是不存在竞争的。
轻量级锁通过 CAS(Compare-And-Swap) 操作和 自旋 来避免线程阻塞和唤醒的开销。
6.2 轻量级锁的加锁流程
Lock Record 锁记录"] CreateRecord --> CopyMark["将对象的 Mark Word
复制到 Lock Record
Displaced Mark Word"] CopyMark --> CAS["使用 CAS 尝试将对象的
Mark Word 替换为指向
Lock Record 的指针"] CAS --> |成功| Success["获取锁成功
执行同步块"] CAS --> |失败| CheckOwner{"检查 Mark Word
是否指向当前线程的栈帧?"} CheckOwner --> |是| Reentrant["锁重入"] CheckOwner --> |否| Inflate["锁膨胀为重量级锁"]
详细步骤:
-
创建 Lock Record:在当前线程的栈帧中创建锁记录(Lock Record)空间,用于存储对象当前的 Mark Word 拷贝(官方称为 Displaced Mark Word)
-
复制 Mark Word:将对象的 Mark Word 复制到锁记录中
-
CAS 替换:使用 CAS 操作尝试将对象的 Mark Word 替换为指向锁记录的指针
- 成功:表示当前线程获得了锁,Mark Word 的锁标志位变为 00
- 失败:检查对象的 Mark Word 是否指向当前线程的栈帧
- 是:表示当前线程已经拥有该对象的锁(锁重入),直接执行同步代码
- 否:表示存在竞争,轻量级锁需要膨胀为重量级锁
6.3 轻量级锁的解锁流程
Mark Word 替换回对象头"] CASRestore --> |成功| Success["解锁成功"] CASRestore --> |失败| HeavyUnlock["锁已膨胀为重量级
执行重量级锁解锁流程"]
详细步骤:
- CAS 恢复 :使用 CAS 操作将 Displaced Mark Word 替换回对象头
- 成功:表示没有竞争发生,解锁成功
- 失败:表示存在竞争,锁已经膨胀为重量级锁,需要释放锁并唤醒等待的线程
6.4 自旋优化
为了避免线程频繁挂起和恢复,轻量级锁引入了 自旋(Spinning) 机制。
自旋策略:
| 策略类型 | 说明 | JVM 参数 | 状态 |
|---|---|---|---|
| 固定次数自旋 | 自旋固定次数(默认 10 次) | -XX:PreBlockSpin=10 |
JDK 9+ 已移除 |
| 自适应自旋 | 根据前一次在同一个锁上的自旋时间和锁拥有者的状态动态调整 | JDK 6+ 默认启用,无需配置 | 现代 JDK 唯一策略 |
注意 :
-XX:PreBlockSpin参数在 JDK 9 及更高版本中已被移除,现代 JVM 完全依赖自适应自旋,由 JIT 编译器根据运行时数据动态优化自旋行为。
自适应自旋的优化逻辑:
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么 JVM 会认为这次自旋也很有可能再次成功,因此允许自旋等待更长的时间
- 如果对于某个锁,自旋很少成功获得过,那么以后获取这个锁时将可能省略掉自旋过程,以避免浪费 CPU 资源
7. 重量级锁的实现原理
7.1 重量级锁的设计理念
当轻量级锁的自旋达到一定次数仍未获得锁,或者有线程在等待锁时,轻量级锁会膨胀为重量级锁。
重量级锁依赖操作系统的 互斥量(Mutex Lock) 实现,会导致线程在用户态和内核态之间切换,开销较大。
7.2 ObjectMonitor 对象
在 HotSpot 虚拟机中,重量级锁通过 ObjectMonitor 对象实现。每个对象都可以关联一个 ObjectMonitor 对象。
ObjectMonitor 的关键字段:
cpp
ObjectMonitor() {
_header = NULL; // displaced object header word
_count = 0; // 记录个数
_waiters = 0; // 等待线程数
_recursions = 0; // 重入次数
_object = NULL; // 关联的对象
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 等待队列(wait()方法)
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 竞争队列
_EntryList = NULL; // 入口队列
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
核心数据结构:
| 字段 | 说明 |
|---|---|
_owner |
指向持有 ObjectMonitor 对象的线程 |
_EntryList |
处于等待锁阻塞状态的线程队列(BLOCKED) |
_WaitSet |
调用 wait() 方法后等待的线程队列(WAITING) |
_recursions |
锁的重入次数 |
_count |
用于辅助计数 |
7.3 重量级锁的加锁流程
字段是否为空?"} CheckOwner --> |是| CASSet["CAS 设置
_owner 为当前线程"] CheckOwner --> |否| CheckCurrent{"检查是否为
当前线程?"} CASSet --> |成功| Success["获取锁成功"] CheckCurrent --> |是| Reentrant["锁重入
_recursions++"] CheckCurrent --> |否| EnterQueue["加入 _EntryList
或 _cxq 队列"] EnterQueue --> Park["阻塞等待
park"] Park --> Wakeup["被唤醒后重新竞争锁"]
7.4 重量级锁的解锁流程
是否大于 0?"} CheckRecursions --> |是| Decrease["_recursions--"] CheckRecursions --> |否| SetNull["将 _owner 设置为 NULL"] SetNull --> Notify["唤醒 _EntryList
或 _cxq 中的一个线程"]
7.5 wait/notify 机制
重量级锁还支持 wait()、notify()、notifyAll() 方法,这些方法的实现依赖 ObjectMonitor。
wait() 流程:
- 线程调用
wait()方法 - 释放持有的锁(
_owner置为 NULL,_recursions置为 0) - 线程进入
_WaitSet队列,状态变为 WAITING - 线程被阻塞
notify() 流程:
- 线程调用
notify()方法 - 从
_WaitSet中取出一个线程 - 将该线程移到
_EntryList或_cxq队列 - 线程状态从 WAITING 变为 BLOCKED
- 该线程等待重新竞争锁
线程状态转换图:
注意 :
wait()被唤醒后线程先进入 BLOCKED 状态等待重新获取锁,获取成功后才变为 RUNNABLE。
8. 锁优化技术
除了锁升级机制,JVM 还提供了多种锁优化技术来提升 synchronized 的性能。
8.1 锁消除(Lock Elimination)
原理: JIT 编译器在运行时,通过逃逸分析,如果判断一段代码中,堆上的所有数据都不会逃逸出去被其他线程访问到,就可以将它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
示例:
java
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
StringBuffer 的 append() 方法是同步方法,但在这个例子中,sb 对象不会逃逸出方法外,因此 JIT 编译器会自动消除 StringBuffer 内部的同步锁。
JVM 参数:
-XX:+DoEscapeAnalysis(默认启用):启用逃逸分析-XX:+EliminateLocks(默认启用):启用锁消除
8.2 锁粗化(Lock Coarsening)
原理: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,JIT 编译器会将加锁同步的范围扩展(粗化)到整个操作序列的外部。
优化前:
java
public void method() {
synchronized(lock) {
// 操作1
}
synchronized(lock) {
// 操作2
}
synchronized(lock) {
// 操作3
}
}
优化后:
java
public void method() {
synchronized(lock) {
// 操作1
// 操作2
// 操作3
}
}
循环中的锁粗化:
java
// 优化前
for (int i = 0; i < 1000; i++) {
synchronized(lock) {
// 操作
}
}
// 优化后
synchronized(lock) {
for (int i = 0; i < 1000; i++) {
// 操作
}
}
8.3 锁的内存语义
synchronized 除了保证原子性,还保证了可见性和有序性。
内存语义:
| 操作 | 内存语义 |
|---|---|
| 加锁(monitorenter) | 清空工作内存中共享变量的值,从主内存中重新读取 |
| 解锁(monitorexit) | 将工作内存中共享变量的值刷新到主内存 |
这保证了:
- 可见性:一个线程释放锁后,所有的修改对后续获得该锁的线程可见
- 有序性:禁止 JVM 和处理器对监视器内的代码进行重排序优化
happens-before 规则:
- 对一个监视器的解锁,happens-before 于随后对这个监视器的加锁
9. 性能对比与使用场景
9.1 性能对比
不同锁状态下的性能差异(相对时间,仅供参考):
| 锁状态 | 获取锁耗时 | 相对性能 |
|---|---|---|
| 无锁 | - | 基准(1x) |
| 偏向锁 | ~10 ns | 1.05x |
| 轻量级锁 | ~100 ns | 10x |
| 重量级锁 | ~1000 ns - 10 μs | 100x - 10000x |
注意: 上表数据为参考估算值,实际性能取决于多种因素:
- CPU 核心数和架构
- 线程数和竞争激烈程度
- 同步块执行时间
- JVM 版本和 JIT 编译优化程度
基准测试建议
建议使用 JMH(Java Microbenchmark Harness) 进行精确的性能测试。JMH 是 OpenJDK 官方提供的基准测试框架,能够有效避免 JIT 优化、死代码消除等因素对测试结果的干扰,获得更可靠的性能数据。
9.2 使用场景建议
| 场景 | 推荐锁类型 | 原因 |
|---|---|---|
| 单线程或基本无竞争 | 偏向锁 | 几乎无开销,适合单线程多次进入同步块 |
| 少量线程,竞争不激烈,同步块执行快 | 轻量级锁 | 自旋等待即可获得锁,避免线程阻塞 |
| 多线程,竞争激烈,同步块执行慢 | 重量级锁 | 避免 CPU 空转,使用操作系统调度 |
| 高度竞争的写操作 | 考虑使用 Lock 或并发容器 | 提供更灵活的控制和更好的性能 |
9.3 JDK 版本差异
| JDK 版本 | 偏向锁状态 | 说明 |
|---|---|---|
| JDK 6 | 引入偏向锁 | 默认启用,延迟 4 秒启动 |
| JDK 7-14 | 默认启用 | 性能优化的重要特性 |
| JDK 15 | 默认禁用(JEP 374) | 可通过 -XX:+UseBiasedLocking 手动启用 |
| JDK 18-20 | 参数过时(obsolete) | 启用参数被忽略,输出警告信息 |
| JDK 21 | 虚拟线程引入(JEP 444) | 偏向锁仍禁用,虚拟线程改变同步语义 |
| JDK 22-25 | 代码保留,功能禁用 | 偏向锁代码仍在 JVM 中,但未移除,以兼容旧应用 |
禁用偏向锁的原因(JEP 374):
- 维护成本高:批量重偏向、批量撤销机制增加了代码复杂度
- 收益有限:现代应用多为高并发场景,单线程重复获取锁的场景减少
- 启动延迟问题:偏向锁延迟 4 秒启动,影响某些应用的启动性能
- 与新特性冲突:偏向锁与 Project Loom(虚拟线程)的设计存在兼容性问题
关于偏向锁移除计划
截至 JDK 25(2025 年),偏向锁代码尚未从 HotSpot JVM 中完全移除。OpenJDK 团队采取了保守策略,保留代码以兼容可能依赖偏向锁行为的旧应用。但请注意,偏向锁功能已被禁用且不建议使用。
9.4 虚拟线程与 synchronized
JDK 21 正式引入虚拟线程(Virtual Threads,JEP 444),这对 synchronized 的使用有重要影响:
| 特性 | 平台线程 | 虚拟线程 |
|---|---|---|
| synchronized 阻塞行为 | 阻塞 OS 线程 | 固定(pin) 载体线程 |
| 重量级锁开销 | 用户态/内核态切换 | 可能导致载体线程被占用 |
| 推荐替代方案 | - | ReentrantLock 或其他 java.util.concurrent 锁 |
虚拟线程中的最佳实践:
- 避免在虚拟线程中长时间持有 synchronized 锁
- 对于可能阻塞的操作,优先使用
ReentrantLock - 使用
-Djdk.tracePinnedThreads=full诊断固定问题
什么是"固定"(Pinning)?
当虚拟线程在 synchronized 块中阻塞时,底层的载体(平台)线程无法被释放去执行其他虚拟线程,这被称为"固定"。这会降低虚拟线程的可伸缩性优势,因此在高并发虚拟线程应用中应尽量避免使用 synchronized。
10. 源码分析
版本说明
本节源码分析基于 OpenJDK 8。在更高版本 JDK 中,部分实现细节可能有所不同(如偏向锁相关代码在 JDK 15+ 中虽然保留但已默认禁用)。建议结合目标 JDK 版本的源码进行对照阅读。
10.1 核心源码位置
在 OpenJDK 8 中,synchronized 相关的核心源码位于以下位置:
| 文件路径 | 说明 |
|---|---|
hotspot/src/share/vm/runtime/synchronizer.cpp |
同步器实现,锁的获取和释放 |
hotspot/src/share/vm/runtime/objectMonitor.cpp |
ObjectMonitor 实现 |
hotspot/src/share/vm/runtime/biasedLocking.cpp |
偏向锁实现 |
hotspot/src/share/vm/oops/markOop.hpp |
Mark Word 定义 |
hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp |
字节码解释器,monitorenter/monitorexit |
10.2 Mark Word 定义
cpp
// hotspot/src/share/vm/oops/markOop.hpp
enum {
locked_value = 0, // 00 轻量级锁
unlocked_value = 1, // 01 无锁或偏向锁
monitor_value = 2, // 10 重量级锁
marked_value = 3, // 11 GC 标记
biased_lock_pattern = 5 // 101 偏向锁
};
10.3 偏向锁核心代码
cpp
// hotspot/src/share/vm/runtime/biasedLocking.cpp
// 偏向锁的撤销与重偏向操作
// 返回值类型 Condition 表示操作结果状态
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(
Handle obj, bool attempt_rebias, TRAPS) {
// 断言:此操作不能在安全点执行
// 因为需要与其他线程交互,安全点会导致死锁
assert(!SafepointSynchronize::is_at_safepoint(), "must not be at safepoint");
// 获取对象的 Mark Word
markOop mark = obj->mark();
// 步骤1:检查是否为偏向锁模式(锁标志位 01,偏向位 1)
if (!mark->has_bias_pattern()) {
// 不是偏向模式,可能是无锁、轻量级锁或重量级锁
return NOT_BIASED;
}
// 步骤2:获取当前偏向的线程 ID(存储在 Mark Word 中)
JavaThread* biased_thread = mark->biased_locker();
// 步骤3:检查是否偏向当前线程
if (biased_thread == THREAD) {
// 已经偏向当前线程,无需任何操作,直接返回
return BIAS_REVOKED;
}
// 步骤4:偏向锁指向其他线程,需要撤销
if (biased_thread != NULL) {
// 偏向锁已经偏向其他线程
// 必须等待安全点(Safepoint)才能撤销,以保证线程安全
// VM_RevokeBias 是一个 VM 操作,会在安全点执行
VM_RevokeBias op(obj, attempt_rebias);
VMThread::execute(&op); // 提交到 VM 线程执行
return op.status_code();
}
// 步骤5:匿名偏向状态(ThreadID == 0),尝试 CAS 偏向当前线程
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
// CAS 操作:尝试将 Mark Word 从匿名偏向改为偏向当前线程
markOop res_mark = obj->cas_set_mark(biased_value, unbiased_prototype);
if (res_mark == unbiased_prototype) {
return BIAS_REVOKED;
}
return NOT_REVOKED;
}
10.4 轻量级锁核心代码
cpp
// hotspot/src/share/vm/runtime/synchronizer.cpp
// 同步器的快速入口和慢速入口实现
// 快速入口:优先尝试偏向锁
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
bool attempt_rebias, TRAPS) {
// 检查是否启用偏向锁(JDK 15+ 默认禁用)
if (UseBiasedLocking) {
// 如果启用偏向锁,先尝试偏向锁路径
if (!SafepointSynchronize::is_at_safepoint()) {
// 非安全点:尝试撤销并重偏向
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(
obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
// 重偏向成功,直接返回
return;
}
} else {
// 安全点:直接撤销偏向锁
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 偏向锁路径失败或未启用,进入轻量级锁路径
slow_enter(obj, lock, THREAD);
}
// 慢速入口:轻量级锁获取逻辑
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
// 获取对象当前的 Mark Word
markOop mark = obj->mark();
// 情况1:无锁状态(is_neutral() 检查锁标志位为 01 且偏向位为 0)
if (mark->is_neutral()) {
// 步骤1:将原始 Mark Word 保存到栈上的 Lock Record(Displaced Mark Word)
lock->set_displaced_header(mark);
// 步骤2:CAS 尝试将对象头替换为指向 Lock Record 的指针
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
// CAS 成功:成功获取轻量级锁,锁标志位变为 00
return;
}
// CAS 失败:说明有其他线程竞争,继续下面的流程
}
// 情况2:轻量级锁重入检测
else if (mark->has_locker() &&
THREAD->is_lock_owned((address)mark->locker())) {
// Mark Word 指向当前线程的栈帧,说明是锁重入
// 重入时 Displaced Mark Word 设为 NULL(作为重入计数的标记)
lock->set_displaced_header(NULL);
return;
}
// 情况3:存在竞争,需要膨胀为重量级锁
// 设置特殊标记,表示需要膨胀
lock->set_displaced_header(markOopDesc::unused_mark());
// inflate() 方法创建或获取 ObjectMonitor,然后调用 enter() 获取锁
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)
->enter(THREAD);
}
10.5 重量级锁核心代码
cpp
// hotspot/src/share/vm/runtime/objectMonitor.cpp
// ObjectMonitor 是重量级锁的核心实现,每个被重量级锁保护的对象都关联一个 ObjectMonitor
// enter() 方法:尝试获取重量级锁
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD; // 当前线程
// 快速路径:尝试 CAS 获取锁(_owner 从 NULL 变为当前线程)
void * cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL);
if (cur == NULL) {
// CAS 成功:锁空闲,当前线程成功获取锁
return;
}
// 检查是否为锁重入(当前线程已经持有锁)
if (cur == Self) {
// 重入:增加重入计数
_recursions++;
return;
}
// 特殊情况:当前线程曾经通过轻量级锁持有过这个锁
// (从轻量级锁膨胀而来的情况)
if (Self->is_lock_owned((address)cur)) {
_recursions = 1; // 设置重入次数为 1
_owner = Self; // 更新所有者为当前线程
return;
}
// 慢速路径:无法快速获取锁,进入等待队列
EnterI(THREAD);
}
// EnterI() 方法:将线程加入等待队列并阻塞
void ObjectMonitor::EnterI(TRAPS) {
Thread * const Self = THREAD;
// 先尝试自旋获取锁(自适应自旋,由 JVM 动态调整次数)
if (TrySpin(Self) > 0) {
// 自旋成功获取锁
return;
}
// 自旋失败,准备加入等待队列
// 创建等待节点(ObjectWaiter)表示当前线程
ObjectWaiter node(Self);
Self->_ParkEvent->reset(); // 重置 park 事件
node._prev = (ObjectWaiter *) 0xBAD; // 哨兵值,用于调试
node.TState = ObjectWaiter::TS_CXQ; // 设置状态为在 CXQ 队列中
// CAS 循环:将节点加入 _cxq(Contention Queue)队列头部
// _cxq 是一个 LIFO 栈结构,用于存放竞争锁的线程
ObjectWaiter * nxt;
for (;;) {
node._next = nxt = _cxq;
// CAS:尝试将 _cxq 头指针指向新节点
if (Atomic::cmpxchg_ptr(&node, &_cxq, nxt) == nxt) break;
// CAS 失败则重试
}
// 主等待循环:阻塞等待被唤醒,然后尝试获取锁
for (;;) {
// 被唤醒后先尝试获取锁
if (TryLock(Self) > 0) break;
// 阻塞等待(使用 OS 原语)
if (_Responsible == Self || (SyncFlags & 1)) {
// 如果是"负责线程",使用带超时的 park,定期唤醒检查
Self->_ParkEvent->park((jlong) RecheckInterval);
} else {
// 普通线程,无限期等待
Self->_ParkEvent->park();
}
// 被唤醒后再次尝试获取锁
if (TryLock(Self) > 0) break;
// 获取失败则继续等待
}
// 成功获取锁,从等待队列中移除自己
UnlinkAfterAcquire(Self, &node);
// 清除 successor 标记(如果有)
if (_succ == Self) _succ = NULL;
}
11. 总结
11.1 核心要点
-
锁升级机制:synchronized 通过无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级路径,在不同竞争场景下使用最合适的同步策略
-
对象头是关键:Mark Word 存储了锁的状态信息,是实现锁升级的核心数据结构
-
性能权衡:
- 偏向锁:适合单线程场景,几乎无开销
- 轻量级锁:适合竞争不激烈且同步块执行快的场景,通过 CAS 和自旋避免阻塞
- 重量级锁:适合竞争激烈或同步块执行慢的场景,依赖操作系统调度
-
只能升级不能降级:锁状态只能从低到高升级,不能降级,这是为了提高效率的设计权衡
-
JVM 持续优化:除了锁升级,JVM 还提供了锁消除、锁粗化等优化技术
11.2 技术演进
| 时期 | synchronized 实现 | 特点 |
|---|---|---|
| JDK 1.6 之前 | 重量级锁 | 直接使用操作系统互斥量,性能较差 |
| JDK 1.6 | 引入锁升级机制 | 偏向锁、轻量级锁、重量级锁,性能大幅提升 |
| JDK 1.7-1.14 | 持续优化 | 自适应自旋、锁消除、锁粗化等优化技术 |
| JDK 15-17 | 禁用偏向锁(JEP 374) | 偏向锁默认关闭,可手动启用 |
| JDK 18-20 | 偏向锁参数过时 | -XX:+UseBiasedLocking 等参数被标记为 obsolete |
| JDK 21+ | 虚拟线程时代(JEP 444) | synchronized 在虚拟线程中会导致固定(pinning),推荐使用 ReentrantLock |
JDK 锁机制演进总览:
| JDK 版本 | 关键变化 | 影响 |
|---|---|---|
| 6-14 | 偏向锁默认启用 | 低争用场景性能提升 |
| 15 | 默认禁用,参数弃用 | 简化 JVM,聚焦高并发场景 |
| 18 | 参数过时(obsolete) | 启用尝试被忽略 |
| 21-25 | 虚拟线程引入,偏向锁代码保留但禁用 | synchronized 语义在虚拟线程下变化,偏向锁仍禁用 |
11.3 相关 JVM 参数速查表
| 参数 | 说明 | 默认值 | 状态(JDK 21+) |
|---|---|---|---|
-XX:+UseBiasedLocking |
启用偏向锁 | JDK 6-14: 开启 JDK 15+: 关闭 | obsolete |
-XX:BiasedLockingStartupDelay |
偏向锁延迟启动时间(ms) | 4000 | obsolete |
-XX:+UseSpinning |
启用自旋锁 | JDK 6-7 需显式启用 JDK 8+ 默认开启 | 已移除 |
-XX:PreBlockSpin |
自旋次数 | 10 | 已移除(JDK 9+) |
-XX:+DoEscapeAnalysis |
启用逃逸分析 | 默认开启 | 正常 |
-XX:+EliminateLocks |
启用锁消除 | 默认开启 | 正常 |
-XX:+PrintBiasedLockingStatistics |
打印偏向锁统计信息 | 关闭 | obsolete |
参数状态说明
- obsolete:参数已过时,使用时会输出警告并被忽略
- 已移除:参数已从 JVM 中删除,使用会导致启动失败
- 正常:参数仍然有效
虚拟线程相关诊断参数(JDK 21+):
| 参数 | 说明 |
|---|---|
-Djdk.tracePinnedThreads=full |
打印虚拟线程被固定的详细堆栈信息 |
-Djdk.tracePinnedThreads=short |
打印虚拟线程被固定的简要信息 |
本文基于 OpenJDK 8 源码分析。随着 JVM 的持续演进,部分内容(特别是偏向锁相关)在更高版本 JDK 中已发生变化。建议读者参考 OpenJDK JEP 索引 和官方发布说明获取最新信息。
参考资料:
- JEP 374: Deprecate and Disable Biased Locking
- JEP 444: Virtual Threads
- Lock (Java SE 23 & JDK 23) - Oracle Help Center
- JMH - Java Microbenchmark Harness
11.4 自述
又是没有大厂约面日子😣😣😣,小编还在找实习的路上,这篇文章是我的笔记汇总整理。