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~

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

原文地址:https://www.cnblogs.com/ghsy/p/13819050.html