• synchronized原理详解


      活像个孤独患者自我拉扯,外向的孤独患者有何不可?

      鸽了一段时间,继续开更。

      1.同步器的存在意义

      多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是: 对象、变量、文件等。由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

      实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

      Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。两种同步器的本质都是加锁实现互斥访问。

      加锁目的:

        序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问) 不过有一点需要区别的是:

          当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的 私有栈中,因此不具有共享性,不会导致线程安全问题。

      2.synchronized底层原理

      synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码 块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、 偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。

      synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置 与结束位置。(如下图)

      每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:

       2.1什么是monitor?

      可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象 是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把 看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的 是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于 HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

    ObjectMonitor() {
    _header = NULL;
    _count = 0; // 记录个数
    _waiters = 0,
    _recursions = 0;
    _object = NULL;
    _owner = NULL;
    _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    _cxq = NULL ;
    FreeNext = NULL ;
    _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
    }
    

      ObjectMonitor中有两个队列,_WaitSet _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

         1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当 前线程,同时monitor中的计数器count加1;

         2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒;

        3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

      同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式 获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须 在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问 数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。

      2.2Monitor监视器锁

      任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和 MonitorExit指令来实现。(可以这么理解,每当new Object(),这个object都有一个monitor来监视这个object)

      1.monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下:

        a:如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者;

        b:如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

        c:如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

      2.monitorexit:执行monitorexit的线程必须是object所对应的monitor的所有者。

        指令执行时,monitor的进入数减 1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。(_count被减到0,标致着当前获取monitor的线程退出,)

        其他被这个monitor阻塞的线程可以尝试去 获取这个 monitor 的所有权。(别个阻塞队列[_EntryList]里面的线程出列尝试去获取锁)

      通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来 完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则 会抛出java.lang.IllegalMonitorStateException的异常的原因

      2.3从字节码分析synchronized关键字

      我们来看下面代码:

    public class SynchronizedClass {
    
        public synchronized void method() {
            System.out.println("Hello World!");
        }
    
        public static void main(String[] args) {
            new SynchronizedClass().method();
        }
    }
    

      执行javap -v,生成的字节码如下:

    public class com.yg.edu.SynchronizedClass
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #8.#23         // java/lang/Object."<init>":()V
       #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #26            // Hello World!
       #4 = Methodref          #27.#28        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #29            // com/yg/edu/SynchronizedClass
       #6 = Methodref          #5.#23         // com/yg/edu/SynchronizedClass."<init>":()V
       #7 = Methodref          #5.#30         // com/yg/edu/SynchronizedClass.method:()V
       #8 = Class              #31            // java/lang/Object
       #9 = Utf8               <init>
      #10 = Utf8               ()V
      #11 = Utf8               Code
      #12 = Utf8               LineNumberTable
      #13 = Utf8               LocalVariableTable
      #14 = Utf8               this
      #15 = Utf8               Lcom/yg/edu/SynchronizedClass;
      #16 = Utf8               method
      #17 = Utf8               main
      #18 = Utf8               ([Ljava/lang/String;)V
      #19 = Utf8               args
      #20 = Utf8               [Ljava/lang/String;
      #21 = Utf8               SourceFile
      #22 = Utf8               SynchronizedClass.java
      #23 = NameAndType        #9:#10         // "<init>":()V
      #24 = Class              #32            // java/lang/System
      #25 = NameAndType        #33:#34        // out:Ljava/io/PrintStream;
      #26 = Utf8               Hello World!
      #27 = Class              #35            // java/io/PrintStream
      #28 = NameAndType        #36:#37        // println:(Ljava/lang/String;)V
      #29 = Utf8               com/yg/edu/SynchronizedClass
      #30 = NameAndType        #16:#10        // method:()V
      #31 = Utf8               java/lang/Object
      #32 = Utf8               java/lang/System
      #33 = Utf8               out
      #34 = Utf8               Ljava/io/PrintStream;
      #35 = Utf8               java/io/PrintStream
      #36 = Utf8               println
      #37 = Utf8               (Ljava/lang/String;)V
    {
      public com.yg.edu.SynchronizedClass();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 18: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/yg/edu/SynchronizedClass;
    
      public synchronized void method();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String Hello World!
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 21: 0
            line 22: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  this   Lcom/yg/edu/SynchronizedClass;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: new           #5                  // class com/yg/edu/SynchronizedClass
             3: dup
             4: invokespecial #6                  // Method "<init>":()V
             7: invokevirtual #7                  // Method method:()V
            10: return
          LineNumberTable:
            line 25: 0
            line 26: 10
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      11     0  args   [Ljava/lang/String;
    }
    SourceFile: "SynchronizedClass.java"
    

      

      从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的: 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor对象。

       两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通 过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切 换,对性能有较大影响。

      从方法上加锁来看,实际上jvm底层操作monitor是通过 ACC_PUBLIC指令和ACC_SYNCHRONIZED来实现synchronized关键字的语意。

      我们在看加锁在方法内的同步代码块:

    public class SynchronizedClass {
    
        private static Object object = new Object();
    
        public void method() {
            synchronized (object) {
                System.out.println("Hello World!");
            }
        }
    
        public static void main(String[] args) {
            new SynchronizedClass().method();
        }
    }
    

      执行javap -v,生成的字节码如下:

    public class com.yg.edu.SynchronizedClass
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #9.#31         // java/lang/Object."<init>":()V
       #2 = Fieldref           #6.#32         // com/yg/edu/SynchronizedClass.object:Ljava/lang/Object;
       #3 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
       #4 = String             #35            // Hello World!
       #5 = Methodref          #36.#37        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #6 = Class              #38            // com/yg/edu/SynchronizedClass
       #7 = Methodref          #6.#31         // com/yg/edu/SynchronizedClass."<init>":()V
       #8 = Methodref          #6.#39         // com/yg/edu/SynchronizedClass.method:()V
       #9 = Class              #40            // java/lang/Object
      #10 = Utf8               object
      #11 = Utf8               Ljava/lang/Object;
      #12 = Utf8               <init>
      #13 = Utf8               ()V
      #14 = Utf8               Code
      #15 = Utf8               LineNumberTable
      #16 = Utf8               LocalVariableTable
      #17 = Utf8               this
      #18 = Utf8               Lcom/yg/edu/SynchronizedClass;
      #19 = Utf8               method
      #20 = Utf8               StackMapTable
      #21 = Class              #38            // com/yg/edu/SynchronizedClass
      #22 = Class              #40            // java/lang/Object
      #23 = Class              #41            // java/lang/Throwable
      #24 = Utf8               main
      #25 = Utf8               ([Ljava/lang/String;)V
      #26 = Utf8               args
      #27 = Utf8               [Ljava/lang/String;
      #28 = Utf8               <clinit>
      #29 = Utf8               SourceFile
      #30 = Utf8               SynchronizedClass.java
      #31 = NameAndType        #12:#13        // "<init>":()V
      #32 = NameAndType        #10:#11        // object:Ljava/lang/Object;
      #33 = Class              #42            // java/lang/System
      #34 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
      #35 = Utf8               Hello World!
      #36 = Class              #45            // java/io/PrintStream
      #37 = NameAndType        #46:#47        // println:(Ljava/lang/String;)V
      #38 = Utf8               com/yg/edu/SynchronizedClass
      #39 = NameAndType        #19:#13        // method:()V
      #40 = Utf8               java/lang/Object
      #41 = Utf8               java/lang/Throwable
      #42 = Utf8               java/lang/System
      #43 = Utf8               out
      #44 = Utf8               Ljava/io/PrintStream;
      #45 = Utf8               java/io/PrintStream
      #46 = Utf8               println
      #47 = Utf8               (Ljava/lang/String;)V
    {
      public com.yg.edu.SynchronizedClass();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 18: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/yg/edu/SynchronizedClass;
    
      public void method();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: getstatic     #2                  // Field object:Ljava/lang/Object;
             3: dup
             4: astore_1
             5: monitorenter
             6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             9: ldc           #4                  // String Hello World!
            11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            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:
             from    to  target type
                 6    16    19   any
                19    22    19   any
          LineNumberTable:
            line 23: 0
            line 24: 6
            line 25: 14
            line 26: 24
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      25     0  this   Lcom/yg/edu/SynchronizedClass;
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 19
              locals = [ class com/yg/edu/SynchronizedClass, class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: new           #6                  // class com/yg/edu/SynchronizedClass
             3: dup
             4: invokespecial #7                  // Method "<init>":()V
             7: invokevirtual #8                  // Method method:()V
            10: return
          LineNumberTable:
            line 29: 0
            line 30: 10
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      11     0  args   [Ljava/lang/String;
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: new           #9                  // class java/lang/Object
             3: dup
             4: invokespecial #1                  // Method java/lang/Object."<init>":()V
             7: putstatic     #2                  // Field object:Ljava/lang/Object;
            10: return
          LineNumberTable:
            line 20: 0
    }
    SourceFile: "SynchronizedClass.java"
    

      monitorexit,指令如果会出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;(同步块指令码上是monitorenter和monitorexit)

      3.对象的内存布局

      通过上面描述,我们已经知道synchronized关键字加锁是加在对象上面,对象是如何记录锁状态的呢?我们这里需要引入一个概念:对象的内存布局

        对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象) 等。Java对象头一般占有2个机器码(在32位虚拟机中,

          1个机器码等于4字节也就是32bit,在64位虚拟机中,1个机器码 是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,

          因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

        实例数据:存放类的属性数据信息,包括父类的属性信息。

        对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。(但是对齐可以大大提高程序效率,对于会被生成很多次对象需要做这个操作)

      对象组成我们看下图:

      我们重点来关注对象头里面的markword,里面是我们对象锁的锁状态的记录区域:

        由于64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的,我们来看32位虚拟机的markword分布。(32位虚拟机markword大小为4byte,32bit)

       我们先来看看对象的对象头信息,使用opjdk提供的工具包:

    <dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
    </dependency>
      来看看下面代码:
     public static void main(String[] args) throws InterruptedException {
    //        TimeUnit.SECONDS.sleep(5);
            Object o = new Object();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
    //        synchronized (o){
    //            System.out.println(ClassLayout.parseInstance(o).toPrintable());
    //        }
        }
    

      执行一下,控制台输出:

    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 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

      这里有3行,我们找第一行markword,括号里面的32位数字,里面就是markword的标志位:00000001 00000000 00000000 00000000

      但是我们这么看最后两位是00,对应的是轻量级锁,但是这个时候对象并没有被加锁。那是因为我们的操作系统分为大端模式和小端模式,我们一般的计算机都是小端模式,需要把高位的放到左侧去。

        实际上我们打印的markword是00000000 00000000 00000000 00000001,我们看后3位是001,这就对应了上面表中的无锁状态。

        我们在看一个拓展的问题,这个时候为什么hashcode没有打印呢?那是因为Object.hashCode方法类似于spring里面的懒加载,调用的时候对象的markword里面才会有对象的hashcode信息,

          具体可以参考这篇文章:https://www.jianshu.com/p/be943b4958f4

      然后我们继续,把代码的同步块位置注释掉:

    public static void main(String[] args) throws InterruptedException {
    // TimeUnit.SECONDS.sleep(5);
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    }

      然后继续执行程序,观察对象的锁状态,控制台输出:

    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 20 (11100101 00000001 00000000 00100000) (536871397)
         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)                           e8 f6 f9 02 (11101000 11110110 11111001 00000010) (49936104)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

      我们来观察同步块里面的32位markword,11101000 11110110 11111001 00000010;转换以后是00000010 11111001 11110110 11101000,后面两位是00,对应的是轻量级锁状态,

        这个就有点不对了,因为现在没有别个线程去竞争这个o对象,讲道理应该是偏向锁;这个是因为jvm默认会去延迟加载偏向锁,大概是4s左右,(这块是jvm启动的时候会有些许线程,

          核心包里面的一些类里面也有synchronized同步块,多个线程竞争肯定是会从无锁升级到偏向锁再到轻量级锁,在往后;所以jvm延迟了偏向锁的加载,启动的时候直接让这些类从无锁到

          轻量级锁,加快jvm的加载效率)。

        我们给程序暂停5s,第一行注释放开:

    public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object o = new Object();
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        }
    

      继续运行程序,控制台输出:

    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 20 (11100101 00000001 00000000 00100000) (536871397)
         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 40 19 03 (00000101 01000000 00011001 00000011) (51986437)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

      我们继续来观察同步块里面的32位markword,11100101 00000001 00000000 00100000;转换以后是00100000 00000000 00000001 11100101,后面三位是101,对应的是偏向状态,

        对应的线程id前面23位是00100000 00000000 0000000ok,没有问题

      但是,我们在回过头来看上面本来应该是无锁状态的时候,我们再来看这个markword,00000101 00000000 00000000 00000000;转换以后是00000000 00000000 00000000  00000101 ;

        本来应该是无锁状态的o,这个时候确实偏向锁状态,这又是为什么呢?

        我们引入一个概念,无锁状态对象的匿名偏向,开启偏向锁之后,新的对象就会是偏向锁状态,但是我们看前面23位,00000000 00000000 0000000,却是没有任何的线程id记录(此时

          对象是处于一种可偏向的状态)

      下面来看这一段代码:

    public static void main(String[] args) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object o = new Object();
            log.info(ClassLayout.parseInstance(o).toPrintable());
    
            new Thread(()->{
                synchronized (o){
                    log.info(ClassLayout.parseInstance(o).toPrintable());
                }
            }).start();
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            log.info(ClassLayout.parseInstance(o).toPrintable());
            new Thread(()->{
                synchronized (o){
                    log.info(ClassLayout.parseInstance(o).toPrintable());
                }
            }).start();
        }
    

      运行结果:

    16:52:47.019 [main] INFO com.yg.edu.T0_BasicLock - 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 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    16:52:47.066 [Thread-0] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 b8 7f 1a (00000101 10111000 01111111 00011010) (444577797)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    16:52:49.081 [main] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 b8 7f 1a (00000101 10111000 01111111 00011010) (444577797)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    16:52:49.081 [Thread-1] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           c0 f3 ce 1a (11000000 11110011 11001110 00011010) (449770432)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         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:如果只有一个线程对这个对象做一个同步,那么其他线程中,对象的锁状态还是不会改变(具体看第三次打印对象的markword位置)

        2:如果有多个线程对这个对象做一个同步,对象的锁状态就会做一个升级(从偏向锁升级到轻量级锁)

      继续,来分析下面代码:

    public static void main(String[] args) throws InterruptedException {
            Thread.sleep(5000);
            Object a = new Object();
    
            Thread thread1 = new Thread(){
                @Override
                public void run() {
                    synchronized (a){
                        System.out.println("thread1 locking");
                        System.out.println(ClassLayout.parseInstance(a).toPrintable());
                        try {
                            //让线程晚点儿死亡,造成锁的竞争
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            Thread thread2 = new Thread(){
                @Override
                public void run() {
                    synchronized (a){
                        System.out.println("thread2 locking");
                        System.out.println(ClassLayout.parseInstance(a).toPrintable());
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            thread1.start();
            thread2.start();
        }
    

      执行代码,控制台输出:

    thread1 locking
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           4a 8a cf 02 (01001010 10001010 11001111 00000010) (47155786)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    thread2 locking
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           4a 8a cf 02 (01001010 10001010 11001111 00000010) (47155786)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

      thread1打印的对象markword:01001010 10001010 11001111 00000010;转换后为00000010 11001111 10001010 01001010,对应的是重量级锁。

      thread2打印的对象markword:01001010 10001010 11001111 00000010;转换后为00000010 11001111 10001010 01001010,对应的是重量级锁。

       我们可以看出,在线程竞争激烈的情况下,匿名偏向状态会直接转为重量级锁(上面程序中,不管是哪个线程拿到了对象锁,里面的sleep 2s,都会让另一个线程在无限自旋,

        出现线程自旋等待,锁状态就会做出一个向上升级的动作)

      上面整了这么多活,我们在来点阴间的东西吧。

      看下面代码:

    public static void main(String[] args) throws InterruptedException {
            // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
            // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动
            // 偏向锁,会出现很多没有必要的锁撤销
            Thread.sleep(5000);
            T t = new T();
            //未出现任何获取锁的时候
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
            synchronized (t){
                // 获取一次锁之后
                System.out.println(ClassLayout.parseInstance(t).toPrintable());
            }
            // 输出hashcode
    //        System.out.println(t.hashCode());
            // 计算了hashcode之后,将导致锁的升级
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
            synchronized (t){
                // 再次获取锁
                System.out.println(ClassLayout.parseInstance(t).toPrintable());
            }
        }
    

      控制台输出:

    com.yg.edu.T 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)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
         12     4    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685)
          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    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685)
          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    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685)
          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    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    

      控制台输出都是正常的偏向锁状态(没有多个线程竞争激烈情况,都是偏向锁状态)

      我们把打印hashcode这行注释放开:

    public static void main(String[] args) throws InterruptedException {
            // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
            // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动
            // 偏向锁,会出现很多没有必要的锁撤销
            Thread.sleep(5000);
            T t = new T();
            //未出现任何获取锁的时候
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
            synchronized (t){
                // 获取一次锁之后
                System.out.println(ClassLayout.parseInstance(t).toPrintable());
            }
            // 输出hashcode
            System.out.println(t.hashCode());
            // 计算了hashcode之后,将导致锁的升级
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
            synchronized (t){
                // 再次获取锁
                System.out.println(ClassLayout.parseInstance(t).toPrintable());
            }
        }
    

      控制台输出:

    com.yg.edu.T 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)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
         12     4    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 48 69 02 (00000101 01001000 01101001 00000010) (40454149)
          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    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    1731722639
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 8f fd 37 (00000001 10001111 11111101 00110111) (939364097)
          4     4        (object header)                           67 00 00 00 (01100111 00000000 00000000 00000000) (103)
          8     4        (object header)                           92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978)
         12     4    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    
    com.yg.edu.T object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           20 f3 4f 02 (00100000 11110011 01001111 00000010) (38794016)
          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    int T.i                                       0
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    

      我们可以看最后两次打印:

        第三次打印:00000001 10001111 11111101 00110111;转换过后是:00110111 11111101 10001111 00000001,最后三位是001,对应的状态是无锁状态。(这就很奇怪了)

        第四次打印:00100000 11110011 01001111 00000010;转换过后是:00000010 01001111 11110011 00100000,最后两位是00,对应的状态是轻量级锁。(锁状态升级了,纳尼?)

      我们这里单独对这个hashcode来分析:

        我们往上翻一翻表格,偏向锁状态下,对象的markword是没地方去存储hashcode,但是,轻量级锁是有地方可以去存储这个对象的hashcode的信息,这里也可以解释的通,

          上面说的,对象的hashcode的获取是一种类似于spring的懒加载的类型。那么轻量级锁状态下,对象的hashcode又是放在哪个位置的呢?

          轻量级锁状态下,对象的hashcode会存储在线程栈中的一块空间中(Lock Record,可以理解成markword副本)过程看下图:

      4.锁的膨胀升级过程

      锁的膨胀升级是不可逆的,锁的膨胀升级是不可逆的,锁的膨胀升级是不可逆的。

      上面的的markword的描述,已经大概可以知道锁的膨胀升级过程了:

        锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重 量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁 的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

      5.自旋锁,对象锁的消除和粗化

      5.1自旋锁

      自旋锁会出现在轻量级锁加锁失败的情况下,为了不直接去升级成重量级锁(升级重量级锁涉及到用户态和内核态的切换,是一个比较重的操作,具体看前面的博文)而做出来的一种优化方式。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线 程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是 自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

      5.2对象锁的粗化

      我们看下面代码:

    private static Object object = new Object();
    
        public static void main(String[] args) {
    //        new SynchronizedClass().method();
            synchronized (object) {
                System.out.println("");
            }
            synchronized (object) {
                System.out.println("");
            }
            synchronized (object) {
                System.out.println("");
            }
        }
    

      对于上面这种代码,在同一个方法中对同一个对象进行多次加锁,性能上面会有一个消耗,通过一个线程的逃逸分析(这个会专门开一篇博客来讲这个逃逸分析),

        jvm会做一个锁的粗化来优化上面的代码,底层优化好以后就想当于是下面的代码:

    private static Object object = new Object();
    
        public static void main(String[] args) {
    //        new SynchronizedClass().method();
            synchronized (object) {
                System.out.println("");
                System.out.println("");
                System.out.println("");
            }
        }
    

      这就是一个锁的粗化的过程。

      5.3锁的消除

      我们看下面代码:

    public static void main(String[] args) {
    //        new SynchronizedClass().method();
            Object object1 = new Object();
            synchronized (object1) {
                System.out.println("");
            }
        }
    

      这个object1并不会被其他线程访问到,也就是说,这个object1不是一个临界资源,当程序对一个不是临界资源的对象加锁的时候,实际上是不生效的,这就是JVM对于这些代码做的一个优化,锁的消除。

      over~

      人类赞歌是勇气的赞歌,人类的伟大是勇气的伟大!!!

  • 相关阅读:
    20162324 2016-2017-2《Java程序设计》课程总结
    Java实验五网络编程与安全
    Java结对编程之挑战出题
    实验四Android开发
    Java四则运算总结
    实验三
    Java结对编程四则运算一周小结
    队列课下作业
    20162325 金立清 S2 W5 C14
    2017-2018-1 我爱学Java 第二周 作业
  • 原文地址:https://www.cnblogs.com/ghsy/p/13819050.html
Copyright © 2020-2023  润新知