并发编程④共享:Monitor、锁优化
1、Monitor
Monitor 是操作系统提供的,负责处理 synchronized 的组件。
在学习 Monitor 之前,要了解 Java 对象头 的概念。
1.1、对象组成
以 32 位虚拟机为例
Java 对象由三部分组成:对象头、实例数据、对齐补充。

1.1.1、对象头
对象头的结构如下
-
Mark Word:存储对象运行时记录(hashcode、GC 分代年龄、锁状态标志、线程 ID 等)
-
Klass Word:元数据指针,指向方法区的 instanceKlass 实例(稍后讲解)
-
array length:数组对象特有,表示数组长度

1.1.2、Mark Word(!)
对象处于不同状态时,Mark Word 有所不同。
末尾 2 位表示锁标记(不加锁、偏向锁、轻量级锁、重量级锁、垃圾回收标志)
-
32 位虚拟机
|---------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |---------------------------------------------------|--------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | Normal | |---------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased | |---------------------------------------------------|--------------------| | ptr_to_lock_record:30 | 00 | Lightweight Locked | |---------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked | |---------------------------------------------------|--------------------| | | 11 | Marked for GC | |---------------------------------------------------|--------------------| -
64 位虚拟机
|-------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |-------------------------------------------------------|--------------------| |unused:25|hashcode:31|unused:1|age:4|biased_lock:0| 01 | Normal | |-------------------------------------------------------|--------------------| | thread:54 | epoch:2 |unused:1|age:4|biased_lock:1| 01 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | 11 | Marked for GC | |-------------------------------------------------------|--------------------|
1.1.3、oop-klass 模型(*)
Hotspot 使用 oop-klass 模型 表示 Java 类和对象。
| instanceKlass | instanceOopDesc | |
|---|---|---|
| 创建时期 | 类加载阶段,JVM 将字节码文件(.class)加载到方法区 |
new 创建对象时,JVM 会创建 instanceOopDesc |
| 说明 | 表示类的元数据,包括常量池、成员变量、方法等结构 | 表示对象实例,实例存放在堆区,对象引用存放在栈区 |
| 指针维护 | 属性 _java_mirror 存储 java.lang.Class 实例的地址 |
对象头的 Klass Word 存储 instanceKlass 的地址 |
代表
java.lang.Class的对象实例
JVM 不把 instanceKlass 暴露给 Java,而是创建一个单独的 instanceOopDesc 实例(代表 java.lang.Class 对象)
-
该对象实例称为 instanceKlass 的 Java 镜像
-
instanceKlass 维护了一个指向镜像的指针(即
_java_mirror) -
指针关系

