写点什么

面试官 synchronized 连环问,学会 Monitor 之后轻松拿下

作者:李子捌
  • 2021 年 12 月 08 日
  • 本文字数:3178 字

    阅读完需:约 10 分钟

面试官synchronized连环问,学会Monitor之后轻松拿下

1、简介

我们 Java 程序员编码时谈论的最多的两个字就是对象,Java 中几乎所有的技术都是围绕对象展开。本文将要讲述的 Monitor 并不是 Java 对象,而是在操作系统中关联的“对象”,Monitor 是 Java 重量级锁 synchronized 实现的关键,因此学习 Java 单机同步机制就离不开对 Monitor 的剖析。Monitor 经常被人们称为监视器锁和管程。

2、对象头

Monitor 与 Java 对象头相关联,因此剖析 Monitor 之前必须了解 Java 对象的组成结构。Java 对象在内存中由三部分组成,分别是对象头、实例数据、对齐填充。以 32 位虚拟机为例(64 位不同),对象头(Header)占 8 个字节共 64 位(数组对象头与普通对象头不同,数组对象头 12 个字节共 96 位);实例数据(Instance Data)存储这对象的实际数据,因此大小与实际数据大小一致;对齐填充(Padding)是可选项,用于将内存对齐为 8 字节的整数倍。​

普通对象内存组成

对象内存结构.drawio.png

对象内存结构.drawio.png


数组对象内存组成

数组对象内存结构.drawio.png

数组对象内存结构.drawio.png

如上两张图展示了 Java 对象内存结构,本文说的 Monitor 和这个有啥关系呢?其实对象头(Header)中的 Mark Word 就是用来存放 Monitor 对象的指针的,在一开始小捌就说了 Monitor 并不是 Java 对象,而是在操作系统中关联的“对象”,因此 Java 对象如果想要和 Monitor 进行关联,就必须在 Java 对象中记录 Monitor 的内存地址,这样才能通过 Java 对象找到这个 Monitor 嘛!


注:Klass Word 存放的是指向对象对应的 Class 对象的指针。

3、Mark Word

可想而知,想要深入探讨 Monitor 肯定避不开 Mark Word,这个时候暴躁的程序员小哥肯定不爽了,你特么不是说 Mark Word 中存放的 Monitor 的内存地址么,我知道了啊……别急,并不是想的那样的,这里稍微有一丢丢复杂,听我慢慢道来。Java 对象在不同的状态下,Mark Word 存储的值完全不同,尤其是在 JDK1.6 对锁优化之后,Mark Word 这 32bits 内存空间,真的是被 Java 大师们压榨到了极致。了解这个需要有一定的 JVM 和 synchronized 知识,如果不懂的话也无所谓,先了解就好,后面我们一起学习 synchronized 锁升级过程。​

初始状态下 Java 对象头的 Mark Word 里默认存储的是对象的 hashcode、GC 分代年龄、是否偏向锁和锁标志位

Java对象头MarkWord初始情况.drawio.png

Java 对象头 MarkWord 初始情况.drawio.png

偏向锁 Java 对象头的 Mark Word 存储结构如下

Java对象头MarkWord整体情况-32位偏向锁.drawio.png

Java 对象头 MarkWord 整体情况-32 位偏向锁.drawio.png

轻量级锁 Java 对象头的 Mark Word 存储结构如下

Java对象头MarkWord轻量级锁情况.drawio.png

Java 对象头 MarkWord 轻量级锁情况.drawio.png

重量级锁 Java 对象头的 Mark Word 存储结构如下

Java对象头MarkWord重量级锁情况.drawio.png

Java 对象头 MarkWord 重量级锁情况.drawio.png

4、Monitor

上面铺垫了这么多东西,其实就是为了讲述 Monitor 和 Java 对象头中 Mark Word 的关系,可以看出来只有在重量级锁的情况下 Java 对象头中 Mark Word 才会关联一个 Monitor 对象,那么 Monitor 又是个什么东西呢?我相信你一定很好奇吧!​

Monitor 内部分由三部分组成分别是 Owner、EntryList、WaitSet;

  • Owner 用于记录当前 Monitor 的所属线程

  • EntryList 是一个链表结构,用于记录阻塞在当前锁对象上的线程

  • WaitSet 用于记录获取锁之后进入 Waiting 状态的线程

monitor初始状态.drawio.png

monitor 初始状态.drawio.png

当对象锁发生锁竞争时,在同一时刻只有一个线程能够获取到锁,其他线程会进入阻塞(BLOCKED)状态,此时这些被阻塞的线程就会进入 EntryList 中等待锁持有者释放锁后被唤醒,再次参与锁竞争(非公平)。如下所示,当 Thread1 持有锁时,Thread2、Thread3、Thread4 均无法获取锁从而进入阻塞队列,等待 Thread1 执行完同步代码块之后通知阻塞队列中等待的线程重新竞争锁,竞争成功的线程成为锁拥有者,失败的线程继续在阻塞队列中阻塞。

