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

  •  备战2022春招或暑期实习,祝大家每天进步亿点点!Java并发编程Day7
  • 本篇总结的是 如何在Java中避免创建不必要的对象,后续会每日更新~
  • 关于《我们一起学Redis》、《我们一起学HarmonyOS》等知识点可以查看我的往期博客
  • 相信自己,越活越坚强活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!

目录

1、简介

2、对象头

3、Mark Word

4、Monitor

5、monitorente && monitorexit


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

如上两张图展示了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对象头的Mark Word存储结构如下

Java对象头MarkWord偏向锁情况.drawio.png

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

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

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

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

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

monitor+blocked.drawio.png

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

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_1
10 iadd
11 putstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I>
14 aload_1
15 monitorexit
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 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_1
10 iadd
11 putstatic #3 <com/lzb/concurrency/demo2/MonitorDemo.count : I>
14 aload_1
15 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_2
20 aload_1
21 monitorexit
22 aload_2
23 athrow
24 return

👇🏻 关注公众号 获取更多资料👇🏻 

在面试中,有时会synchronized的优化题。synchronized是一种实现对象锁的方式,它可以确保多个线程在访共享资源时的互斥性,避免数据不一致的题。在早期的JDK版本中,synchronized的性能较差,原因主要有两个方面。首先,JDK 6以前的synchronized是基于重量级锁实现的,它使用操作系统的互斥量来实现线程的互斥访,需要进行用户态和内核态之间的切换,开销较大。其次,synchronized在锁竞争激烈的情况下,会导致线程频繁地进行阻塞和唤醒操作,增加了线程切换的次数,影响了系统的吞吐量。 为了对synchronized进行优化,JDK 6以后引入了轻量级锁和偏向锁的概念。轻量级锁使用CAS(Compare and Swap)操作来实现线程的互斥访,避免了用户态和内核态之间的切换,提升了性能。偏向锁则是在对象第一次被一个线程访时,将该线程的ID记录在对象头中,之后该线程再次访该对象时,无需进行同步操作,从而减少了锁的竞争,提高了性能。这些优化措施使得synchronized并发场景下的性能得到了显著的提升。 总结起来,JDK 6以前synchronized的性能较差,主要是因为它使用了重量级锁,并且在锁竞争激烈的情况下会导致线程频繁阻塞和唤醒。而JDK 6以后引入了轻量级锁和偏向锁的优化机制,使得synchronized并发场景下的性能得到了提升。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【面试】Synchronized常见面试题](https://blog.csdn.net/HeavenDan/article/details/120776181)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李子捌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值