在前面的章节中,我们已经铺垫了 synchronized 关键字的基本使用、Java 对象头结构(Mark Word)、CPU 原子性与 CAS 核心原理,而 synchronized 锁机制的精髓——「锁升级」,正是本篇要讲的核心内容。

synchronized 并非一开始就是“重量级”锁,而是会根据线程竞争的激烈程度,从 偏向锁 → 轻量级锁 → 重量级锁 逐步升级,这种自适应的锁升级策略,是 JVM 对并发性能的极致优化。

1. 锁的载体是「Java 对象」,所有锁的状态都记录在对象头的 Mark Word 中

2. 本篇讨论的所有锁实现,均针对「重量级锁是操作系统层面的互斥锁(mutex)」展开,JDK 1.6 及以后引入偏向锁、轻量级锁,本质是为了减少操作系统层面的上下文切换开销;

3. 所有结论均基于「HotSpot 虚拟机」(主流 JVM,如 Oracle JDK、OpenJDK)

一、复习:Mark Word 与锁状态的关联

并发编程(十):对象头 Mark Word 与 synchronized 锁升级机制-CSDN博客

锁状态

Mark Word 核心字段(64位)

状态标识位(2位)

是否偏向(1位,仅偏向锁相关)

无锁状态

对象哈希码(31位) + 分代年龄(4位) + 未使用(25位)

01

0(无偏向)

偏向锁

偏向线程 ID(54位) + Epoch(2位) + 分代年龄(4位)

01

1(有偏向)

轻量级锁

指向栈帧中锁记录的指针(62位)

00

无(该位无效)

重量级锁

指向操作系统互斥锁(mutex)的指针(62位)

10

无(该位无效)

二、锁升级第一步:偏向锁

1 定义

偏向锁,是「偏向于第一个获取它的线程」,在无其他线程竞争的情况下,持有偏向锁的线程再次进入同步代码块时,无需做任何 CAS 操作或加锁解锁操作,直接就能进入,彻底消除无竞争场景下的锁开销。

大多数场景下,同步代码块的执行都是「单线程」的,即使有多个线程,也很少出现竞争,偏向锁就是利用这一特性,减少无竞争时的性能损耗。

2 底层实现

偏向锁的实现,完全基于 Mark Word 的修改,无需操作系统介入,全程在 JVM 层面完成,步骤如下:

  1. 线程 1 第一次进入同步代码块(synchronized (obj)),此时 obj 处于「无锁状态」(标识位 01,偏向位 0)

  2. JVM 检测到 obj 无锁且未偏向,会通过 CAS 操作,将 Mark Word 中的「偏向位」设为 1,同时将当前线程(线程 1)的 ID 写入 Mark Word 的「偏向线程 ID」字段

  3. CAS 成功后,obj 进入「偏向锁状态」,此时 Mark Word 存储的是「线程 1 的 ID + Epoch + 分代年龄」

  4. 线程 1 再次进入同步代码块时,JVM 会直接对比 Mark Word 中的偏向线程 ID 与当前线程 ID:

    1. 如果一致,直接进入同步代码块,无需任何加锁操作

    2. 如果不一致,进入「偏向锁撤销」流程。

  5. 线程 1 退出同步代码块时,不会释放偏向锁,依然保留线程 1 的 ID,下次进入时直接复用。

补充:Epoch 字段(2位):用于批量撤销偏向锁时的版本控制,当一批对象的偏向锁需要撤销时,无需逐个修改 Mark Word,只需修改 Epoch 版本,后续线程获取锁时,发现 Epoch 不匹配,就会跳过偏向锁,直接升级为轻量级锁,减少批量撤销的开销。

3 触发升级的条件

偏向锁仅适用于「无竞争」场景,一旦出现其他线程竞争该锁,偏向锁就会被撤销,进而升级为轻量级锁,触发条件主要有 3 种:

  1. 核心条件:有其他线程尝试获取该偏向锁(最常见)

    1. 比如线程 2 尝试进入 synchronized (obj),此时 obj 是偏向线程 1 的偏向锁

    2. JVM 会先检查线程 1 是否还存活:

      • 如果线程 1 已死亡,JVM 会撤销偏向锁,将 obj 恢复为无锁状态,然后线程 2 竞争锁时,直接升级为轻量级锁

      • 如果线程 1 还存活,且仍在执行同步代码块(持有偏向锁),则撤销偏向锁,将 obj 升级为轻量级锁,线程 1 和线程 2 进入轻量级锁的竞争流程

  2. 触发条件 2:调用对象的 hashCode() 或 System.identityHashCode() 方法

    1. 偏向锁状态下,Mark Word 存储的是「偏向线程 ID + Epoch + 分代年龄」,没有空间存储对象的哈希码

    2. 一旦调用 hashCode(),JVM 会撤销偏向锁,将 obj 恢复为无锁状态,同时将哈希码写入 Mark Word;后续线程获取锁时,直接升级为轻量级锁(因为无锁状态下,竞争锁会直接走轻量级锁流程)

  3. 触发条件 3:批量偏向撤销(JVM 优化机制)

    1. 当同一个类的多个对象,都出现偏向锁撤销(比如多个线程竞争该类的不同对象锁),JVM 会认为该类的对象不适合使用偏向锁,会将该类的「偏向锁开关」关闭;

    2. 后续该类新创建的对象,直接处于无锁状态,不再开启偏向锁;已存在的偏向锁对象,会被批量撤销,升级为轻量级锁。

