并发4️⃣管程③Monitor
Monitor 是操作系统提供的、负责处理 synchronized 的组件。
在学习 Monitor 之前,要了解 Java 对象头 的概念。
1、对象组成
以 32 位虚拟机为例
Java 对象由三部分组成:对象头、实例数据、对齐补充。
1.1、对象头
对象头的结构
-
Mark Word:存储对象运行时记录(如 hashcode、GC 分代年龄、锁状态标志、线程 ID 等)。
-
Klass Word:元数据指针,指向方法区的 instanceKlass 实例。
-
array length:数组类型的对象特有,表示数组长度。
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.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
) -
指针关系
2、Monitor 原理(❗)
JVM 的同步是基于进入和退出 Monitor 对象来实现的。
- 每个 Java 对象都可以关联一个 Monitor 对象,Monitor 随着 Java 对象而创建和销毁。
- Monitor 对象是 C++ 实现的,依赖于操作系统。
- 会导致线程上下文切换,导致用户态和内核态的切换,影响性能,
2.1、Monitor
管程、监视器
-
当使用
synchronized
给对象加锁时(重量级),对象头中的 Mark Word 就被设置为指向 Monitor 对象的指针。 -
不加
synchronized
的对象不会关联 Monitor。 -
不同 Java 对象关联的是不同的 Monitor。
2.2、Monitor 结构
结构
含义 | 对应线程状态 | |
---|---|---|
owner | 锁的持有者,正在执行同步代码块 (同一时刻,Monitor 只能有一个 Owner) |
RUNNABLE |
EntryList | 由于锁已被其它线程获取,尝试获取锁失败的阻塞线程 | BLOCKED |
WaitSet | 锁的持有者由于条件不满足,主动释放锁,进入等待状态 | WAITING |
Waitset 和 EntryList 的区别
Waitset | EntryList | |
---|---|---|
线程状态 | WAITING | BLOCKED |
进入条件 | 已获得锁的线程,由于条件不满足而调用 wait() |
尝试获得锁时,由于锁已被其它线程获取而阻塞 |
唤醒时机 | Owner 调用 notify() 或 notifyAll() |
Owner 释放锁 |
注意 | 唤醒后不会直接成为 Owner,而是进入 EntryList 竞争锁 | 若有多个等待线程,则竞争锁(非公平) |
2.2、图解
-
以普通对象为例(非数组类型,区别在于对象头)
-
注:以下案例针对同一个对象实例的 Monitor。
synchronized(obj){ ... }
线程 t1 执行到
synchronized(obj){}
代码块
-
根据对象头的
Mark Word
找到关联的 Monitor。 -
判断 Monitor 的
Owner
为空,将其设置为当前线程 t1,进入临界区执行同步代码块(RUNNABLE
)
线程 t2 执行到
synchronized(obj){}
代码块
-
根据对象头的
Mark Word
找到关联的 Monitor,判断 Monitor 的Owner
不为空 -
t2 进入
EntryList
,变成BLOCKED
状态。 -
此时如果有 t3、t4 线程也执行到
synchronized(obj){}
,也会经历 t2 的过程。
若 Monitor 的
Owner
(t1)执行过程中,条件不满足(涉及wait/notify
)
-
t1 主动释放锁,进入
WaitSet
,变成WAITING
状态。 -
此时其它线程可以成为竞争锁,成为
Owner
。 -
若某个获取了 obj 对象锁的线程执行了
notify()
,说明 t1 的执行条件已满足。 -
线程 t1 退出
WaitSet
,进入EntryList
竞争锁(而不是直接成为Owner
)。
当线程 t1 执行完同步代码块内容
将 Owner
置为 null,唤醒 EntryList
中的阻塞线程。
-
若
EntryList
中只有 t2,则唤醒 t2 ,t2 获得锁。 -
若
EntryList
中有多个线程,则唤醒所有阻塞线程来竞争锁(RUNNABLE
)。-
竞争是非公平的(先阻塞的未必先获得)
-
竞争到锁的线程成为新的
Owner
,竞争失败的锁再次进入BLOCKED
状态。
-
3、synchronized 字节码
Monitor 加锁前会复制一份被加锁对象的引用,保证出现异常时也能正常解锁。
3.1、示例代码
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
3.2、字节码
-
仅展示字节码指令、异常表
-
常用字节码指令:字节码技术 2.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
3.2.1、正常流程分析
正常流程(关键步骤说明)
dup
:复制 lock 对象引用,存放到局部变量表(用于解锁)。monitorenter
:将对象关联 Monitor(Mark Word
设为 Monitor 对象地址),进入临界区执行同步代码块。monitorexit
:重置Mark Word
(Owner 设为 null),唤醒EntryList
的阻塞线程。- 没有异常,跳到第 24 行字节码指令,return 结束。
3.2.2、异常分析
JVM 根据异常表,监视 6-16、19-22 行的字节码指令。
任意一行指令发生 any 异常(任意类型),跳转到第 19 行。
- 将异常存储到 slot2
- 加载局部变量表 slot1 的 lock 引用,进行解锁。
- 抛出异常并结束。
3.3、说明
synchronized
修饰对象时,才会生成对应的 monitorenter/monitorexit 指令。synchronzied
修饰方法时,不会生成这对指令,而是生成一个ACC0SYNCHRONIZED
标志。