并发4️⃣管程③Monitor

Monitor 是操作系统提供的、负责处理 synchronized 的组件。

在学习 Monitor 之前,要了解 Java 对象头 的概念。

1、对象组成

以 32 位虚拟机为例

Java 对象由三部分组成:对象头、实例数据、对齐补充。

image-20220328180944313

1.1、对象头

对象头的结构

  • Mark Word:存储对象运行时记录(如 hashcode、GC 分代年龄、锁状态标志、线程 ID 等)。

  • Klass Word元数据指针,指向方法区的 instanceKlass 实例。

  • array length:数组类型的对象特有,表示数组长度。

    image-20220328181316137

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

  • 指针关系

    image-20220328205818924

2、Monitor 原理(❗)

JVM 的同步是基于进入和退出 Monitor 对象来实现的。

  • 每个 Java 对象都可以关联一个 Monitor 对象,Monitor 随着 Java 对象而创建和销毁。
  • Monitor 对象是 C++ 实现的,依赖于操作系统。
  • 会导致线程上下文切换,导致用户态和内核态的切换,影响性能,

2.1、Monitor

管程、监视器

  • 当使用 synchronized 给对象加锁时(重量级),对象头中的 Mark Word 就被设置为指向 Monitor 对象的指针。

  • 不加 synchronized 的对象不会关联 Monitor。

  • 不同 Java 对象关联的是不同的 Monitor。

    image-20220403171009349

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){} 代码块

  1. 根据对象头的 Mark Word 找到关联的 Monitor

  2. 判断 Monitor 的 Owner 为空,将其设置为当前线程 t1,进入临界区执行同步代码块RUNNABLE

    image-20220328210244324

线程 t2 执行到 synchronized(obj){} 代码块

  1. 根据对象头的 Mark Word 找到关联的 Monitor,判断 Monitor 的 Owner 不为空

  2. t2 进入 EntryList,变成 BLOCKED 状态。

  3. 此时如果有 t3、t4 线程也执行到 synchronized(obj){},也会经历 t2 的过程。

    image-20220328210424107

若 Monitor 的 Owner(t1)执行过程中,条件不满足(涉及 wait/notify

  1. t1 主动释放锁,进入 WaitSet,变成 WAITING 状态。

    image-20220328210813929

  2. 此时其它线程可以成为竞争锁,成为 Owner

  3. 若某个获取了 obj 对象锁的线程执行了 notify(),说明 t1 的执行条件已满足。

  4. 线程 t1 退出 WaitSet,进入 EntryList 竞争锁(而不是直接成为 Owner)。

当线程 t1 执行完同步代码块内容

Owner 置为 null,唤醒 EntryList 中的阻塞线程。

  1. EntryList 中只有 t2,则唤醒 t2 ,t2 获得锁。

  2. EntryList 中有多个线程,则唤醒所有阻塞线程来竞争锁RUNNABLE)。

    • 竞争是非公平的(先阻塞的未必先获得)

    • 竞争到锁的线程成为新的 Owner,竞争失败的锁再次进入 BLOCKED 状态。

      image-20220328211100831

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、正常流程分析

正常流程(关键步骤说明)

  1. dup复制 lock 对象引用,存放到局部变量表(用于解锁)。
  2. monitorenter:将对象关联 MonitorMark Word 设为 Monitor 对象地址),进入临界区执行同步代码块。
  3. monitorexit:重置 Mark WordOwner 设为 null),唤醒 EntryList 的阻塞线程
  4. 没有异常,跳到第 24 行字节码指令,return 结束。

3.2.2、异常分析

JVM 根据异常表,监视 6-16、19-22 行的字节码指令。

任意一行指令发生 any 异常(任意类型),跳转到第 19 行。

  1. 将异常存储到 slot2
  2. 加载局部变量表 slot1 的 lock 引用,进行解锁
  3. 抛出异常并结束。

3.3、说明

  1. synchronized 修饰对象时,才会生成对应的 monitorenter/monitorexit 指令。
  2. synchronzied 修饰方法时,不会生成这对指令,而是生成一个 ACC0SYNCHRONIZED 标志。
posted @ 2022-04-16 17:57  Jaywee  阅读(117)  评论(0编辑  收藏  举报

👇