1.2、Monitor 原理
1.2.1、说明
Monitor(管程、监视器)
每个 Java 对象都可以关联一个 Monitor。
- 当使用
synchronized给对象加锁时(重量级),对象头中的 Mark Word 就被设置为指向 Monitor 对象的指针。 - 即
ptr_to_heavyweight_monitor,且锁标志为10。 - 不加
synchronized的对象不会关联 Monitor,不同对象关联独有的 Monitor。
Monitor 结构
| 含义 | 对应线程状态 | |
|---|---|---|
| owner | 锁的持有者,正在执行同步代码块 | RUNNABLE |
| EntryList | 由于锁已被其它线程获取,尝试获取锁失败的阻塞线程 | BLOCKED |
| WaitSet | 锁的持有者由于条件不满足,主动释放锁,进入等待状态 | WAITING |
1.2.1、图解 Monitor
- 以普通对象为例(非数组类型,区别在于对象头)
- 注:以下案例针对同一个对象实例的 Monitor。
线程 t1 执行到
synchronized(obj){}代码块
-
根据对象头的 Mark Word 找到关联的 Monitor。
-
判断 Monitor 的 Owner 为空,将其设置为当前线程 t1,进入临界区执行同步代码块(
RUNNABLE)
线程 t2 执行到
synchronized(obj){}代码块(同一时刻,Monitor 只能有一个 Owner)
-
根据对象头的 Mark Word 找到关联的 Monitor,判断 Monitor 的 Owner 不为空
-
t2 进入 EntryList,变成
BLOCKED状态。 -
此时如果有 t3、t4 线程也执行到
synchronized(obj){},也会经历 t2 的过程。
若 Monitor 的 Owner(线程 t1)的执行条件不满足(涉及
wait/notify)
-
t1 主动释放锁,进入 WaitSet,变成
WAITING状态
-
此时其它线程可以成为竞争锁,成为 Owner。
-
等待某个获取同一个对象锁的线程执行了
notify()(表示条件满足) -
线程 t1 退出 WaitSet,进入 EntryList 竞争锁(而不是直接成为 Owner)。
线程 t1 执行完同步代码块内容,将 Owner 置为 null,唤醒 EntryList 中的阻塞线程
-
若 EntryList 中只有 t2,则 t2 获得锁。
-
若 EntryList 中有多个线程,则 t1 会唤醒所有阻塞线程来竞争锁(
RUNNABLE)。-
竞争是非公平的(先阻塞的未必先获得)
-
竞争到锁的线程成为新的 Owner,竞争失败的锁再次进入
BLOCKED状态。
-
1.3、synchronized 字节码
1.3.1、示例代码
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
1.3.2、字节码
-
仅展示字节码指令、异常表
-
常用字节码指令:
Stack=2, locals=3, args_size=1 0 getstatic #2 3 dup # 复制 4 astore_1 # 将复制的引用至存储到slot1 5 monitorenter # 将lock对象的Mark Word置为指向Monitor指针 6 getstatic #3 9 iconst_1 10 iadd 11 putstatic #3 14 aload_1 15 monitorexit # 将lock对象的Mark Word重置,唤醒EntryList 16 goto 24 19 astore_2 # 将异常存储到slot2 20 aload_1 # 加载slot1的对象引用 21 monitorexit 22 aload_2 23 athrow # 抛出异常 24 return Exception table: from to target type 6 16 19 any 19 22 19 any
正常流程(关键步骤)
dup:复制 lock 对象引用,存放到局部变量表(用于解锁)。monitorenter:将对象关联 Monitor(Mark Word 设为 Monitor 对象地址),进入临界区执行同步代码块。monitorexit:重置 Mark Word(Owner 设为 null),唤醒 EntryList 的阻塞线程。- 没有异常,跳到第 24 行字节码指令,return 结束。
发生异常(参考异常表)
JVM 会监视 6-16、19-22 行的字节码指令。
任意一行指令发生 any 异常(任意类型),跳转到第 19 行。
- 将异常存储到 slot2
- 加载局部变量表 slot1 的 lock 引用,进行解锁。
- 抛出异常并结束。
1.3.3、说明
结构
- JVM 为每个线程分配一个独有的栈区,为每个方法分配一个栈帧。
- 栈帧主要结构:操作数栈(stacks)、局部变量表(locals)、锁记录(lock record)
加锁过程
- 加锁前会复制一份被加锁对象的引用,保证出现异常时也能正常解锁。
monitorenter和monitorexit成对出现,对应加锁和解锁操作。
2、JVM 中的锁优化
synchronized 是重量级锁,并发性能低。
为了提高并发时的系统吞吐量,JVM 提供了锁优化策略。
- 加锁策略:不加锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 其它策略:自旋、锁粗化、锁消除等
2.1、偏向锁
2.1.1、Biased locking
思想:当一个线程获取锁时进入偏向模式,同一个线程再次请求锁时,无需做同步操作。
-
优点:节省大量有关锁申请(如 CAS)的操作,提高性能。
-
适用场合:没有锁竞争,只有一个线程对该对象加锁。
- 锁竞争:有其它线程也对该对象加锁。
- 发生不同时刻的锁竞争时升级为轻量级锁,发生同一时刻的锁竞争时膨胀为重量级锁。
-
虚拟机参数
# 开启偏向锁(默认开启) -XX:+UserBiasedLocking # 偏向锁开启延时(JDK10前默认4000ms,之后默认0) -XX:BiasedLockingStartupDelay=毫秒 -
锁标志:Mark Word 末尾三位为
101(不加锁是001,区别在于倒数第三位)
2.1.2、Mark Word 情况
若没开启偏向锁
对象实例化时
- 末尾三位:
001。 - age 为 0:垃圾回收时才会设置。
- Hashcode 为 0:第一次用到 hashCode 才会设置。
若有开启偏向锁(默认开启)
-
对象实例化:
- 末尾三位:
101。 - Thread、epoch、age 为 0:加锁 / 垃圾回收时才设置。
- 末尾三位:
-
偏向锁延迟开启
- 在 JDK 10 之前默认 4000ms,即程序启动 4s 后才会开启。
- 从 JDK 10 开启调整为 0,可使用虚拟机参数手动设置。
2.1.3、锁撤销
偏向锁在没有发生锁竞争时生效。
-
发生撤销的情况
说明 原因 获取对象 hashcode Mark Word 中需要存储 hashcode 偏向锁状态的对象的 Mark Word 中存储的是 thread 和 epoch 发生不同时刻的锁竞争 升级为轻量级锁 发生同一时刻的锁竞争 膨胀为重量级锁 调用 wait/notify膨胀为重量级锁 涉及到 Monitor 结构 -
对撤销的优化:撤销次数达到阈值时,会触发批量重偏向或批量撤销。
- 默认阈值:批量重偏向 20,批量撤销 40。
撤销次数 = 阈值 - 1,就认为达到了阈值。
相关说明
关于偏向锁的 JVM 结构
revocation_count:撤销计数器BiasdLockingDecayTime:即重新开启偏向锁的时间(默认 25000ms)
其它说明
- 撤销次数是针对类计数,而不是对象实例。
- 重偏向不增加撤销次数。
- 批量重偏向的对象,在规定时间(即
BiasLockingDecayTime)内无法再次批量重偏向至新的线程。
2.1.4、批量重偏向
思想
场合:同一个类的对象实例发生不同时刻的锁竞争。
- 撤销次数达到阈值后(默认 20)不再进行撤销操作。
- 而是将该类剩余的需要被加锁的实例,批量重偏向至另一个线程。
示例理解
用 Vector 存储 User 对象,模拟多线程访问相同的对象实例。
Vector userVector = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
User user = new User();
userVector.add(user);
synchronized (user) {
}
}
}, "t1");
t1.start();
// 保证t1和t2在不同时刻加锁(否则膨胀为重量级锁)
t1.join();
new Thread(() -> {
for (int i = 0; i < 39; i++) {
User user = userVector.get(i);
synchronized (user) {
}
}
}, "t2").start();
线程 t1:对 100 个 user 对象实例加锁
- 100 个 User 都从无锁变成偏向锁。
- Mark Word 后三位为
101,thread 为 t1。
线程 t2:对前 39 个 user 对象加锁,分析如下
- user1-19:撤销偏向锁,加锁为轻量级锁(
00),解锁后设为禁用偏向锁(001) - user20-39:达到默认阈值 20 不再撤销,将之后此线程需加锁的所有实例都重偏向至 t2(
101,thread 置为 t2)
全程的对象状态
# t1
user1-100
加锁前: 001 无锁
加锁中: 101 偏向锁,thread为t1
解锁后: 101 偏向锁,thread为t1
# t2
# user1-19:撤销,升级为轻量级锁,禁用偏向锁
加锁前: 101 偏向锁,thread为t1
加锁中: 00 轻量级锁
解锁后: 001 禁用偏向锁
# user20-39:批量重偏向至 t2
加锁前: 101 偏向锁,thread为t1
加锁中: 101 偏向锁,thread为t2
解锁后: 101 偏向锁,thread为t2
# user40-100
101 偏向锁,thread为t1
2.1.5、批量撤销
思想
场合:同一个类的对象实例发生不同时刻的锁竞争。
- 撤销次数达到阈值后(默认 40)不再进行撤销或重偏向操作。
- 而是撤销该类所有实例的偏向锁,并禁用该类的偏向锁。
示例理解
在批量重偏向的例子中,增加 t3 线程。
Vector userVector = new Vector<>();
// 保证t1和t2在不同时刻加锁(否则膨胀为重量级锁)
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
User user = new User();
userVector.add(user);
synchronized (user) {
}
}
}, "t1");
t1.join();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 39; i++) {
User user = userVector.get(i);
synchronized (user) {
}
}
}, "t2");
// 保证t2和t3在不同时刻加锁(否则膨胀为重量级锁)
t2.join();
new Thread(() -> {
for (int i = 0; i < 39; i++) {
User user = userVector.get(i);
synchronized (user) {
}
}
}, "t3").start();
线程 t1:对 100 个 User 对象实例加偏向锁(101,thread 为 t1)
线程 t2:对 UserVector 前 39 个对象加锁(前 19 个禁用偏向锁,20-39 是 t2 偏向锁)
线程 t3:对前 40 个 user 对象加锁,分析如下
- user1-19:t2 加锁时已设为禁用偏向锁,加锁时为轻量级锁,解锁时仍为禁用偏向锁。
- user20-38:(共 19 个)撤销偏向锁,加锁为轻量级锁(
00),解锁后设为禁用偏向锁(001) - user39:达到批量重偏向阈值,但未超过 BiasedLockingDecayTime
- 无法二次重偏向(操作同上,加轻量级锁,禁用偏向锁)。
达到阈值 40
- 撤销次数共 39 次,达到阈值。
- 撤销该类所有实例的偏向锁,并禁用该类的偏向锁。
- 此后创建该类的实例对象,也是处于禁用偏向锁的状态(
001)。
全程的对象状态
# t1
# user1-100
加锁前: 001 无锁
加锁中: 101 偏向锁,thread为t1
解锁后: 101 偏向锁,thread为t1
# t2
# user1-19:撤销,升级为轻量级锁,禁用偏向锁
加锁前: 101 偏向锁,thread为t1
加锁中: 00 轻量级锁
解锁后: 001 禁用偏向锁
# user20-39:批量重偏向至 t2
加锁前: 101 偏向锁,thread为t1
加锁中: 101 偏向锁,thread为t2
解锁后: 101 偏向锁,thread为t2
# user40-100
101 偏向锁,thread为t1
# t3
# user1-19:保持禁用偏向锁
加锁前: 001 禁用偏向锁
加锁中: 00 轻量级锁
解锁后: 001 禁用偏向锁
# user20-38:撤销,升级为轻量级锁,禁用偏向锁
加锁前: 101 偏向锁,thread为t2
加锁中: 00 轻量级锁
解锁后: 001 禁用偏向锁
# user39:达到批量重偏向阈值,但已被批量重偏向过
加锁前: 101 偏向锁,3的thread为t2
加锁中: 00 偏向锁,轻量级锁
解锁后: 001 禁用偏向锁
# user40-100
001 禁用偏向锁
2.2、轻量级锁
当没有发生锁竞争时,偏向锁生效。
思想:当发生不同时刻的锁竞争时,使用轻量级锁(而不是直接膨胀为重量级锁)
-
适用场合:发生不同时刻的锁竞争
- 多线程在不同时间对同一个对象加锁
- 若多线程在同一时刻尝试加锁,则膨胀为重量级锁。
-
锁标志:Mark Word 保存锁记录对象地址,末尾两位为
00
2.2.1、锁记录
锁记录:每个线程的栈帧中的空间结构
锁记录对象的结构
-
锁记录地址、锁标志