备注:偏向锁可以通过 JVM 参数关闭(-XX:-UseBiasedLocking),关闭后,synchronized 会直接从无锁状态升级为轻量级锁,不再经过偏向锁阶段。

4 偏向锁的撤销流程

偏向锁的撤销,需要「暂停持有偏向锁的线程」,步骤如下:

  1. JVM 检测到偏向锁竞争,暂停持有偏向锁的线程(线程 1)

  2. 检查线程 1 的栈帧,判断线程 1 是否还在执行同步代码块(是否还持有锁)

  3. 如果线程 1 已不持有锁(已退出同步代码块),则将 Mark Word 恢复为无锁状态,撤销偏向锁

  4. 如果线程 1 仍持有锁,则将偏向锁升级为轻量级锁,为线程 1 创建「锁记录(Lock Record)」,并将 Mark Word 中的指针指向该锁记录

  5. 唤醒线程 1,线程 1 继续执行,后续线程(线程 2)进入轻量级锁竞争流程

偏向锁的撤销是「一次性」的,一旦撤销,就不会再恢复为偏向锁,后续竞争只会走轻量级锁或重量级锁流程(锁升级不可逆的第一步体现)。

三、锁升级第二步:轻量级锁:低竞争场景的无锁优化

1 定义

轻量级锁,适用于「低竞争」场景(多个线程交替进入同步代码块,不频繁同时竞争)。

设计初衷:避免偏向锁撤销后直接升级为重量级锁(重量级锁会触发操作系统层面的上下文切换,开销大),通过 CAS 自旋,让线程在用户态等待,减少上下文切换开销。

注意:轻量级锁的自旋是「有限次数」的(JDK 8 中默认自旋 10 次,可通过 -XX:PreBlockSpin 参数调整),自旋次数耗尽仍未获取锁,就会升级为重量级锁。

2 底层实现

轻量级锁的实现,依然在 JVM 层面完成,核心依赖「栈帧中的锁记录(Lock Record)」和 CAS 操作,步骤分「锁获取」和「锁释放」两部分:

1 轻量级锁的获取流程
  1. 线程进入同步代码块,JVM 先检查对象的锁状态:如果是无锁状态(标识位 01,偏向位 0)或偏向锁已撤销,就为当前线程在栈帧中创建一个「锁记录(Lock Record)」;

  2. 锁记录中存储两个核心信息:① 当前对象的 Mark Word 副本(称为 Displaced Mark Word);② 指向当前对象的指针;

  3. JVM 通过 CAS 操作,将对象 Mark Word 中的内容替换为「指向当前线程锁记录的指针」,同时将锁状态标识位改为 00(轻量级锁状态);

  4. CAS 成功:当前线程获取轻量级锁,进入同步代码块执行;

  5. CAS 失败:说明有其他线程正在竞争该锁(低竞争升级为高竞争),当前线程进入「自旋等待」,尝试再次执行 CAS 操作。

2 轻量级锁的释放流程
  1. 线程退出同步代码块,JVM 执行轻量级锁释放操作,核心是「反向 CAS」

  2. 通过 CAS 操作,将锁记录中存储的「Displaced Mark Word(原 Mark Word 副本)」,写回对象的 Mark Word 中

  3. CAS 成功:锁释放完成,对象恢复为无锁状态(标识位 01,偏向位 0)

  4. CAS 失败:说明当前锁已经被其他线程竞争(自旋次数耗尽,已升级为重量级锁),此时 JVM 会直接唤醒等待的线程,进入重量级锁的调度流程

3 触发升级的条件

