阿里面试题:请描述一下synchrnoized的底层实现及重入的实现原理

JAVA herman 646浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:codedq,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:codedq,之前的微信号好友位已满,备注:返现
饿了么大量招人,我内推!Java 方向!薪资不设上限,工作年龄不限!工作地点限魔都,可电话面试!简历,发我微信:codedq
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

前两天,我闲着无事,在群里发了一个关于高并发的面试题,今天我来说一说这套面试题的第一小题的第一部分!

1、请描述synchrnoized和reentrantlock的底层实现及重入的底层原理

2、请描述锁的四种状态和升级过程

3、CAS的ABA问题如何解决

4、请谈一下AQS,为什么AQS的底层是CAS + volatile

5、请谈一下你对volatile的理解

6、volatile的可见性和禁止指令重排序是如何实现的

7、CAS是什么

8、请描述一下对象的创建过程

9、对象在内存中的内存布局

10、DCL单例为什么要加volatile

11、解释一下锁的四种状态

12、Object 0 = new Object()在内存中占了多少字节? 

13、请描述synchronized和ReentrantLock的异同

14、聊聊你对as-if-serial和happens-before语义的理解

15、你了解ThreadLocal吗?你知道ThreadLocal中 如何解决内存泄漏问题吗? 

16、请描述一下锁的分类以及JDK中的应用

17、自旋锁一定比重量级锁效率高吗?

上面是这套面试题的 17 个小题,不会的抓紧学习吧,拉勾和极客时间对应的并发专栏,从我这里购买都有返现!

要回答这个问题,我们先要知道对象在内存中的布局:

已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。

对象头主要是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)。

实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐。

填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的。

Java 对象头

Synchronized 论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么 Synchronized 锁对象是存在哪里的呢?答案是存在锁对象的对象头的 MarkWord 中。那么 MarkWord 在对象头中到底长什么样,也就是它到底存储了什么呢?

在 32 位的虚拟机中:

MarkWord 对象头中在 32 位虚拟机中的布局

在 64 位的虚拟机中:

MarkWord 对象头中在 64 位虚拟机中的布局

上图中的偏向锁和轻量级锁都是在 java6 以后对锁机制进行优化时引进的,synchronized 关键字对应的是重量级锁。

synchronized 在 JVM 中的实现原理,接下来对重量级锁在 Hotspot JVM 中的实现锁讲解。

synchronized 在 JVM 中的实现原理

重量级锁对应的锁标志位是 10,存储了指向重量级监视器锁的指针,在 Hotspot 中,对象的监视器(monitor)锁对象由 ObjectMonitor 对象实现(C++),其跟同步相关的数据结构如下:

ObjectMonitor() {
  _count        = 0; //用来记录该对象被线程获取锁的次数
  _waiters      = 0;
  _recursions   = 0; //锁的重入次数
  _owner        = NULL; //指向持有ObjectMonitor对象的线程 
  _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
  _WaitSetLock  = 0 ;
  _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}

光看这些数据结构对监视器锁的工作机制还是一头雾水,那么我们首先看一下线程在获取锁的几个状态的转换:

线程在获取锁的几个状态的转换

线程的生命周期存在 5 个状态,start、running、waiting、blocking 和 dead。

对于一个 synchronized 修饰的方法(代码块)来说:

  • 当多个线程同时访问该方法,那么这些线程会先被放进 _EntryList 队列,此时线程处于 blocking 状态
  • 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running 状态,执行方法,此时,ObjectMonitor 对象的 _owner 指向当前线程,_count加1表示当前对象锁被一个线程获取
  • 当 running 状态的线程调用 wait() 方法,那么当前线程释放 monitor 对象,进入 waiting 状态,ObjectMonitor 对象的 _owner 变为 null,_count 减 1,同时线程进入 _WaitSet 队列,直到有线程调用 notify() 方法唤醒该线程,则该线程重新获取 monitor 对象进入 _Owner 区
  • 如果当前线程执行完毕,那么也释放 monitor 对象,进入 waiting 状态,ObjectMonitor 对象的 _owner 变为 null,_count 减 1

那么 synchronized 修饰的代码块/方法如何获取 monitor 对象的呢?

在 JVM 规范里可以看到,不管是方法同步还是代码块同步都是基于进入和退出 monitor 对象来实现,然而二者在具体实现上又存在很大的区别。通过 javap 对 class 字节码文件反编译可以得到反编译后的代码。

(1)Synchronized 修饰代码块:

synchronized 代码块同步在需要同步的代码块开始的位置插入 monitorentry 指令,在同步结束的位置或者异常出现的位置插入 monitorexit 指令;JVM 要保证 monitorentry 和 monitorexit 都是成对出现的,任何对象都有一个 monitor 与之对应,当这个对象的 monitor 被持有以后,它将处于锁定状态。

例如,同步代码块如下:

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

对同步代码块编译后的 class 字节码文件反编译,结果如下(仅保留方法部分的反编译内容):

public void syncTask();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=3, locals=3, args_size=1
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter  //注意此处,进入同步方法
       4: aload_0
       5: dup
       6: getfield      #2             // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2            // Field i:I
      14: aload_1
      15: monitorexit   //注意此处,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit //注意此处,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
    //省略其他字节码.......

可以看出同步方法块在进入代码块时插入了 monitorentry 语句,在退出代码块时插入了 monitorexit 语句,为了保证不论是正常执行完毕(第15行)还是异常跳出代码块(第21行)都能执行 monitorexit 语句,因此会出现两句 monitorexit 语句。

(2)synchronized 修饰方法:

synchronized 方法同步不再是通过插入 monitorentry 和 monitorexit 指令实现,而是由方法调用指令来读取运行时常量池中的 ACC_SYNCHRONIZED 标志隐式实现的,如果方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 标志被设置,那么线程在执行方法前会先去获取对象的 monitor 对象,如果获取成功则执行方法代码,执行完毕后释放 monitor 对象,如果 monitor 对象已经被其它线程获取,那么当前线程被阻塞。

同步方法代码如下:

public class SyncMethod {
   public int i;
   public synchronized void syncTask(){
           i++;
   }
}

对同步方法编译后的 class 字节码反编译,结果如下(仅保留方法部分的反编译内容):

public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}

可以看出方法开始和结束的地方都没有出现 monitorentry 和 monitorexit 指令,但是出现的 ACC_SYNCHRONIZED 标志位。

以上就是 synchronized 的底层实现原理,在实际面试时,捡主要的描述即可!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:xttblog(5000人已满),微信号3:codedq(超3800)。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 阿里面试题:请描述一下synchrnoized的底层实现及重入的实现原理