-
对象引用:存储当前线程正加锁的对象
2.2.2、加锁过程
- 创建 Lock Record → CAS → 成功
- 创建 Lock Record → CAS → 失败 → 自旋 → 成功或膨胀
创建 Lock Record
-
线程执行到
synchronized(user){}代码块,在当前栈帧(Frame)创建锁记录对象(Lock Record)。 -
Object Reference 保存
user地址。
CAS
-
尝试用锁记录地址,交换
user的 Mark Word
-
CAS 替换成功则加轻量级锁成功,此时
-
Lock Record中存储了user的原 Mark Word -
user的 Mark Word 存储了锁记录地址和锁标志(即ptr_to_lock_record00)
-
CAS 失败的情况
假设线程 t1 已对 user 加轻量级锁,且未释放
User user = new User();
new Thread(()->{
synchronized(user){
// 轻量级锁
}
},"t1").start;
分析以下情况
-
case1:线程 t2 尝试加轻量级锁
new Thread(()->{ synchronized(user){ // 锁膨胀(先自旋) } },"t2").start;- 创建
Lock Record。 - 尝试 CAS,但
user的 Mark Word 已存储锁记录地址,CAS 失败。 - 查看锁记录地址,发现不属于当前线程(即产生同一时刻的锁竞争)
- 进入锁膨胀过程(稍后讲解)。
- 创建
-
case2:t1 再次该对象加锁(锁重入)
-
创建
Lock Record。 -
尝试 CAS,但
user的 Mark Word 已存储锁记录地址,CAS 失败。 -
查看锁记录地址,发现属于当前线程 (锁重入)
-
重置
Lock Record的锁记录地址(置为 null),作为重入计数。
-
2.2.3、解锁过程
线程 t1 退出
synchronized代码块时,查看当前栈帧中Lock Record的头部。
- null:说明是锁重入。
- 非 null:存储的是对象实例的原 Mark Word,通过 CAS 恢复给对象实例。
- CAS 成功:轻量级锁解锁成功。
- CAS 失败:说明轻量级锁已膨胀,需进入重量级锁解锁流程。
2.2.4、锁膨胀 & 锁自旋
锁膨胀
锁膨胀:将轻量级锁变成重量级锁。
场景:线程尝试加轻量级锁时 CAS 失败,其它线程已获取该对象的轻量级锁。
线程 t2 尝试加轻量级锁失败,进入锁膨胀过程
- t2 为
user申请 Monitor,让user的 Mark Word 指向 Monitor 对象地址。 - t2 进入 EntryList(
BLOCKED)。 - t1 执行完临界区代码,退出
synchronized代码块时 CAS 失败。 - t1 进入重量级锁解锁流程
- 通过
Lock Record的对象引用,根据user的 Mark Word 找到 Monitor 对象。 - 将 Owner 置为 null,唤醒 EntryList 中的 t2(即 Monitor 原理)。
- 通过
自旋
发生同一时刻的锁竞争时,轻量级锁会膨胀为重量级锁。
- 导致当前线程进入 EntryList,变成阻塞状态(
BLOCKED) - 导致线程上下文切换,影响性能。
因此在锁膨胀之前,JVM 会尝试自旋优化。
- 让当前线程做若干次空循环(即自旋),而不是直接阻塞。
- 若在此期间,轻量级锁被持有者释放,则当前线程成功获得锁。
说明
- 自旋会消耗 CPU:自旋的做法是使线程执行空循环,在此期间消耗一定的 CPU 资源。
- 多核 CPU 下自旋才有效:至少有一个 CPU 用于执行 owner 的线程,另一个 CPU 用于当前线程的自旋。
- Java 6 后自旋锁自适应(自动调整自旋次数),Java 7 后默认开启自旋功能。
2.3、锁消除 & 锁粗化
锁消除是 JVM 的 技术。
JVM 执行引擎
- 当程序启动时,解释器首先发挥作用,直接将代码解释执行(省去编译的时间)。
- 随着代码执行次数越来越多,JIT 编译器开始发挥作用,将热点代码编译成本地机器码,提高执行效率。
锁消除(Lock Elimination)
以线程安全的 StringBuffer 类为例。
-
变量 result 是局部变量,只会在方法内部被调用,不存在线程安全问题。
-
JVM 在运行时自动将 StringBuffer 对象内部的锁消除。
public void add(String str1, String str2) { StringBuffer result = new StringBuffer(); result.append(str1).append(str2); }
锁粗化(Lock Coarsening)
仍以 StringBuffer 类为例。
-
假如没有锁粗化,以下代码会对同一个对象进行 10000 次加锁和解锁操作,浪费资源。
-
JVM 在运行时会将加锁的范围粗化(比如对整个循环体加锁),使得整个操作只进行 1 次加锁和解锁操作。
StringBuffer result = new StringBuffer(); public String add(String str) { for (int i = 0; i < 10000; i++){ result.append(str) } return result.toString(); }