并发编程(十二):偏向锁→轻量级锁→重量级锁底层实现、触发条件、不可逆原因
在前面的章节中,我们已经铺垫了 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 第一次进入同步代码块(synchronized (obj)),此时 obj 处于「无锁状态」(标识位 01,偏向位 0)
-
JVM 检测到 obj 无锁且未偏向,会通过 CAS 操作,将 Mark Word 中的「偏向位」设为 1,同时将当前线程(线程 1)的 ID 写入 Mark Word 的「偏向线程 ID」字段
-
CAS 成功后,obj 进入「偏向锁状态」,此时 Mark Word 存储的是「线程 1 的 ID + Epoch + 分代年龄」
-
线程 1 再次进入同步代码块时,JVM 会直接对比 Mark Word 中的偏向线程 ID 与当前线程 ID:
-
如果一致,直接进入同步代码块,无需任何加锁操作
-
如果不一致,进入「偏向锁撤销」流程。
-
-
线程 1 退出同步代码块时,不会释放偏向锁,依然保留线程 1 的 ID,下次进入时直接复用。
补充:Epoch 字段(2位):用于批量撤销偏向锁时的版本控制,当一批对象的偏向锁需要撤销时,无需逐个修改 Mark Word,只需修改 Epoch 版本,后续线程获取锁时,发现 Epoch 不匹配,就会跳过偏向锁,直接升级为轻量级锁,减少批量撤销的开销。
3 触发升级的条件
偏向锁仅适用于「无竞争」场景,一旦出现其他线程竞争该锁,偏向锁就会被撤销,进而升级为轻量级锁,触发条件主要有 3 种:
-
核心条件:有其他线程尝试获取该偏向锁(最常见)
-
比如线程 2 尝试进入 synchronized (obj),此时 obj 是偏向线程 1 的偏向锁
-
JVM 会先检查线程 1 是否还存活:
-
如果线程 1 已死亡,JVM 会撤销偏向锁,将 obj 恢复为无锁状态,然后线程 2 竞争锁时,直接升级为轻量级锁
-
如果线程 1 还存活,且仍在执行同步代码块(持有偏向锁),则撤销偏向锁,将 obj 升级为轻量级锁,线程 1 和线程 2 进入轻量级锁的竞争流程
-
-
-
触发条件 2:调用对象的 hashCode() 或 System.identityHashCode() 方法
-
偏向锁状态下,Mark Word 存储的是「偏向线程 ID + Epoch + 分代年龄」,没有空间存储对象的哈希码
-
一旦调用 hashCode(),JVM 会撤销偏向锁,将 obj 恢复为无锁状态,同时将哈希码写入 Mark Word;后续线程获取锁时,直接升级为轻量级锁(因为无锁状态下,竞争锁会直接走轻量级锁流程)
-
-
触发条件 3:批量偏向撤销(JVM 优化机制)
-
当同一个类的多个对象,都出现偏向锁撤销(比如多个线程竞争该类的不同对象锁),JVM 会认为该类的对象不适合使用偏向锁,会将该类的「偏向锁开关」关闭;
-
后续该类新创建的对象,直接处于无锁状态,不再开启偏向锁;已存在的偏向锁对象,会被批量撤销,升级为轻量级锁。
-
备注:偏向锁可以通过 JVM 参数关闭(-XX:-UseBiasedLocking),关闭后,synchronized 会直接从无锁状态升级为轻量级锁,不再经过偏向锁阶段。
4 偏向锁的撤销流程
偏向锁的撤销,需要「暂停持有偏向锁的线程」,步骤如下:
-
JVM 检测到偏向锁竞争,暂停持有偏向锁的线程(线程 1)
-
检查线程 1 的栈帧,判断线程 1 是否还在执行同步代码块(是否还持有锁)
-
如果线程 1 已不持有锁(已退出同步代码块),则将 Mark Word 恢复为无锁状态,撤销偏向锁
-
如果线程 1 仍持有锁,则将偏向锁升级为轻量级锁,为线程 1 创建「锁记录(Lock Record)」,并将 Mark Word 中的指针指向该锁记录
-
唤醒线程 1,线程 1 继续执行,后续线程(线程 2)进入轻量级锁竞争流程
偏向锁的撤销是「一次性」的,一旦撤销,就不会再恢复为偏向锁,后续竞争只会走轻量级锁或重量级锁流程(锁升级不可逆的第一步体现)。
三、锁升级第二步:轻量级锁:低竞争场景的无锁优化
1 定义
轻量级锁,适用于「低竞争」场景(多个线程交替进入同步代码块,不频繁同时竞争)。
设计初衷:避免偏向锁撤销后直接升级为重量级锁(重量级锁会触发操作系统层面的上下文切换,开销大),通过 CAS 自旋,让线程在用户态等待,减少上下文切换开销。
注意:轻量级锁的自旋是「有限次数」的(JDK 8 中默认自旋 10 次,可通过 -XX:PreBlockSpin 参数调整),自旋次数耗尽仍未获取锁,就会升级为重量级锁。
2 底层实现
轻量级锁的实现,依然在 JVM 层面完成,核心依赖「栈帧中的锁记录(Lock Record)」和 CAS 操作,步骤分「锁获取」和「锁释放」两部分:
1 轻量级锁的获取流程
-
线程进入同步代码块,JVM 先检查对象的锁状态:如果是无锁状态(标识位 01,偏向位 0)或偏向锁已撤销,就为当前线程在栈帧中创建一个「锁记录(Lock Record)」;
-
锁记录中存储两个核心信息:① 当前对象的 Mark Word 副本(称为 Displaced Mark Word);② 指向当前对象的指针;
-
JVM 通过 CAS 操作,将对象 Mark Word 中的内容替换为「指向当前线程锁记录的指针」,同时将锁状态标识位改为 00(轻量级锁状态);
-
CAS 成功:当前线程获取轻量级锁,进入同步代码块执行;
-
CAS 失败:说明有其他线程正在竞争该锁(低竞争升级为高竞争),当前线程进入「自旋等待」,尝试再次执行 CAS 操作。
2 轻量级锁的释放流程
-
线程退出同步代码块,JVM 执行轻量级锁释放操作,核心是「反向 CAS」
-
通过 CAS 操作,将锁记录中存储的「Displaced Mark Word(原 Mark Word 副本)」,写回对象的 Mark Word 中
-
CAS 成功:锁释放完成,对象恢复为无锁状态(标识位 01,偏向位 0)
-
CAS 失败:说明当前锁已经被其他线程竞争(自旋次数耗尽,已升级为重量级锁),此时 JVM 会直接唤醒等待的线程,进入重量级锁的调度流程
3 触发升级的条件
轻量级锁仅适用于「低竞争、线程交替执行」的场景,一旦竞争加剧(多个线程同时自旋等待锁),就会升级为重量级锁,触发条件主要有 2 种:
-
核心条件:自旋次数耗尽,仍未获取锁
-
JDK 8 中,轻量级锁的默认自旋次数是 10 次(可通过 -XX:PreBlockSpin 调整)
-
线程自旋 10 次后,依然没有通过 CAS 获取到锁,说明竞争激烈,继续自旋会浪费 CPU 资源(忙等)
-
JVM 会将轻量级锁升级为重量级锁,当前线程放弃自旋,进入操作系统层面的阻塞状态(等待锁释放)
-
-
触发条件 2:多个线程同时自旋,导致 CPU 利用率飙升
-
如果多个线程同时自旋等待同一个轻量级锁,会导致 CPU 利用率急剧升高(多个线程空转);
-
JVM 检测到这种情况,会主动将轻量级锁升级为重量级锁,避免 CPU 资源浪费,后续线程会直接进入阻塞状态,而非自旋。
-
补充:轻量级锁升级为重量级锁后,对象的 Mark Word 会被修改为「指向操作系统互斥锁(mutex)的指针」,锁状态标识位改为 10,后续所有竞争该锁的线程,都会直接进入操作系统层面的阻塞状态。
四、锁升级第三步:重量级锁:高竞争场景的最终方案
1 定义
重量级锁,是 synchronized 锁机制的最终形态,依赖「操作系统层面的互斥锁(mutex)」实现,适用于「高竞争」场景(多个线程同时竞争锁,频繁阻塞唤醒)。
设计初衷:当竞争激烈时,自旋会浪费大量 CPU 资源,此时通过操作系统的互斥锁,让未获取锁的线程进入阻塞状态(不占用 CPU 资源),直到锁释放后被唤醒,平衡 CPU 开销和线程调度效率。
重量级锁的核心开销,是「操作系统层面的线程上下文切换」(线程从运行态 → 阻塞态,再从阻塞态 → 运行态),这也是重量级锁比偏向锁、轻量级锁开销大的根本原因。
2 底层实现
重量级锁的实现,需要 JVM 与操作系统协同工作,核心是「操作系统互斥锁(mutex)」和「线程阻塞/唤醒机制」,步骤如下:
-
轻量级锁升级为重量级锁时,JVM 会向操作系统申请一个「互斥锁(mutex)」,并将该 mutex 的指针写入对象的 Mark Word 中,同时将锁状态标识位改为 10(重量级锁状态)
-
线程 1 获取到重量级锁后,进入同步代码块执行,此时其他线程(线程 2、线程 3 等)尝试获取该锁
-
未获取到锁的线程,会被 JVM 交给操作系统处理,操作系统将这些线程标记为「阻塞状态」,并放入「锁等待队列」(等待该 mutex 释放)
-
线程 1 退出同步代码块,释放重量级锁时,JVM 会通知操作系统,操作系统从锁等待队列中唤醒一个线程(按优先级或 FIFO 顺序)
-
被唤醒的线程,重新尝试获取重量级锁,获取成功后进入同步代码块执行,未获取成功的线程继续留在等待队列中,等待下一次唤醒
补充:重量级锁的释放,是「主动释放」+「操作系统唤醒」的协同过程,JVM 负责通知操作系统,操作系统负责线程的阻塞和唤醒,这也是重量级锁开销大的核心原因(跨内核态和用户态切换)。
3 重量级锁的特点
1. 开销大:核心是操作系统层面的上下文切换,跨内核态和用户态,比偏向锁、轻量级锁的开销高 10 倍以上
2. 无自旋:未获取锁的线程直接阻塞,不占用 CPU 资源,适合高竞争场景
3. 公平性:默认是「非公平锁」(唤醒线程时,不保证等待时间最长的线程先获取锁),JDK 8 中无法通过参数修改为公平锁(synchronized 仅支持非公平锁)
4. 最终态:一旦升级为重量级锁,就不会再降级为轻量级锁或偏向锁
五、锁升级为什么是不可逆的?
这是本篇最核心的点,——synchronized 锁升级的顺序是「偏向锁 → 轻量级锁 → 重量级锁」,一旦升级,就无法反向降级。
根本原因:「优化开销」远大于「降级收益」,JVM 选择放弃降级,来避免额外的性能损耗,具体拆解为 3 点:
1 从设计初衷来看:锁升级是“自适应”的,适配竞争强度
锁升级的本质,是 JVM 根据「线程竞争强度」动态调整锁的实现,目的是最大化性能:
-
无竞争 → 偏向锁:消除无竞争开销,极致高效
-
低竞争 → 轻量级锁:通过自旋减少上下文切换
-
高竞争 → 重量级锁:通过阻塞避免 CPU 空转
一旦升级为重量级锁,说明当前场景是「高竞争」,后续大概率依然是高竞争
如果允许降级,需要 JVM 持续检测竞争强度,频繁切换锁状态,反而会增加额外的检测和修改开销,得不偿失。
2 从实现成本来看:降级需要额外的资源开销,性价比极低
锁的状态切换,需要修改 Mark Word、处理锁记录、协调线程状态,甚至涉及操作系统层面的交互,具体来说:
-
重量级锁降级为轻量级锁:需要操作系统释放互斥锁(mutex),JVM 修改 Mark Word,唤醒等待的线程,还要重新初始化轻量级锁的锁记录,过程复杂,开销大
-
轻量级锁降级为偏向锁:需要撤销轻量级锁的 CAS 记录,恢复 Mark Word 的偏向状态,还要重新设置偏向线程 ID,同时需要检测是否有线程竞争,同样会增加开销
降级带来的性能提升,远不足以覆盖降级过程本身的开销,JVM 从性价比角度出发,选择不支持降级
3 从线程状态来看:重量级锁的阻塞状态,无法反向恢复为自旋或无锁
轻量级锁升级为重量级锁时,未获取锁的线程会被操作系统标记为「阻塞状态」,进入等待队列;而阻塞状态的线程,需要通过操作系统的唤醒机制才能恢复,无法直接恢复为轻量级锁的「自旋状态」:
-
如果允许重量级锁降级为轻量级锁,需要将阻塞的线程唤醒,改为自旋等待,这会导致线程状态频繁切换(阻塞 → 运行 → 自旋),增加上下文切换开销
-
同时,自旋需要占用 CPU 资源,高竞争场景下,自旋反而会降低性能,与重量级锁“避免 CPU 空转”的设计初衷相悖
总结:锁升级不可逆的核心结论
JVM 设计锁升级机制的核心是「最大化性能、最小化开销」,而降级带来的收益远小于其实现成本,因此选择让锁升级不可逆,一旦进入更高等级的锁状态,就持续适配对应的竞争场景,避免频繁切换锁状态带来的额外损耗。
六、完整锁升级流程梳理
-
初始状态:对象处于无锁状态(Mark Word 存储哈希码、分代年龄,标识位 01,偏向位 0)
-
第一次获取锁(无竞争):JVM 通过 CAS 设为偏向锁(偏向位 1,标识位 01,存储偏向线程 ID),后续该线程再次获取锁,直接进入
-
出现竞争(其他线程尝试获取偏向锁):撤销偏向锁,升级为轻量级锁(标识位 00,Mark Word 指向线程栈帧的锁记录),线程通过 CAS 自旋竞争锁
-
竞争加剧(自旋次数耗尽或多线程同时自旋):轻量级锁升级为重量级锁(标识位 10,Mark Word 指向操作系统 mutex),未获取锁的线程进入阻塞状态
-
最终:重量级锁无法降级,后续所有竞争都通过操作系统 mutex 实现,线程阻塞唤醒,完成锁的调度
七、常见误区
-
误区 1:偏向锁是“加锁”,退出时需要释放锁 → 错误!偏向锁退出时不释放,仅在出现竞争时才撤销;
-
误区 2:轻量级锁就是自旋锁 → 不完全正确!轻量级锁的核心是 CAS 自旋,但自旋只是轻量级锁的竞争方式,两者不能完全等同;
-
误区 3:synchronized 可以实现公平锁 → 错误!synchronized 底层的重量级锁是默认非公平锁,无法修改;
-
误区 4:锁升级是可逆的 → 错误!锁升级仅能从低等级到高等级,无法反向降级;
-
误区 5:JDK 8 中,偏向锁是默认开启的 → 正确!JDK 8 默认开启偏向锁,可通过 -XX:-UseBiasedLocking 关闭。
八、总结
synchronized 锁升级机制,是 JVM 对并发性能的极致优化,核心是「根据竞争强度自适应调整锁的实现」,从偏向锁的无开销,到轻量级锁的自旋优化,再到重量级锁的阻塞调度,每一步都对应不同的并发场景。
锁升级顺序:偏向锁 → 轻量级锁 → 重量级锁,不可逆;
底层核心:所有锁状态都存在 Mark Word 中,锁升级的本质是修改 Mark Word 的字段和状态标识位;
触发条件:偏向锁因竞争/调用 hashCode 撤销升级,轻量级锁因自旋耗尽/高竞争升级;
不可逆原因:降级收益小于实现成本,JVM 优先保证性能,放弃降级;
适用场景:偏向锁(无竞争)、轻量级锁(低竞争)、重量级锁(高竞争)。
更多推荐


所有评论(0)