zoukankan      html  css  js  c++  java
  • java并发笔记之synchronized 偏向锁 轻量级锁 重量级锁证明

    Java的对象头mark word

    上一篇博客我们编译了Linux源码来证明了Java中有偏向锁,但是我们从周志明大佬的《深入理解java虚拟机》的书中知道,我们可以通过分析Java对象头中MarkWord来查看是那种锁,下面是32位JVM的对象中的Mark Word图,但是随着JDK的不断升级,JDK没有32位的版本,所以我们要研究64的JVM中对象的MarkWord。

    在这里插入图片描述

    64位JVM的对象中的Mark Word图

    当我在网上找了很多资料的后,发现都是32位JVM,无法满足我们对64位JVM的研究,于是我想到了JDK源码,看看其中有没有注释,于是我去编译好的JDK源码找找看,找到对应的源码的注释如下:

    在这里插入图片描述

    我们可以将上面的注释转成以下的表格

    image-20210926132258246

    在这里插入图片描述

    java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态:

    1. 无锁状态、
    2. 加锁状态、
    3. gc标记状态

    那么我可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。

    但是java当中的锁有分为很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。

    这三种锁的效率 完全不同、关于效率的分析会在下文分析,我们只有合理的设计代码,才能合理的利用锁、那么这三种锁的原理是什么?

    所以我们需要先研究这个对象头。

    java对象的布局以及对象头的布局

    1、JOL来分析java的对象布局

    <dependency>
         <groupId>org.openjdk.jol</groupId>
         <artifactId>jol-core</artifactId>
         <version>0.10</version>
    </dependency>
    
    

    然后创建A.java

    public class A{}
    
    

    然后创建JOLExample1.java

    import org.openjdk.jol.info.ClassLayout;
    import org.openjdk.jol.vm.VM;
    
    import static java.lang.System.out;
    
    public class JOLExample1 {
        static A a;
    
        public static void main(String[] args) {
            a = new A();
            //打印JVM的详细信息
            out.println(VM.current().details());
            //打印对应的对象头信息
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }
    
    

    运行结果1 如下:

    Connected to the target VM, address: '127.0.0.1:62530', transport: 'socket'
    # Running 64-bit HotSpot VM.
    # Using compressed oop with 0-bit shift.
    # Using compressed klass with 3-bit shift.
    # Objects are 8 bytes aligned.
    # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] //(1)
    # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
    
    com.luban.layout.A 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)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes //(2)
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    Disconnected from the target VM, address: '127.0.0.1:62530', transport: 'socket'
    
    Process finished with exit code 0
    
    

    image-20210926143950965

    分析结果:

    整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0B?

    出现两个问题 :

    1、什么叫做对象的实例数据呢?

    2、那么对象头里面的12B到底存的是什么呢?

    首先要明白什么对象的实例数据很简单,我们可以在A当中添加一个boolean的字段,大家都知道boolean字段占 1B,然后再看结果

    2.对象实例数据展示

    修改A.java

    public class A {
        //占一个字节的boolean字段
        boolean flag =false;
    }
    
    

    运行结果2

    # Running 64-bit HotSpot VM.
    # Using compressed oop with 0-bit shift.
    # Using compressed klass with 3-bit shift.
    # Objects are 8 bytes aligned.
    # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
    # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
    
    com.luban.layout.A 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)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
         12     1   boolean A.flag                                    false
         13     3           (loss due to the next object alignment)
    Instance size: 16 bytes
    
    

    分析结果2

    整个对象的大小还是没有改变一共16B,其中对象头(Object header)12B,boolean字段flag(对象的实例数据)占 1B、剩下的

    3B就是对齐字节。由此我们可以认为一个对象的布局大体分为三个部分分别是对象头(Object header)、 对象的实例数据、字节对齐

    在64位虚拟机上对象的大小必须是8的倍数证明:

    上面说是64位虚拟机上对象的大小必须是8的倍数,我们可以证明一下,再加一个int(4个字节)数据,我们再次修改A.java如下所示

    public class A {
        //占一个字节的boolean字段
        private boolean flag;
        //占四个字节的int字段
        private int a;
    }
    

    结果:

    image-20210926150337523

    可以看到对象的大小是8的倍数

    3.对象头为什么是12B?这个12B当中分别存储的是什么呢?

    接下来讨论第二个问题,对象头为什么是12B?这个12B当中分别存储的是什么呢?(不同位数的VM对象头的长度不一 样,这里指的是64bit的vm) 关于java对象头的一些专业术语 http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.htm

    首先引用OpenJDK的官网的解释

    object header

    Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental

    information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it

    is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format

    上述引用中提到一个java对象头包含了2个word,并且好包含了堆对象的布局、类型、GC状态、同步状态和标识哈 希码,具体怎么包含的呢?又是哪两个word呢?

    mark word

    The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

    mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,第二个word是什么 呢?

    klass pointer

    The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".

    **klass word为对象头的第二个word主要指向对象的元数据。 **

    假设我们理解一个对象头主要上图两部分组成(数组对象除外,数组对象的对象头还包含一个数组长度),那么 一个java的对象头多大呢?我们从JVM的源码注释中得知到一个mark word一个是64bit,那么klass的长度是多少呢?

    所以我们需要想办法来获得java对象头的详细信息,验证一下他的大小,验证一下里面包含的信息是否正确。

    根据上述利用JOL打印的对象头信息可以知道一个对象头是12B,其中8B是mark word 那么剩下的4B就是klass word了,

    image-20210926132258246

    但是看上图不是说Klass word是64bits(8个字节)为啥这边计算出来就是只有4个字节???

    答案是因为我们开启了指针压缩,我们可以关闭指针压缩看看,是不是8个字节。我们只需要使用以下的JVM运行参数

    4.对象头指针压缩

    去掉指针压缩

    -XX:-UseCompressedOops
    
    

    image-20210926145552363

    结果:

    可以看到我们Klass对象是64bits(16个字节),具体如下图

    image-20210926145728795

    5.Mark word里面信息

    一个对象头主要由上图两个部分组成(数组对象除外,数组对象的对象还包含一个数组长度),由我们的推导出Mark word是8个字节,klass word(开启指针压缩的情况下是4个字节,不开启的时候是8个字节)。我们打印出来的对象头是12个字节,所以其中的8个字节是Mark word,剩下的4个字节是klass word,但是和锁相关的就是Mark word,那么接下来要重点分析Mark word里面信息。

    在这里插入图片描述

    如图所示: 在无锁的情况下markword当中的前56bit存的是对象的hashcode,那么来验证一下 :

    去掉禁用指针压缩

    修改A.java 的代码如下

    public class A {
        //占一个字节的boolean字段
        private boolean flag;
    }
    
    

    demo1:对象头为无锁状态

    新建一个JOLExample2.java具体代码如下

    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    public class JOLExample2 {
    
        public static void main(String[] args) {
            A a = new A();
            //没有计算HashCode之前的对象头
            out.println("before hash");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            
            //jvm计算HashCode
            out.println("jvm----------" + Integer.toHexString(a.hashCode()));
            HashUtil.countHash(a);
    
            //当计算完HashCode之后,我们可以查看对象头的信息变化
            out.println("after hash");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            
        }
    }
    
    
    
    package com.luban.layout;
    
    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    
    public class HashUtil {
        public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
            // 手动计算HashCode
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            long hashCode = 0;
            for (long index = 7; index > 0; index--) {
                // 取Mark Word中的每一个Byte进行计算
                hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
            }
            String code = Long.toHexString(hashCode);
            System.out.println("util-----------0x"+code);
    
        }
    }
    
    

    运行的结果如下:

    image-20210926151446164

    对象A的状态 可以通过看对象头信息判断出来属于无锁状态

    解释运行结果:

    |---------------------------------------------------------------------------------------------------------------------------------------------------------------------|

    befor hash
    com.luban.layout.A 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) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
    12 1 boolean A.flag false
    13 3 (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

    jvm------------0x1a1d6a08
    util----------- 0x1a1d6a08

    after hash

    com.luban.layout.A object internals:
    OFFSET SIZE TYPE DESCRIPTION VALUE
    0 4 (object header) 01 08 6a 1d (00000001 00001000 01101010 00011101) (493488129)
    4 4 (object header) 1a 00 00 00 (00011010 00000000 00000000 00000000) (26)
    8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
    12 1 boolean A.flag false
    13 3 (loss due to the next object alignment)
    Instance size: 16 bytes

    |---------------------------------------------------------------------------------------------------------------------------------------------------------------------|

    无锁状态:
    befor hash

    00000001 00000000 00000000 00000000

    00000000 00000000 00000000 00000000

    因为计算是是小端存储高位放在低地址。。。

    所以00000001 这八位存储的分别就是分代年龄、偏向锁信息、对象状态,这八位为00000001 ,是无锁的情况

    image-20210926155301758

    可以对象a在计算hashcode之前是属于无锁状态的 ,

    那么蓝色表示的是什么东西呢?

    image-20210926160631858

    在befor hash之前,是没有进行hashcode之前的对象头信息,可以看出hashcode无值

    after hash

    00000001 00001000 01101010 00011101
    00011010 00000000 00000000 00000000

    image-20210926161402322

    我们的HashUtil 工具就是手动计算 调用 Integer.toHexString(a.hashCode())方法后a对象的hashcode值。
    HashUtil.countHash(a)工具是计算

    00001000 01101010 00011101
    00011010 00000000 00000000 00000000

    的16进制

    jvm------------0x1a1d6a08 、 util-----------0x1a1d6a08 相同

    关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记,那么2bit,如何能表示五种状

    态(2bit最多只能表示4中状态分别是:00,01,10,11),

    jvm做的比较好的是把偏向锁和无锁状态表示为同一个状态,然后根据图中偏向锁的标识再去标识是无锁还是偏向锁状态。什么意思呢?写个代码分析一下,在写代码之前我们先记得 无锁状态下的信息00000001,然后写一个偏向锁的例子看看结果

    dem02:对象头为偏向锁状态:

    偏量锁延迟

    public class JOLExample3 {
       static A a;
        public static void main(String[] args) throws Exception {
            a= new A();
            out.println("befor lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            synchronized (a){
                out.println("lock ing");
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }
    
    

    结果:

    image-20210926162634905

    分析结果:

    上面这个程序只有一个线程去调用sync方法,应该是偏向锁,wocao!!!居然是0 00 (使用了轻量锁)不是1 01(偏量锁),为啥会出现这种情况呢?

    经过翻hotspot源码发现:

    路径: openjdk/hotspot/src/share/vm/runtime/globals.hpp

    product(bool, UseBiasedLocking, true,                   
    
    ​    "Enable biased locking in JVM")                  
    
    ​                                     
    
    product(intx, BiasedLockingStartupDelay, 4000,              
    
    ​    "Number of milliseconds to wait before enabling biased locking") 
    
    ​    range(0, (intx)(max_jint-(max_jint%PeriodicTask::interval_gran))) 
    
    ​    constraint(BiasedLockingStartupDelayFunc,AfterErgo)        
    
    BiasedLockingStartupDelay, 4000  //偏向锁延迟4000ms
    

    其实这是因为虚拟机在启动的时候对于偏向锁有延迟,那我们怎么看到打印的对象头是偏向锁呢?有两种方式:第一种是加锁之前先让线程睡几秒。第二种加上JVM的运行参数,关闭偏向锁的延迟,具体的命令如下:

    在这里插入图片描述

    -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    
    

    避免偏量锁延迟后,使用偏量锁

    第一种方式:修改JOLExample3.java如下
    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    
    public class JOLExample3 {
    
        static A a;
    
        public static void main(String[] args) throws InterruptedException {
            //切记延迟一定要放在对象创建之前,不然是无效的,因为在你对象创建之前,偏向锁的延迟的时间
            //没有给你睡过去,这时候,对象已经创建了,对象头的信息已经生成了。
            Thread.sleep(5000);
            a = new A();
            out.println("before lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
            sync();
    
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
        }
    
        private static void sync() {
            synchronized (a) {
                out.println("lock ing");
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    }
    
    
    

    再次运行,查看结果如下:使用了

    image-20210926164656018

    如图所示:之前的 001 变成了101 说明偏向锁的biased_lock状态已经启用了,偏向锁标识为: 1 此时对象的状态为 01 ;需要注意的是after lock,退出同步后依然保持了偏向信息

    image-20210926170518183

    第二种方式:利用jvm参数,首先我们先关闭睡眠5秒的,然后运行配置如下:
    -XX:BiasedLockingStartupDelay=0
    
    public class JOLExample3 {
       static A a;
        public static void main(String[] args) throws Exception {
            a= new A();
            out.println("befor lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            synchronized (a){
                out.println("lock ing");
               out.println(ClassLayout.parseInstance(a).toPrintable());
            }
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }
    

    image-20210926170119252

    image-20210926201738624

    这时候大家会有疑问了,为什么在没有加这时候大家会有疑问了:

    为什么在没有加锁之前是对象的状态是偏向锁,准确的说,应该是叫可偏向的状态,因为它后面没有存线程的ID,当lock ing的时候,后面存储的就是线程的ID(46675973)既然这儿存储是线程的ID,并且释放锁时,对象偏向锁状态保留,线程ID也保留

    image-20210926170518183

    使用hashcode方法后,使用轻量锁

    存储线程id,那么对象的HashCode又存储到什么地方去了?是不是计算了HashCode就是不能偏向了?我们来验证一下,计算完HashCode,还是不是偏向锁了? [注意还是禁止了偏向锁延迟]

    我们再次修改JOLExample3.java,具体代码如下:

    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    
    public class JOLExample3 {
    
        static A a;
    
        public static void main(String[] args) throws InterruptedException {
            //切记延迟一定要放在对象创建之前,不然是无效的,因为在你对象创建之前,偏向锁的延迟的时间
            //没有给你睡过去,这时候,对象已经创建了,对象头的信息已经生成了。
            //Thread.sleep(5000);
            a = new A();
            out.println("before lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            a.hashCode();
    
            sync();
    
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
        }
    
        private static void sync() {
            synchronized (a) {
                out.println("lock ing");
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    }
    
    

    image-20210926170119252

    结果:使用hashcode之后,偏向锁线程id无法储存 优化成使用轻量锁,最后无锁

    image-20210926172112656

    我们可以发现:在before lock的时候是可偏向的状态lock ing的时候变成了轻量锁after lock 的时候变成了无锁,所以我们得出对象计算了HashCode,就不是偏向锁了。

    想想为什么偏向锁会延迟?

    因为jvm 在启动的时候需要加载资源,这些对象加上偏向锁没有任何意义啊,减少了大量偏向锁撤销的成本;所以默认就把偏向锁延迟了4000ms;

    经过翻hotspot源码发现:

    路径:openjdk/hotspot/src/share/vm/runtime/biasedLocking.cpp

    void BiasedLocking::init() {
    
     // If biased locking is enabled, schedule a task to fire a few
    
     // seconds into the run which turns on biased locking for all
    
     // currently loaded classes as well as future ones. This is a
    
     // workaround for startup time regressions due to a large number of
    
     // safepoints being taken during VM startup for bias revocation.
    
     // Ideally we would have a lower cost for individual bias revocation
    
     // and not need a mechanism like this.
    
     if (UseBiasedLocking) {
    
      if (BiasedLockingStartupDelay > 0) {
    
       EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
    
       task->enroll();
    
      } else {
    
       VM_EnableBiasedLocking op(false);
    
       VMThread::execute(&op);
    
      }
    
     }
    
    }
    

    英文大概翻译为: 当jvm启动记载资源的时候,初始化的对象加偏向锁会耗费资源,减少大量偏向锁撤销的成本(jvm的偏向锁的优化)

    这就解释了加上睡眠5000ms,偏向锁就会出现;为了方便我们测试我们可以直接通过修改jvm的参数来禁止偏向锁延迟(不用在代码睡眠了):

    -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

    注意:这块严谨来说,在jdk 1.6之后,关于使用偏向锁和轻量级锁,jvm是有优化的,

    在没有禁止偏向锁延迟的情况下,使用的是轻量级锁;

    禁止偏向锁延迟的话,使用的是偏向锁;

    demo3 对象头为轻量锁

    看完了偏向锁的对象头,我们再来看看轻量锁的对象头,轻量级锁尝试在应用层面解决线程同步问题,而不触发操作系统的互斥操作,轻量级锁减少多线程进入互斥的几率,不能代替互斥。

    创建JOLExample4.java,代码如下:

    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    
    public class JOLExample4 {
        static A a;
    
        public static void main(String[] args) {
            a = new A();
            out.println("before lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
            sync();
    
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    
        private static void sync() {
            synchronized (a) {
                out.println("lock ing");
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    }
    
    

    运行结果如下:

    在这里插入图片描述

    可以得出:before lock 的时候是 00000001 无锁的状态,lock ing 的时候是 01010000 轻量锁的状态,after lock 的时候是 00000001 无锁的状态。

    dem4:重量锁的对象头

    看完了轻量锁的对象头,我们再来看看重量锁的对象头,我们先创建一个JOLExample5.java具体代码如下:

    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    
    public class JOLExample5 {
        static A a;
    
        public static void main(String[] args) throws InterruptedException {
            a = new A();
            out.println("before lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            
            Thread t1 = new Thread(()->{
                synchronized (a) {
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    out.println("t1 release");
                }
            });
            t1.start();
            
            Thread.sleep(1000);
            out.println("t1 lock ing");
            out.println(ClassLayout.parseInstance(a).toPrintable());
            
            sync();
            out.println("after lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
            System.gc();
            out.println("after gc()");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    
        private static void sync() {
            synchronized (a) {
                out.println("main lock ing");
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    }
    
    

    运行结果如下:

    在这里插入图片描述

    在加锁之前(before lock)是 00000001 无锁,这时候t1来加锁,因为只有他一个线程所以轻量锁(t1 lock ing 00010000)由于t1在run方法中睡眠了5秒,这时候主线程也来尝试加锁,这个时候就是两个线程竞争了,所以是重量锁(main lock ing 00101010

    在这里插入图片描述

    当结束的时候,还是重量锁(afteer lock 00101010),当执行一次gc操作过后发现变成了无锁但是年龄加了1(after gc() 00001001

    调用wait方法会直接变成重量锁

    还有一点需要我们注意的就是:当调用wait方法会直接变成重量锁,我们来验证一下,创建JOLExample6.java,代码如下:

    import org.openjdk.jol.info.ClassLayout;
    
    import static java.lang.System.out;
    
    public class JOLExample6 {
        static A a;
    
        public static void main(String[] args) throws Exception {
    
            a = new A();
            out.println("before lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
    
            Thread t1 = new Thread(() -> {
                try {
                    synchronized (a) {
                        out.println("before wait");
                        out.println(ClassLayout.parseInstance(a).toPrintable());
                        a.wait();
                        out.println("after wait");
                        out.println(ClassLayout.parseInstance(a).toPrintable());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            });
            t1.start();
            Thread.sleep(5000);
            synchronized (a) {
                a.notifyAll();
            }
        }
    }
    
    

    运行结果如下:

    在这里插入图片描述

    可以看到在加锁之前是无锁的状态,执行wait方法之前是轻量锁,执行wait 方法之后,被唤醒后是重量锁。

    锁之间的性能比较

    既然synchronized关键字有这三种锁,我们简单的比较它们之间的性能(粗略的比较下),书写以下的代码

    public class A {
        int i;
        public synchronized void parse() {
            i++;
        }
    }
    //关闭偏向锁延迟‐XX:BiasedLockingStartupDelay=0
    public class JOLExample7 {
        public static void main(String[] args) throws Exception {
            A a = new A();
            long start = System.currentTimeMillis();
            //调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
            //如果不出意外,结果灰常明显
            for (int i = 0; i < 1000000000L; i++) {
                a.parse();
            }
            long end = System.currentTimeMillis();
            System.out.println(String.format("%sms", end - start));
        }
    
    }
    
    

    偏向锁

    先运行加上jvm参数关闭偏向锁延迟,就是偏向锁,然后运行的结果如下:

    在这里插入图片描述

    轻量锁

    我们在开启偏向锁延迟就是轻量锁,然后运行结果如下:

    在这里插入图片描述

    重量锁

    最后我们在看重量锁,具体代码如下:

    public class A {
        int i;
        public synchronized void parse() {
            JOLExample8.countDownLatch.countDown();
            i++;
        }
    }
    
    
    import java.util.concurrent.CountDownLatch;
    
    public class JOLExample8 {
        static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
    
        public static void main(String[] args) throws Exception {
            final A a = new A();
    
            long start = System.currentTimeMillis();
    
            //调用同步方法1000000000L 来计算1000000000L的++,对比各种锁的性能
            //如果不出意外,结果灰常明显
            for (int i = 0; i < 2; i++) {
                new Thread(() -> {
                    while (countDownLatch.getCount() > 0) {
                        a.parse();
                    }
                }).start();
            }
            countDownLatch.await();
            long end = System.currentTimeMillis();
            System.out.println(String.format("%sms", end - start));
        }
    }
    
    

    重量级锁的执行结果如下:

    在这里插入图片描述

    最后总结的结果如下:

    偏向锁 轻量锁 重量锁
    2355ms 23564ms 31227ms

    最后我们再画个图总结下各种锁的对象头(只画出了最重要的部分,其他的省略)

    在这里插入图片描述

    原文链接:https://blog.csdn.net/qq_36434742/article/details/106854061

  • 相关阅读:
    TDirectory.GetParent获取指定目录的父目录
    TDirectory.GetLogicalDrives获取本地逻辑驱动器
    获取设置目录创建、访问、修改时间
    TDirectory.GetLastAccessTime获取指定目录最后访问时间
    TDirectory.GetDirectoryRoot获取指定目录的根目录
    「洛谷P1262」间谍网络 解题报告
    「洛谷P1198」 [JSOI2008]最大数 解题报告
    「洛谷P3931」 SAC E#1
    「UVA1328」「POJ1961」 Period 解题报告
    「博客美化」I 页面的CSS
  • 原文地址:https://www.cnblogs.com/tangliMeiMei/p/15339400.html
Copyright © 2011-2022 走看看