• JUC并发编程系列详解篇十二(synchronized底层原理进阶)


    通过对象头分析锁升级过程

    可以通过对象头分析工具观察一下锁升级时对象头的变化:运行时对象头锁状态分析工具JOL,是OpenJDK开源工具包,引入下方maven依赖:

    <dependencies>
            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.10</version>
            </dependency>
    </dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    观察无锁状态下的对象头【无锁状态】:

    import org.openjdk.jol.info.ClassLayout;
    
    public class SynchronizedTest01 {
        public static void main(String[] args) throws InterruptedException {
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里先详细解释一下打印结果,后面就不做详细分析了:

    • OFFSET : 内存地址偏移量
    • SIZE : 本条信息对应的字节大小
    • Instance size: 16 bytes :本次new出的Object对象的大小

    由于当前所使用的的机器是64位操作系统的机器,所以前两行代表的就是对象头MarkWord,已经在上述运行结果中标出,刚好是8字节,每个字节8位,刚好是64位;由前文中32位对象头与64位对象头的位数对比可知,分析对象头锁升级情况看第一行的对象头即可。

    第三行指的是类型指针(上文中有说过,指向的是方法区的类元信息),已经在上述运行结果中标出,Klass Pointer在64位机器默认是8字节,这里由于指针压缩的原因当前是4字节。

    第四行指的是对齐填充,有的时候有有的时候没有,JVM内部需要保证对象大小是8个字节的整数倍,实际上计算机底层通过大量计算得出对象时8字节的整数倍可以提高对象存储的效率。

    可以观察到本次new出的Object对象的大小实际只有12字节,这里对象填充为其填充了4个字节,就是为了让Object对象大小为16字节是8字节的整数倍。

    JVM采用的是小端模式,需要现将其转换成大端模式,具体转换如下图所示:
    在这里插入图片描述
    可以看出一开始对象没有加锁,通过最后三位的“001”也能观察到,前25位代表hashcode,那这里为什么前25位是0呢?其实hashcode是通过C语言类似于“懒加载”的方式获取到的,所以看到该对象的高25位并没有hashcode。

    观察有锁无竞争状态下的对象头【无锁->偏向锁】:

    import org.openjdk.jol.info.ClassLayout;
    
    public class SynchronizedTest02 {
        public static void main(String[] args) throws InterruptedException {
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            synchronized (object){
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果(JVM默认小端模式):

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           90 39 62 05 (10010000 00111001 01100010 00000101) (90323344)
          4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果分析:

    通过运行结果可以看到,先打印出来的是一个“001”无锁的状态,但是后打印出来的“000”并不是偏向锁的状态,查上面的表可以发现“000”直接就是轻量级锁的状态了。

    JVM启动的时候内部实际上也是有很多个线程在执行synchronized,JVM就是为了避免无畏的锁升级过程(偏向锁->轻量级锁->重量级锁)带来的性能开销,所以JVM默认状态下会延迟启动偏向锁。

    只要将代码前面加个延迟时间即可观察到偏向锁:

    import org.openjdk.jol.info.ClassLayout;
    import java.util.concurrent.TimeUnit;
    
    public class SynchronizedTest02 {
    
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(6);
            Object o = new Object();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果(JVM默认小端模式):

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 90 80 de (00000101 10010000 10000000 11011110) (-561999867)
          4     4        (object header)                           b2 7f 00 00 (10110010 01111111 00000000 00000000) (32690)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    对未开启偏向锁与开启偏向锁的运行结果分析:

    1. 未开启偏向锁(大端模式),没加锁:00000000 00000000 00000000 00000001
    2. 开启偏向锁(大端模式),没加锁 :00000000 00000000 00000000 00000101
    3. 开启偏向锁(大端模式),加锁 :11011110 10000000 10010000 00000101

    开启偏向锁之后的无锁状态,会加上一个偏向锁,叫匿名偏向(可偏向状态),表示该对象锁是可以加偏向锁的,从高23位的23个0可以看出暂时还没有偏向任何一个线程,代表已经做好了偏向的准备,就等着接下来的某个线程能拿到就直接利用CAS操作把线程id记录在高23位的位置。

    观察有锁有竞争状态下的对象头【偏向锁->轻量级锁】:

    import org.openjdk.jol.info.ClassLayout;
    
    public class SynchronizedTest03 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread.sleep(5000);
    
            Object object = new Object();
    
            //main线程
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
    
            //线程t1
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            },"t1").start();
    
            Thread.sleep(2000);
    
            //main线程
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            //线程t2
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果(JVM默认小端模式):

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)        //main线程打印
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 90 94 2d (00000101 10010000 10010100 00101101) (764710917)    //t1线程打印
          4     4        (object header)                           c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 90 94 2d (00000101 10010000 10010100 00101101) (764710917)     //main线程打印
          4     4        (object header)                           c9 7f 00 00 (11001001 01111111 00000000 00000000) (32713)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           08 a9 d5 07 (00001000 10101001 11010101 00000111) (131442952)    //t2线程打印
          4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    运行结果分析:
    一开始main线程打印出的object对象头可以看出是匿名偏向;

    接着线程t1打印了object对象头,可以与第一个打印出来的对象头对比不难发现t1打印的也是偏向锁,但是t1打印的对象头已经把t1的线程id记录在了其对应的23位;

    程序再次回到main线程,其还是打印出来刚刚t1的对象头数据,也就是说偏向锁一旦偏向了某个线程后,如果线程不能重新偏向的话,那么这个偏向锁还是会一直记录着之前偏向的那个线程的对象头状态;

    接着线程t2又开始打印了object对象头,可以看出最后一次打印已经升级成了轻量级锁,因为这里已经存在两个线程t1、t2交替进入了object对象锁的同步代码块,并且锁的不激烈竞争,所以锁已经升级成了轻量级锁。

    观察无锁升级成重量级锁状态下的对象头的整个过程【无锁->重量级锁】:

    import org.openjdk.jol.info.ClassLayout;
    
    public class SynchronizedTest04 {
        public static void main(String[] args) throws InterruptedException {
            Thread.sleep(5000);
            Object object = new Object();
    
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
    
            new Thread(()->{
                synchronized (object) {
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    //延长锁的释放,造成锁的竞争
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"t0").start();
    
            Thread.sleep(5000);
    
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    //延长锁的释放,造成锁的竞争
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"t1").start();
    
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    运行结果:

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)      //main线程打印
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 d8 8f ef (00000101 11011000 10001111 11101111) (-275785723)  //t0线程打印
          4     4        (object header)                           ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           00 e9 a9 09 (00000000 11101001 10101001 00001001) (162130176)  //t1线程打印
          4     4        (object header)                           ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           0a d8 80 f0 (00001010 11011000 10000000 11110000) (-259991542)  //t2线程打印
          4     4        (object header)                           ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    运行结果分析(JVM默认小端模式):
    程序一开始就是设置了5秒钟的睡眠,目的在于让JVM优先加载完成后,让JVM默认状态下会延迟启动偏向锁,可以开出一开始main线程打印的是“101”就是默认的匿名偏向锁,但是并没有设置线程id;之后t0线程就立马打印了,此时只需利用CAS操作把t0的线程id设置进对象头即可,所以这个时候也是一个偏向锁状态;之后的程序睡眠5秒钟后,程序中t1、t2线程执行代码块时,有意的将其线程睡眠几秒钟,目的在于不管那个线程率先抢到锁,都能让另外一个线程在自旋等待中,所以t1线程打印的是“00”就已经是轻量级锁了,最后看程序执行结果,t2打印的是“10”就已经升级为重量级锁了,显然t2线程已经超过了自旋的最大次数,已经转成重量级锁了。

    参考文章

    • 微信公众号(得物技术) :精选文章|深入理解synchronzied底层原理
  • 相关阅读:
    记录 android studio 通过安装NDK 编译C文件,得到需要的so文件
    2022下半年软考考试时间安排已确定!
    天视通等小众冷门摄像机接入安防监控系统EasyCVR平台的常见兼容问题及解决方法
    go-micro集成RabbitMQ实战和原理
    【3D图像分割】基于 Pytorch 的 VNet 3D 图像分割3(3D UNet 模型篇)
    zlog 日志库使用说明,看完不懂打我...
    【Android进阶】13、对话框
    网页翻译插件
    [ Linux ] 进程控制 (1) 进程创建与进程终止
    蓝桥杯练习题八 - k倍区间(c++)
  • 原文地址:https://blog.csdn.net/m0_46198325/article/details/126801609