一、核心思想:为什么需要"锁升级"?
想象一下,你去一个几乎没人的公共卫生间:
- 你进去后,会特意把每个隔间都锁上吗?不会,你只需要找一个没人的隔间,象征性地关上门(甚至不关) 就行了。这很高效。
- 但如果人多了,开始排队了,你就得从里面把门锁上,防止别人误入。
- 再极端点,如果因为谁占坑太久引发了冲突,大家就得吵吵嚷嚷,甚至需要管理员(操作系统)来协调谁先谁后。
Java的锁也是这个道理。它认为大部分情况下,多线程竞争是不激烈的 。如果一开始就用最重量级的、需要调用操作系统功能的锁(像吵架找管理员),成本太高了。所以,JVM设计了一套聪明的策略:从低到高,逐步升级,根据竞争的激烈程度来选择合适的锁。
这套策略中的锁状态,就记录在Java对象的"身份证"里------对象头(Object Header) 。
二、锁的"身份证":对象头(Object Header)
每一个Java对象(包括new Object()
)在堆内存中都有两部分:
- 对象头(Header) :存储对象的元数据,包括锁信息、GC年龄、类型指针等。
- 实例数据(Instance Data) :对象各个字段的值。
- 对齐填充(Padding) :为了内存对齐,提高访问效率。
我们的焦点是对象头(Mark Word) 。在32位和64位JVM中,它的长度分别是32bit和64bit。它就像一个多功能瑞士军刀,在不同的状态下,表示不同的含义。下图是32位JVM下的Mark Word布局(64位类似,只是空间更大):
无锁状态下的Mark Word结构:
text
scss
|-------------------------------------------------------|--------------------|
| 25 bits (unused) | 4 bits |1 bit | 1 bit |
| | (age) | (biased) | (lock) |
|-------------------------------------------------------|--------------------|
| identity_hashcode | 0 | 0 | 01 |
|-------------------------------------------------------|--------------------|
identity_hashcode
: 对象的哈希码age
: 分代回收的年龄biased_lock
: 偏向锁标识位,0表示未开启偏向锁lock
: 锁标志位,01代表无锁状态
关键在于,当锁状态变化时,整个Mark Word的内容和意义都会发生改变!
锁的升级过程就是:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 。锁标志位(lock
)和偏向锁位(biased_lock
)共同决定了当前处于何种状态。
锁状态 | 偏向锁位(biased_lock) | 锁标志位(lock) | 组合值 |
---|---|---|---|
无锁 | 0 | 01 | 001 |
偏向锁 | 1 | 01 | 101 |
轻量级锁 | (不关心这个位) | 00 | 00 |
重量级锁 | (不关心这个位) | 10 | 10 |
GC标记 | (不关心这个位) | 11 | 11 |
三、逐层深入锁的升级过程
1. 偏向锁 (Biased Locking)
核心思想 :假设锁始终只由一个线程获得,不存在竞争。那么只需要在Mark Word里记录一下这个线程的ID,以后这个线程再来加锁,看一眼是自己的ID就直接通行,连一次CAS操作都省了,代价极低。
适用场景:真正无竞争或几乎无竞争的环境。
工作原理(代码视角) :
我们写一段简单的同步代码:
java
csharp
public class LockExample {
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) { // 第一次加锁,lock对象处于无锁状态
count++;
}
}
}
底层发生了什么(以第一次加锁为例) :
-
检查状态 :线程A进入
synchronized
块,发现lock
对象的Mark Word中锁标志是01
(无锁/偏向锁),且偏向锁位是0
(未偏向)。 -
CAS抢偏 :线程A发起一次CAS(Compare-And-Swap)操作,试图将Mark Word的前23位(32位JVM)替换成自己的线程ID,同时将偏向锁位设为1,锁标志位仍为01。
- CAS是一个CPU原子指令,可以理解为"我认为M的值应该是A,如果是,我就把它改成B;否则就不修改"。这个过程是硬件保证的,非常高效。
-
成功:如果CAS成功,Mark Word现在就变成了:
text
lua|-------------------------------------------------------|--------------------| | Thread A's ID (54 bits in 64bit JVM) | Epoch | age | 1 | 01 | |-------------------------------------------------------|--------------------|
现在,这个锁就偏向了线程A。
-
后续加锁 :以后只要线程A再次进入这个
synchronized
块,它只需要做一步:- 检查Mark Word里的线程ID是不是自己。
- 发现是,ok,直接通行!零次CAS操作,性能极高。
偏向锁的撤销 :
如果另一个线程B也来尝试加锁(发生了竞争),它检查Mark Word:
-
发现线程ID不是自己,而是线程A。
-
这表明存在竞争了,偏向锁需要撤销(Revoke Bias) 。
-
撤销过程需要等待一个全局安全点(JVM停顿所有线程的时刻),然后检查原持有偏向锁的线程A是否还活着或者还在同步块内。
- 如果线程A已经不活了或者不在同步块了,可以将锁对象重置为无锁状态(01) ,然后线程B再尝试重新竞争,此时可能会直接升级为轻量级锁。
- 如果线程A还在同步块内,说明竞争已经发生,直接将锁升级为轻量级锁。
注意 :Java 15以后,默认已经禁用了偏向锁(
-XX:-UseBiasedLocking
)。因为维护偏向锁和撤销带来的开销,在现代多核高并发应用中,往往弊大于利。但理解它对于掌握锁的完整演进至关重要。
2. 轻量级锁 (Lightweight Locking)
核心思想:当锁有轻微的竞争时(比如两个线程交替执行),避免直接动用操作系统的重量级锁。它采用一种"自旋"的方式在用户态尝试获取锁。
适用场景 :线程竞争是交替执行的,不存在同一时刻多个线程激烈竞争的情况。
工作原理(加锁) :
-
当发生竞争导致偏向锁撤销,或一个无锁对象首次遇到竞争时,JVM会准备升级到轻量级锁。
-
在当前线程的栈帧(Stack Frame) 中,开辟一块名为锁记录(Lock Record) 的空间。
-
将对象当前的Mark Word复制 到线程的锁记录中,官方称这个拷贝为Displaced Mark Word。
-
线程使用CAS操作 ,尝试将对象头的Mark Word替换为指向其栈中锁记录的指针 。如果成功,锁标志位变为
00
。-
此时对象头的Mark Word结构:
text
lua|-------------------------------------------------------| | ptr_to_lock_record (62 bits) | 00 | |-------------------------------------------------------|
-
-
如果第4步的CAS操作失败了怎么办?这意味着至少有两个线程同时在竞争这个锁。
- 失败线程会自旋(Spin) ------ 即在一个空循环里不停地重试CAS操作,期望持有锁的线程很快会释放锁。
- 自适应自旋:JVM会根据上次自旋是否成功来智能决定这次自旋的次数。如果上次成功了,这次就多自旋一会儿;如果上次失败了,这次可能干脆不自旋了。
工作原理(解锁) :
- 持有轻量级锁的线程执行完同步代码块后,使用CAS操作,想用之前拷贝的Displaced Mark Word去替换回对象头的Mark Word。
- 如果替换成功,整个同步过程就完成了,锁恢复到无锁状态(
01
)。 - 如果替换失败,说明在持有锁期间,已经有其他线程来竞争导致锁升级了 (比如自旋失败,升级为了重量级锁)。此时在解锁时,需要唤醒被挂起的线程,正式进入重量级锁的解锁流程。
3. 重量级锁 (Heavyweight Locking)
核心思想 :当竞争非常激烈,线程自旋一定次数后还是无法获取锁(浪费CPU),就升级为最重量级的锁。这需要依赖操作系统内核的互斥量(Mutex) 来实现。线程会从用户态陷入内核态,由操作系统负责线程的调度和阻塞/唤醒。
适用场景:高并发、线程持有锁时间较长、竞争激烈的场景。
工作原理:
-
当轻量级锁自旋失败后,锁就会升级。
-
JVM会向操作系统申请一个互斥量(Mutex) ,同时在对象头的Mark Word中,用一个指针指向这个互斥量和相关的监视器(Monitor) 结构。锁标志位变为
10
。-
此时对象头的Mark Word结构:
text
lua|-------------------------------------------------------| | ptr_to_heavyweight_monitor (62 bits) | 10 | |-------------------------------------------------------|
-
-
这个Monitor (在JVM里由
ObjectMonitor
类实现,C++代码)是重量级锁的核心。它内部维护着几个关键队列:_EntryList
:等待获取锁的线程队列(竞争失败后进来的)。_WaitSet
:调用了wait()
方法而等待的线程队列。
-
当一个线程(比如线程C)尝试获取重量级锁失败后,它会被操作系统挂起(Park) ,然后放入
_EntryList
中,等待被唤醒。这个过程涉及从用户态到内核态的切换,成本非常高。 -
当持有锁的线程释放锁时,它会去
_EntryList
中唤醒一个线程(比如线程D)。这个被唤醒的线程D会尝试重新获取锁。注意:唤醒操作同样涉及内核态切换。
ObjectMonitor
的核心源码概念(hotspot源码):
cpp
arduino
// 位于 hotspot/src/share/vm/runtime/objectMonitor.hpp
class ObjectMonitor {
...
void* volatile _owner; // 指向当前持有锁的线程( owner == null 表示未锁定)
volatile jlong _count; // 重入次数
volatile int _waiters; // 等待的线程数
protected:
ObjectWaiter* volatile _EntryList; // 等待获取锁的线程列表(竞争失败)
ObjectWaiter* volatile _WaitSet; // 调用了 wait() 的线程列表
...
// 获取锁
void ATTR ObjectMonitor::enter(TRAPS);
// 释放锁
void ATTR ObjectMonitor::exit(TRAPS);
};
synchronized
的monitorenter
和monitorexit
指令,在重量级锁模式下,最终就会调用到ObjectMonitor
的enter
和exit
方法。
四、总结与全景图
现在,让我们把整个过程串联起来,形成一张完整的锁升级全景图:
Diagram
Code
css
flowchart TD
A[无锁状态 (001)] -->|第一个线程访问| B[尝试偏向锁]
B -->|CAS成功| C[偏向锁状态 (101)]
C -->|同一线程再次访问| C["✅ 直接进入 (零成本)"]
C -->|其他线程访问<br>发生竞争| D[撤销偏向锁]
D -->|原线程已退出| A
D -->|原线程未退出| E[升级为轻量级锁]
A -->|有竞争| E
E[轻量级锁状态 (00)] -->|线程CAS将Mark Word指向栈中锁记录| F[成功]
F -->|执行同步代码| G[解锁:CAS还原Mark Word]
G -->|成功| A
G -->|失败| H[升级为重量级锁]
E -->|CAS失败| I[自旋重试]
I -->|自旋成功| F
I -->|自旋失败| H
H[重量级锁状态 (10)] -->|Mark Word指向Monitor| J[线程进入_EntryList]
J -->|被OS挂起| J
J -->|被owner线程唤醒| K[竞争锁]
K -->|成功| L[成为owner]
L -->|执行同步代码| M[解锁并唤醒_EntryList线程]
M --> H
五、如何观察和验证?
-
使用JOL工具(Java Object Layout) :
这是一个开源库,可以让你直接查看一个对象在内存中的布局,包括Mark Word。
java
less// 添加依赖:org.openjdk.jol:jol-core import org.openjdk.jol.vm.VM; import org.openjdk.jol.info.ClassLayout; Object obj = new Object(); System.out.println(VM.current().details()); // 输出VM详情 System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // 输出对象内存布局
通过这个工具,你可以清晰地看到加锁前后Mark Word的变化。
-
看汇编字节码 :
使用
javap -c YourClass
反编译你的class文件,可以看到monitorenter
和monitorexit
指令。但请注意,这只是字节码层面的指令,具体的锁升级逻辑是在JVM执行这些指令时(C++代码)实现的。
希望这次超详细的讲解,能让你对Java锁的底层实现有一个深刻而清晰的认识。它从一个简单的CAS开始,根据竞争的残酷程度,一步步演化,最终不得不求助操作系统内核。这种设计思想本身就是一种极致的性能优化艺术。