synchronized 锁升级原理:从 JDK 8 实现到 JDK 25 演进

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 锁的关键,它包含两部分:

  1. Mark Word(标记字):存储对象自身的运行时数据
  2. 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 锁升级流程图

flowchart TD Start["开始访问同步代码块"] --> CheckMark["检查 Mark Word"] CheckMark --> |无锁态 01,0| Unlocked["无锁态"] CheckMark --> |偏向锁态 01,1| Biased["偏向锁态"] CheckMark --> |轻量级锁 00| Lightweight["轻量级锁态"] Unlocked --> TryLock["尝试获取锁(根据当前状态)"] Biased --> TryLock Lightweight --> TryLock TryLock --> |成功| Success["获取成功"] TryLock --> |出现竞争| Contention["竞争出现"] TryLock --> |持续竞争| HeavyContention["持续竞争"] Success --> Execute["执行同步代码块"] Contention --> UpgradeLight["锁升级到轻量级"] HeavyContention --> UpgradeHeavy["锁升级到重量级"] Execute --> Release["释放锁"] UpgradeLight --> Release UpgradeHeavy --> Release

4.2 详细升级过程

4.2.1 无锁 → 偏向锁

  1. 检查对象头:线程访问同步块时,检查 Mark Word 的锁标志位和偏向锁标志
  2. 判断是否可偏向
    • 如果是可偏向状态(偏向锁标志为 1)且 ThreadID 为空
    • 使用 CAS 操作尝试将当前线程 ID 写入 Mark Word
  3. 获取偏向锁成功:CAS 成功后,以后该线程进入同步块时只需简单测试 Mark Word 中是否存储着指向当前线程的偏向锁

4.2.2 偏向锁 → 轻量级锁

当另一个线程尝试获取已被偏向的锁时:

  1. 检测到竞争:发现 Mark Word 中的线程 ID 不是自己
  2. 暂停偏向线程:到达全局安全点(Safepoint),暂停持有偏向锁的线程
  3. 检查偏向线程状态
    • 如果线程已经退出同步块或不再存活:直接撤销偏向锁,变为无锁状态
    • 如果线程仍在同步块中:将偏向锁升级为轻量级锁
  4. 恢复线程执行

4.2.3 轻量级锁 → 重量级锁

当自旋达到一定次数仍未获得锁时:

  1. 自旋失败:线程自旋等待锁的次数超过阈值(JDK 6+ 默认启用自适应自旋,由 JVM 动态调整)
  2. 膨胀为重量级锁:将轻量级锁膨胀为重量级锁
  3. 线程阻塞:未获得锁的线程进入阻塞状态,等待被唤醒

注意 :早期版本可通过 -XX:PreBlockSpin 参数设置固定自旋次数(默认 10 次),但该参数在现代 JDK 中已被自适应自旋取代,不再生效。


5. 偏向锁的实现原理

5.1 偏向锁的设计理念

偏向锁的核心思想:锁不仅不存在多线程竞争,而且总是由同一线程多次获得

在这种情况下,锁的获取和释放连 CAS 操作都不需要,只需简单地测试 Mark Word 中是否存储着指向当前线程的偏向锁,大大提高性能。

5.2 偏向锁的获取流程

flowchart TD Start["线程进入同步块"] --> CheckBias{"检查 Mark Word
偏向锁标志是否为 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)。

撤销流程:

  1. 到达安全点:暂停持有偏向锁的线程
  2. 检查线程状态
    • 线程未活动或已退出同步块:直接将 Mark Word 改为无锁状态(01,0)
    • 线程仍在同步块中:将偏向锁升级为轻量级锁
  3. 恢复线程执行

批量重偏向和批量撤销:

为了优化偏向锁撤销的性能,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 轻量级锁的加锁流程

