美文网首页
面试高频问题:JAVA的锁升级

面试高频问题:JAVA的锁升级

作者: 进击的波仔 | 来源:发表于2020-07-22 11:53 被阅读0次

    背景

    在java中,经常会用到synchronized关键字来保证线程安全,那么什么时候会存在线程安全呢?

    • 共享数据的修改
    • 临界资源访问

    应用场景

    • 修饰普通同步方法:锁当前实例对象
    • 修饰静态同步方法:锁当前的类Class对象
    • 修饰同步代码块:锁Synchronized后面括号里配置的对象,这个对象可以是任意对象;

    synchronized原理

    在绝大多数情况下,都只会有一个线程去访问synchronized修饰的代码块,所以synchronized在jdk1.6之后为了提升效率,优化了synchronized的机制,就是所谓的锁升级。通过对象头ObjectMonitor对象将锁划分了几个类型,其升级顺序为:无锁->偏向锁->轻量级锁->重量级锁,要了解它的原理,则必须要了解对象头。

    对象头

    java对象保存在内存中,由3个部分组成:

    • 对象头

    • 实例数据

    • 对齐填充字节

      这里,我们只对对象头加以说明

    1、对象头的存在形式

    JVM中的对象头有两种形式,它由三部分组成:

    • Mark Word
    • Klass Pointer(指向类的指针)
    • 数组长度(只有数组对象才有)
    1.1 普通对象
    |--------------------------------------------------------------|
    |                     Object Header (64 bits)                  |
    |------------------------------------|-------------------------|
    |        Mark Word (32 bits)         |    Klass Word (32 bits) |
    |------------------------------------|-------------------------|
    
    1.2 数组对象
    |---------------------------------------------------------------------------------|
    |                                 Object Header (96 bits)                         |
    |--------------------------------|-----------------------|------------------------|
    |        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
    |--------------------------------|-----------------------|------------------------|
    
    2、对象头组成
    2.1 MarkWord

    这部分主要存储对象自身的运行时数据,如hashCode,gc分代年龄,锁标记等等。MarkWord的长度根据jvm来确认,32位的JVM的mark word为32bit,64位的mark word为64bit。当一个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

    Mark Word在不同的锁状态下,存储不同,在32位JVM中是这样的:

    |-------------------------------------------------------|-----------------------------------------|
    |                  Mark Word (32 bits)                 |       状态                                |
    |----------------------------------------- -------------|-----------------------------------------|
    | identity hashcode:25 | 分代年龄:4 | 是否偏向锁:1 | 锁标志位:2 |无锁        |
    |------------------------------------------------------|----------------------------------------  |
    |  thread id:23 | epoch:2 | 分代年龄:4 | 是否偏向锁:1 | 锁标志位:2 |  偏向锁|
    |-----------------------------------------------------|--------------------|
    |               指向栈中锁记录的指针:30          | 锁标志位:2   |           轻量级锁|
    |--------------------------------------  -------------|-------------------------------------------|
    |               指向重量级锁的指针:30  | 锁标志位:2 |                    重量级锁  |
    |----------------------------------------------------|-------------------------------------------|
    |                 空                     | 锁标志位:2 |GC  标记
    

    注意:hashCode与identity hashcode并非完全是一个东西,identity hashcode是Object的hashCode

    2.2 指向类的指针

    该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

    Java对象的类数据保存在方法区。

    2.3 数组长度

    只有数组对象保存了这部分数据。

    该数据在32位和64位JVM中长度都是32bit。

    Monitor

    当锁膨胀为重量级锁时,多个线程来访问一段同步代码时,这些线程会被放到一个EntrySet集合中,处于阻塞状态的线程都会被放到该列表中。接下来,当线程获取到对象的Monitor时,Monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有改mutex,这时其他线程就无法再获取到该mutex

    如果线程调用了wait方法,那么该线程就会释放掉所持有的mutex,并且该线程会进入到WaitSet集合中,等待下一次被其他线程调用notify/notifyAll唤醒。如果当前线程顺利执行完毕方法,那么它也会释放掉所持有的mutex。

    由于这种实现方式,Monitor是依赖底层的操作系统实现,这样存在用户态和内核态之间的切换,所以会增加性能开销。

    锁升级的过程

    JVM一般是这样使用锁和Mark Word的:

    1、当没有被当做锁的时候,这就是个普通对象,锁标志位为01,是否偏向锁为0

    2、当对象被当做同步锁时,一个线程A抢到锁时,锁标志位依然是01,是否偏向锁为1,前23位记录A线程的线程ID,此时锁升级为偏向锁

    3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义

    4、当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,这里是有可能成功的,因为A线程一般不会释放偏向锁。如果失败,则执行5

    5、偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。JVM会在当前线程的现场栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上面的保存都是CAS操作,如果竞争成功,代表线程B抢到了锁,可以执行同步代码。如果抢锁失败,则继续执行6

    6、轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。从JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定。如果抢锁成功,则执行同步代码;如果抢锁失败,则执行7

    7、自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级锁

    相关文章

      网友评论

          本文标题:面试高频问题:JAVA的锁升级

          本文链接:https://www.haomeiwen.com/subject/yjuzkktx.html