Java并发中的锁升级 偏向锁 轻量级锁升级成重量级锁
这是一个非常经典且高频的Java并发面试题,也是理解 synchronized 性能优化的核心。
在 JDK 1.6 之前,synchronized 是一个“重量级锁”,效率较低。为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了偏向锁和轻量级锁。
锁的状态总共有四种,级别从低到高依次是:
无锁状态 (No Lock) -> 偏向锁 (Biased Lock) -> 轻量级锁 (Lightweight Lock) -> 重量级锁 (Heavyweight Lock)
锁只能升级,不能降级(除了GC阶段),这种策略是为了提高获得锁和释放锁的效率。
一、 核心基础:对象头 (Mark Word)
要理解锁升级,必须先理解 Java 对象在内存中的布局,特别是 对象头(Object Header) 中的 Mark Word。
Mark Word 是一个非固定的数据结构,用于存储对象的运行时数据(HashCode、GC分代年龄、锁状态标志、线程持有的锁等)。
在 64 位虚拟机中,Mark Word 的大概结构如下(简化版):
|
锁状态 |
25bit (未使用) |
31bit (HashCode) |
1bit (未使用) |
4bit (分代年龄) |
1bit (偏向锁标识) |
2bit (锁标志位) |
|
无锁 |
... |
HashCode |
0 |
age |
0 |
01 |
|
偏向锁 |
ThreadID (54bit) |
Epoch (2bit) |
... |
age |
1 |
01 |
|
轻量级锁 |
指向栈中 Lock Record 的指针 (62bit) |
... |
... |
... |
... |
00 |
|
重量级锁 |
指向互斥量 (Monitor) 的指针 (62bit) |
... |
... |
... |
... |
10 |
注意:锁标志位和偏向锁标识共同决定了锁的状态。
二、 锁升级的详细过程
1. 偏向锁 (Biased Lock)
场景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。
原理:
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID。
- 以后该线程在进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
- 如果测试成功,表示线程已经获得了锁。
偏向锁的撤销:
- 偏向锁使用了一种等到竞争出现才释放锁的机制。只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
- 需要等待全局安全点(Safepoint,在这个时间点上没有正在执行的字节码)。
- 暂停拥有偏向锁的线程,检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,升级为轻量级锁。
注:JDK 15 之后,偏向锁默认是禁用的(Deprecated),因为在现代高并发应用中,维护偏向锁撤销的成本可能高于其带来的收益。
2. 轻量级锁 (Lightweight Lock)
场景:线程交替执行同步块(即没有激烈的并发竞争),或者竞争非常短暂。
原理 (CAS):
- 加锁:线程在执行同步块之前,JVM 会在当前线程的栈帧中创建用于存储锁记录的空间(Lock Record),并将对象头中的 Mark Word 复制到锁记录中(Displaced Mark Word)。
- 然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
- 如果成功,当前线程获得锁,标志位变为
00。 - 自旋 (Spinning):如果失败,表示有竞争。当前线程不会立刻阻塞(不放弃CPU),而是进行自旋(也就是写一个
while(true)空循环),尝试看看持有锁的线程是否很快释放。如果自旋一定次数后(或者智能自旋)还没拿到锁,就要升级。
3. 重量级锁 (Heavyweight Lock)
场景:多个线程同时竞争锁,自旋失败,或者竞争激烈。
原理:
- 当轻量级锁升级为重量级锁时,锁标志的状态值变为
10,Mark Word 中存储的是指向**重量级锁(ObjectMonitor)**的指针。 - 此时等待锁的线程会被阻塞 (Block),进入内核态。
- ObjectMonitor:这是基于操作系统的 Mutex Lock(互斥锁)实现的。
-
_owner:指向持有 ObjectMonitor 对象的线程。_EntryList:处于等待锁 block 状态的线程,会被加入到该列表。_WaitSet:处于 wait 状态的线程,会被加入到该列表。
代价:
- 线程的阻塞和唤醒需要操作系统介入,需要在用户态和内核态之间切换,由于 CPU 上下文切换的成本非常高,所以称为“重量级”。
三、 锁升级流程图 (可视化)
为了让你更直观地理解,我画了一个 Mermaid 流程图来描述这个升级过程:
graph TD
Start((对象创建)) --> NoLock[无锁状态<br/>MarkWord: hash|age|0|01]
NoLock -- 线程A访问 --> BiasCheck{是否开启偏向锁?}
BiasCheck -- Yes --> BiasedLock[偏向锁<br/>MarkWord: ThreadA_ID|age|1|01]
BiasCheck -- No --> LightLock
BiasedLock -- 线程A再次访问 --> BiasedLock
BiasedLock -- 线程B尝试获取锁 --> RevokeBias[撤销偏向锁<br/>(需等待全局安全点)]
RevokeBias --> IsALive{线程A是否存活<br/>且持有锁?}
IsALive -- No (A已结束) --> NoLock
IsALive -- Yes (A还在用) --> UpgradeLight[升级为轻量级锁]
UpgradeLight --> LightLock[轻量级锁<br/>MarkWord: 指向栈中Lock Record|00]
LightLock -- 线程A/B交替执行 --> LightLock
LightLock -- 发生竞争 (CAS失败) --> Spin{自旋<br/>(Adaptive Spin)}
Spin -- 自旋成功 --> LightLock
Spin -- 自旋失败/次数过多 --> UpgradeHeavy[升级为重量级锁]
UpgradeHeavy --> HeavyLock[重量级锁<br/>MarkWord: 指向Monitor|10]
HeavyLock -- 线程阻塞/挂起 --> WaitOS[等待操作系统唤醒<br/>(内核态切换)]
四、 图解内存结构变化
为了更形象,我们画一下内存中 Mark Word 的变化:
1. 初始/偏向阶段
线程 A 的栈帧
+-------------+
| Lock Record | <-- 不需要存 Displaced Mark Word
+-------------+
堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ Thread A ID | Epoch | Age | 1 | 01 ] | <-- 记录了线程A的ID
+---------------------------------------------------+
2. 轻量级锁阶段 (线程A持有,线程B来抢,升级)
线程 A 的栈帧 线程 B 的栈帧
+-----------------------+ +-----------------------+
| Lock Record (LR) | | Lock Record (LR) |
| [ Original Mark Word ]| | [ Original Mark Word ]|
+-----------------------+ +-----------------------+
^ |
| (CAS 成功指向 A) | (CAS 失败)
| v
堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ ptr to A's Lock Record | 00 ] |
+---------------------------------------------------+
3. 重量级锁阶段 (自旋失败,膨胀)
堆中的对象 (Object)
+---------------------------------------------------+
| Mark Word: [ ptr to ObjectMonitor | 10 ] |
+---------------------------------------------------+
|
v
C++ ObjectMonitor 对象 (OS Mutex)
+------------------------------------------+
| _owner: Thread A |
| _EntryList: [ Thread B, Thread C... ] | <-- 都在阻塞等待
| _WaitSet: [ ... ] |
+------------------------------------------+
五、 总结对比
|
锁类型 |
触发场景 |
优点 |
缺点 |
适用场景 |
|
偏向锁 |
只有一个线程访问 |
加锁/解锁不需要CAS,只需对比ThreadID,消耗极低 |
若存在竞争,撤销锁需要等待安全点,开销大 |
只有一个线程访问同步块的场景 |
|
轻量级锁 |
多个线程交替执行,无同时竞争 |
竞争的线程不会阻塞,使用CAS和自旋,响应速度快 |
若始终得不到锁,长时间自旋会消耗CPU |
追求响应时间,同步块执行速度非常快 |
|
重量级锁 |
多线程同时竞争,竞争激烈 |
线程被阻塞,不消耗CPU |
线程阻塞/唤醒需内核态切换,响应慢,吞吐量低 |
追求吞吐量,同步块执行时间较长 |
六、 一句话总结
锁升级就是JVM为了避免每次都去请求操作系统层面的互斥锁(重),而先尝试在JVM层面利用对象头标记(偏)和CAS自旋(轻)来解决冲突的优化手段。
更多推荐

所有评论(0)