monitor+blocked.drawio.png

monitor+blocked.drawio.png

当对象获取到锁之后,由于某些资源并未准备完成,需要等待其他线程去准备资源,此时线程会通过 wait()/notify()等方法进入等待/通知模式,在这种情况下线程释放锁之后会进入 WaitSet,当其他线程准备好资源之后会通知 WaitSet 中等待的线程,WaitSet 中的线程会进入到 EntryList 中,重新参与锁竞争。

monitor+waiting.drawio.png

monitor+waiting.drawio.png

5、monitorente && monitorexit

知道了 Monitor 是什么,也知道了 Java 对象与 Monitor 之间的关系,但是还有一层疑问;程序在运行过程中是如何知道要给 Java 对象去关联一个 Monitor 呢?这就需要一点点 Java 字节码相关的知识了,Java 的源代码在编译器编译之后生成的 Class 文件中存储的是字节码指令,程序执行本质上是一条条指令按照既定顺序的流水线工作,那这只有一种可能了,编译器在编译成 Java 字节码时做了记号,这个记号就是 monitorente /monitorexit。​

我们先来看一段简单的 synchronized 代码块:

/** * @Author: Liziba */public class MonitorDemo {
    static final Object LOCK = new Object();    static int count = 0;
    public static void main(String[] args) {
        synchronized (LOCK) {            count++;        }
    }
}
复制代码

然后在 IDEA 中借助 ByteCode Viewer 插件查看类的字节码指令(安装后如果插件未失效可以重启 IDEA)

image.png


在 Toolbar 中点击 View 选择 Show Bytecode With Jclasslib

image.png


此时找到 synchronized 关键字所在的 main 方法,选择字节码页签,即可看到 main 方法的字节码指令了。

image.png


字节码指令如下:


 0 getstatic #2 <com/lzb/concurrency/demo2/MonitorDemo.LOCK : Ljava/lang/Object;> 3 dup             4 astore_1 5 monitorenter 6 getstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I> 9 iconst_110 iadd11 putstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I>14 aload_115 monitorexit16 goto 24 (+8)19 astore_220 aload_121 monitorexit22 aload_223 athrow24 return
复制代码

前三行字节码分别表示:

  • getstatic 获取静态锁对象 LOCK

  • dup 复制一份 LOCK 对象的引用,用于锁退出

  • astore_1 复制的引用存入临时变量 1 中 3 dup reference -> slot 1

 0 getstatic #2 <com/lzb/concurrency/demo2/MonitorDemo.LOCK : Ljava/lang/Object;> 3 dup             4 astore_1
复制代码

synchronized 临界区七行字节码分别表示:

  • monitorenter 关联一个操作系统 Monitor 对象,替换 LOCK 对象的 Mark Word 为 Monitor 地址

  • getstatic 获取静态变量 count

  • iadd count++操作

  • putstatic 赋值++操作后的 count

  • aload_1 获取 LOCK 对象的引用,上面 dup 复制后 astore_1 指令存储的那份地址

  • monitorexit 还原 Mark Word,将 Monitor 对象指针替换为 monitorenter 加锁时保存在 Monitor 对象中的数据,如 hashcode、分代年龄等数据;同时唤醒等待在 EntryList 中阻塞等待的线程。

 5 monitorenter 6 getstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I> 9 iconst_110 iadd11 putstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I>14 aload_115 monitorexit
复制代码

最后几条指令涉及到异常产生时 JVM 释放锁的处理和方法返回,首先要看一个异常表:

image.png


异常表中有两行记录:


  • 第一行表示:6 -> 16 行字节码中发生了异常,会跳转到 19 行,这就是 synchronized 加锁的代码区域,如果加锁中出现异常,JVM 会处理异常,正确释放锁

  • 第二行表示:19 -> 22 行字节码中发生了异常,会跳转到 19 行,

未发生异常情况:

  • goto 24 (+8) 没有产生异常,直接执行 24 行指令

  • return 方法运行结束

16 goto 24 (+8)...24 return
复制代码

发生异常情况:

  • astore_2 将异常对象存储到临时变量中 e -> slot 2

  • aload_1 加载 LOCK 锁对象引用地址

  • monitorexit 还原 Mark Word,将 Monitor 对象指针替换为 monitorenter 加锁时保存在 Monitor 对象中的数据,如 hashcode、分代年龄等数据;同时唤醒等待在 EntryList 中阻塞等待的线程。

  • aload_2 加载异常对象

  • athrow 抛出异常对象

  • return 方法运行结束

16 goto 24 (+8)19 astore_220 aload_121 monitorexit22 aload_223 athrow24 return
复制代码


发布于: 2 小时前阅读数: 5
用户头像

李子捌

关注

华为云享专家 2020.07.20 加入

公众号【李子捌】

评论

发布
暂无评论
面试官synchronized连环问,学会Monitor之后轻松拿下