轻量级锁仅适用于「低竞争、线程交替执行」的场景,一旦竞争加剧(多个线程同时自旋等待锁),就会升级为重量级锁,触发条件主要有 2 种:

  1. 核心条件:自旋次数耗尽,仍未获取锁

    1. JDK 8 中,轻量级锁的默认自旋次数是 10 次(可通过 -XX:PreBlockSpin 调整)

    2. 线程自旋 10 次后,依然没有通过 CAS 获取到锁,说明竞争激烈,继续自旋会浪费 CPU 资源(忙等)

    3. JVM 会将轻量级锁升级为重量级锁,当前线程放弃自旋,进入操作系统层面的阻塞状态(等待锁释放)

  2. 触发条件 2:多个线程同时自旋,导致 CPU 利用率飙升

    1. 如果多个线程同时自旋等待同一个轻量级锁,会导致 CPU 利用率急剧升高(多个线程空转);

    2. JVM 检测到这种情况,会主动将轻量级锁升级为重量级锁,避免 CPU 资源浪费,后续线程会直接进入阻塞状态,而非自旋。

补充:轻量级锁升级为重量级锁后,对象的 Mark Word 会被修改为「指向操作系统互斥锁(mutex)的指针」,锁状态标识位改为 10,后续所有竞争该锁的线程,都会直接进入操作系统层面的阻塞状态。

四、锁升级第三步:重量级锁:高竞争场景的最终方案

1 定义

重量级锁,是 synchronized 锁机制的最终形态,依赖「操作系统层面的互斥锁(mutex)」实现,适用于「高竞争」场景(多个线程同时竞争锁,频繁阻塞唤醒)。

设计初衷:当竞争激烈时,自旋会浪费大量 CPU 资源,此时通过操作系统的互斥锁,让未获取锁的线程进入阻塞状态(不占用 CPU 资源),直到锁释放后被唤醒,平衡 CPU 开销和线程调度效率。

重量级锁的核心开销,是「操作系统层面的线程上下文切换」(线程从运行态 → 阻塞态,再从阻塞态 → 运行态),这也是重量级锁比偏向锁、轻量级锁开销大的根本原因。

2 底层实现

重量级锁的实现,需要 JVM 与操作系统协同工作,核心是「操作系统互斥锁(mutex)」和「线程阻塞/唤醒机制」,步骤如下:

  1. 轻量级锁升级为重量级锁时,JVM 会向操作系统申请一个「互斥锁(mutex)」,并将该 mutex 的指针写入对象的 Mark Word 中,同时将锁状态标识位改为 10(重量级锁状态)

  2. 线程 1 获取到重量级锁后,进入同步代码块执行,此时其他线程(线程 2、线程 3 等)尝试获取该锁

  3. 未获取到锁的线程,会被 JVM 交给操作系统处理,操作系统将这些线程标记为「阻塞状态」,并放入「锁等待队列」(等待该 mutex 释放)

  4. 线程 1 退出同步代码块,释放重量级锁时,JVM 会通知操作系统,操作系统从锁等待队列中唤醒一个线程(按优先级或 FIFO 顺序)

  5. 被唤醒的线程,重新尝试获取重量级锁,获取成功后进入同步代码块执行,未获取成功的线程继续留在等待队列中,等待下一次唤醒

补充:重量级锁的释放,是「主动释放」+「操作系统唤醒」的协同过程,JVM 负责通知操作系统,操作系统负责线程的阻塞和唤醒,这也是重量级锁开销大的核心原因(跨内核态和用户态切换)。

3 重量级锁的特点

1. 开销大:核心是操作系统层面的上下文切换,跨内核态和用户态,比偏向锁、轻量级锁的开销高 10 倍以上

2. 无自旋:未获取锁的线程直接阻塞,不占用 CPU 资源,适合高竞争场景

3. 公平性:默认是「非公平锁」(唤醒线程时,不保证等待时间最长的线程先获取锁),JDK 8 中无法通过参数修改为公平锁(synchronized 仅支持非公平锁)

4. 最终态:一旦升级为重量级锁,就不会再降级为轻量级锁或偏向锁

五、锁升级为什么是不可逆的?

这是本篇最核心的点,——synchronized 锁升级的顺序是「偏向锁 → 轻量级锁 → 重量级锁」,一旦升级,就无法反向降级。

根本原因:「优化开销」远大于「降级收益」,JVM 选择放弃降级,来避免额外的性能损耗,具体拆解为 3 点:

1 从设计初衷来看:锁升级是“自适应”的,适配竞争强度

锁升级的本质,是 JVM 根据「线程竞争强度」动态调整锁的实现,目的是最大化性能:

  • 无竞争 → 偏向锁:消除无竞争开销,极致高效

  • 低竞争 → 轻量级锁:通过自旋减少上下文切换

  • 高竞争 → 重量级锁:通过阻塞避免 CPU 空转

一旦升级为重量级锁,说明当前场景是「高竞争」,后续大概率依然是高竞争