flowchart TD Start["线程进入同步块"] --> CreateRecord["在栈帧中创建
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["锁膨胀为重量级锁"]

详细步骤:

  1. 创建 Lock Record:在当前线程的栈帧中创建锁记录(Lock Record)空间,用于存储对象当前的 Mark Word 拷贝(官方称为 Displaced Mark Word)

  2. 复制 Mark Word:将对象的 Mark Word 复制到锁记录中

  3. CAS 替换:使用 CAS 操作尝试将对象的 Mark Word 替换为指向锁记录的指针

    1. 成功:表示当前线程获得了锁,Mark Word 的锁标志位变为 00
    2. 失败:检查对象的 Mark Word 是否指向当前线程的栈帧
    • :表示当前线程已经拥有该对象的锁(锁重入),直接执行同步代码
    • :表示存在竞争,轻量级锁需要膨胀为重量级锁

6.3 轻量级锁的解锁流程

flowchart TD Start["线程退出同步块"] --> CASRestore["使用 CAS 将 Displaced
Mark Word 替换回对象头"] CASRestore --> |成功| Success["解锁成功"] CASRestore --> |失败| HeavyUnlock["锁已膨胀为重量级
执行重量级锁解锁流程"]

详细步骤:

  1. 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 重量级锁的加锁流程

flowchart TD Start["线程尝试获取重量级锁"] --> CheckOwner{"检查 _owner
字段是否为空?"} CheckOwner --> |是| CASSet["CAS 设置
_owner 为当前线程"] CheckOwner --> |否| CheckCurrent{"检查是否为
当前线程?"} CASSet --> |成功| Success["获取锁成功"] CheckCurrent --> |是| Reentrant["锁重入
_recursions++"] CheckCurrent --> |否| EnterQueue["加入 _EntryList
或 _cxq 队列"] EnterQueue --> Park["阻塞等待
park"] Park --> Wakeup["被唤醒后重新竞争锁"]

7.4 重量级锁的解锁流程

flowchart TD Start["线程释放重量级锁"] --> CheckRecursions{"检查 _recursions
是否大于 0?"} CheckRecursions --> |是| Decrease["_recursions--"] CheckRecursions --> |否| SetNull["将 _owner 设置为 NULL"] SetNull --> Notify["唤醒 _EntryList
或 _cxq 中的一个线程"]

7.5 wait/notify 机制

重量级锁还支持 wait()notify()notifyAll() 方法,这些方法的实现依赖 ObjectMonitor。

wait() 流程:

  1. 线程调用 wait() 方法
  2. 释放持有的锁(_owner 置为 NULL,_recursions 置为 0)
  3. 线程进入 _WaitSet 队列,状态变为 WAITING
  4. 线程被阻塞

notify() 流程:

  1. 线程调用 notify() 方法
  2. _WaitSet 中取出一个线程
  3. 将该线程移到 _EntryList_cxq 队列
  4. 线程状态从 WAITING 变为 BLOCKED
  5. 该线程等待重新竞争锁

线程状态转换图:

stateDiagram-v2 [*] --> NEW NEW --> RUNNABLE: start() RUNNABLE --> WAITING: wait() RUNNABLE --> TIMED_WAITING: wait(timeout)/sleep() RUNNABLE --> BLOCKED: 等待synchronized锁 WAITING --> BLOCKED: notify()/notifyAll() TIMED_WAITING --> BLOCKED: 超时/notify() BLOCKED --> RUNNABLE: 获得锁 RUNNABLE --> TERMINATED: 执行完毕 TERMINATED --> [*]

注意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();
}

StringBufferappend() 方法是同步方法,但在这个例子中,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) 将工作内存中共享变量的值刷新到主内存

这保证了:

  1. 可见性:一个线程释放锁后,所有的修改对后续获得该锁的线程可见
  2. 有序性:禁止 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):

  1. 维护成本高:批量重偏向、批量撤销机制增加了代码复杂度
  2. 收益有限:现代应用多为高并发场景,单线程重复获取锁的场景减少
  3. 启动延迟问题:偏向锁延迟 4 秒启动,影响某些应用的启动性能
  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 核心要点

  1. 锁升级机制:synchronized 通过无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级路径,在不同竞争场景下使用最合适的同步策略

  2. 对象头是关键:Mark Word 存储了锁的状态信息,是实现锁升级的核心数据结构

  3. 性能权衡

    • 偏向锁:适合单线程场景,几乎无开销
    • 轻量级锁:适合竞争不激烈且同步块执行快的场景,通过 CAS 和自旋避免阻塞
    • 重量级锁:适合竞争激烈或同步块执行慢的场景,依赖操作系统调度
  4. 只能升级不能降级:锁状态只能从低到高升级,不能降级,这是为了提高效率的设计权衡

  5. 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 索引 和官方发布说明获取最新信息。

参考资料:

11.4 自述

又是没有大厂约面日子😣😣😣,小编还在找实习的路上,这篇文章是我的笔记汇总整理。

相关推荐
ERP老兵-冷溪虎山1 小时前
Python/JS/Go/Java同步学习(第五十篇半)四语言“path路径详解“对照表: 看完这篇定位文件就通透了(附源码/截图/参数表/避坑指南)
java·javascript·python·golang·中医编程·编程四语言同步学·path路径详解
零匠学堂20251 小时前
移动学习平台与在线学习平台是什么?主要有哪些功能?
java·spring boot·学习
少平8181 小时前
一分钱的Bug(求助帖)
java
q***01771 小时前
Spring.factories
java·数据库·spring
-大头.1 小时前
Spring Bean作用域深度解析与实战
java·后端·spring
qq_336313931 小时前
java基础-排序算法
java·开发语言·排序算法
豆沙沙包?1 小时前
2025年--Lc298-1019. 链表中的下一个更大节点(栈)--java版
java·数据结构·链表
疯狂的程序猴1 小时前
APP上架苹果应用商店经验教训与注意事项
后端
fengfuyao9851 小时前
匈牙利算法的MATLAB实现
java·算法·matlab