如果允许降级,需要 JVM 持续检测竞争强度,频繁切换锁状态,反而会增加额外的检测和修改开销,得不偿失。

2 从实现成本来看:降级需要额外的资源开销,性价比极低

锁的状态切换,需要修改 Mark Word、处理锁记录、协调线程状态,甚至涉及操作系统层面的交互,具体来说:

  1. 重量级锁降级为轻量级锁:需要操作系统释放互斥锁(mutex),JVM 修改 Mark Word,唤醒等待的线程,还要重新初始化轻量级锁的锁记录,过程复杂,开销大

  2. 轻量级锁降级为偏向锁:需要撤销轻量级锁的 CAS 记录,恢复 Mark Word 的偏向状态,还要重新设置偏向线程 ID,同时需要检测是否有线程竞争,同样会增加开销

降级带来的性能提升,远不足以覆盖降级过程本身的开销,JVM 从性价比角度出发,选择不支持降级

3 从线程状态来看:重量级锁的阻塞状态,无法反向恢复为自旋或无锁

轻量级锁升级为重量级锁时,未获取锁的线程会被操作系统标记为「阻塞状态」,进入等待队列;而阻塞状态的线程,需要通过操作系统的唤醒机制才能恢复,无法直接恢复为轻量级锁的「自旋状态」:

  • 如果允许重量级锁降级为轻量级锁,需要将阻塞的线程唤醒,改为自旋等待,这会导致线程状态频繁切换(阻塞 → 运行 → 自旋),增加上下文切换开销

  • 同时,自旋需要占用 CPU 资源,高竞争场景下,自旋反而会降低性能,与重量级锁“避免 CPU 空转”的设计初衷相悖

总结:锁升级不可逆的核心结论

JVM 设计锁升级机制的核心是「最大化性能、最小化开销」,而降级带来的收益远小于其实现成本,因此选择让锁升级不可逆,一旦进入更高等级的锁状态,就持续适配对应的竞争场景,避免频繁切换锁状态带来的额外损耗。

六、完整锁升级流程梳理

  1. 初始状态:对象处于无锁状态(Mark Word 存储哈希码、分代年龄,标识位 01,偏向位 0)

  2. 第一次获取锁(无竞争):JVM 通过 CAS 设为偏向锁(偏向位 1,标识位 01,存储偏向线程 ID),后续该线程再次获取锁,直接进入

  3. 出现竞争(其他线程尝试获取偏向锁):撤销偏向锁,升级为轻量级锁(标识位 00,Mark Word 指向线程栈帧的锁记录),线程通过 CAS 自旋竞争锁

  4. 竞争加剧(自旋次数耗尽或多线程同时自旋):轻量级锁升级为重量级锁(标识位 10,Mark Word 指向操作系统 mutex),未获取锁的线程进入阻塞状态

  5. 最终:重量级锁无法降级,后续所有竞争都通过操作系统 mutex 实现,线程阻塞唤醒,完成锁的调度

七、常见误区

  • 误区 1:偏向锁是“加锁”,退出时需要释放锁 → 错误!偏向锁退出时不释放,仅在出现竞争时才撤销;

  • 误区 2:轻量级锁就是自旋锁 → 不完全正确!轻量级锁的核心是 CAS 自旋,但自旋只是轻量级锁的竞争方式,两者不能完全等同;

  • 误区 3:synchronized 可以实现公平锁 → 错误!synchronized 底层的重量级锁是默认非公平锁,无法修改;

  • 误区 4:锁升级是可逆的 → 错误!锁升级仅能从低等级到高等级,无法反向降级;

  • 误区 5:JDK 8 中,偏向锁是默认开启的 → 正确!JDK 8 默认开启偏向锁,可通过 -XX:-UseBiasedLocking 关闭。

八、总结

synchronized 锁升级机制,是 JVM 对并发性能的极致优化,核心是「根据竞争强度自适应调整锁的实现」,从偏向锁的无开销,到轻量级锁的自旋优化,再到重量级锁的阻塞调度,每一步都对应不同的并发场景。

  1. 锁升级顺序:偏向锁 → 轻量级锁 → 重量级锁,不可逆;

  2. 底层核心:所有锁状态都存在 Mark Word 中,锁升级的本质是修改 Mark Word 的字段和状态标识位;

  3. 触发条件:偏向锁因竞争/调用 hashCode 撤销升级,轻量级锁因自旋耗尽/高竞争升级;

  4. 不可逆原因:降级收益小于实现成本,JVM 优先保证性能,放弃降级;

  5. 适用场景:偏向锁(无竞争)、轻量级锁(低竞争)、重量级锁(高竞争)。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