关于jvm:从-JVM-中深入探究-Synchronized

39次阅读

共计 24213 个字符,预计需要花费 61 分钟才能阅读完成。

开篇语

Synchronized,Java 敌对的提供了的一个关键字,它让开发者能够疾速的实现同步。它就像一个星星,远远看去就是一个小小的点。然而走近一看,却是一个宏大的蛋糕。而这篇文章就是要将这个微小的蛋糕切开,吃进肚子外面去。

Synchronized 应用

在 Java 中,如果要实现同步,Java 提供了一个关键词 synchronized 来让开发人员能够疾速实现同步代码块。

public class Test {public static void main(String[] args){Object o = new Object();

        Thread thread1 = new Thread(() -> {synchronized (o){System.out.println("获取锁胜利");
            }
        }).start();}
}

线程 thread1 获取对象 o 的锁,并且输入一句话“获取锁胜利”。

public class Test {

    private int i = 0;

    public synchronized void set(int i){this.i = i;}

    public synchronized static String get(){return "静态方法";}

    public void put(){synchronized (this){System.out.println("同步代码块");
        }
    }
}

synchronized 关键字除了能够用于代码块,还能够用于办法上。用于实例办法上时,线程执行该办法之前,会主动获取该对象锁,获取到对象锁之后才会继续执行实例办法中的代码;用于静态方法上时,线程执行该办法之前,会主动获取该对象所属类的锁,获取到类锁之后才会继续执行静态方法中的代码。用于代码块上时,能够传入任意对象作为锁,并且能够管制锁的粒度。

synchronized 实现原理

上面是 Test 类的字节码文件

public class Test
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // Test
  super_class: #8                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #8.#27         // java/lang/Object."<init>":()V
   #2 = Fieldref           #7.#28         // Test.i:I
   #3 = String             #29            // 静态方法
   #4 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #32            // 同步代码块
   #6 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #35            // Test
   #8 = Class              #36            // java/lang/Object
   #9 = Utf8               i
  #10 = Utf8               I
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               LTest;
  #18 = Utf8               set
  #19 = Utf8               (I)V
  #20 = Utf8               get
  #21 = Utf8               ()Ljava/lang/String;
  #22 = Utf8               put
  #23 = Utf8               StackMapTable
  #24 = Class              #37            // java/lang/Throwable
  #25 = Utf8               SourceFile
  #26 = Utf8               Test.java
  #27 = NameAndType        #11:#12        // "<init>":()V
  #28 = NameAndType        #9:#10         // i:I
  #29 = Utf8               静态方法
  #30 = Class              #38            // java/lang/System
  #31 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #32 = Utf8               同步代码块
  #33 = Class              #41            // java/io/PrintStream
  #34 = NameAndType        #42:#43        // println:(Ljava/lang/String;)V
  #35 = Utf8               Test
  #36 = Utf8               java/lang/Object
  #37 = Utf8               java/lang/Throwable
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (Ljava/lang/String;)V
{public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field i:I
         9: return
      LineNumberTable:
        line 5: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LTest;

  public synchronized void set(int);
    descriptor: (I)V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field i:I
         5: return
      LineNumberTable:
        line 10: 0
        line 11: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LTest;
            0       6     1     i   I

  public static synchronized java.lang.String get();
    descriptor: ()Ljava/lang/String;
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #3                  // String 静态方法
         2: areturn
      LineNumberTable:
        line 14: 0

  public void put();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #5                  // String 同步代码块
         9: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 18: 0
        line 19: 4
        line 20: 12
        line 21: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  this   LTest;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [class Test, class java/lang/Object]
          stack = [class java/lang/Throwable]
        frame_type = 250 /* chop */
          offset_delta = 4
}

咱们通过查看字节码能够发现,synchronized 关键字作用在实例办法和静态方法上时,JVM 是通过 ACC_SYNCHRONIZED 这个标记来实现同步的。而作用在代码块时,而且通过指令 monitorenter 和 monitorexit 来实现同步的。monitorenter 是获取锁的指令,monitorexit 则是开释锁的指令。

对象头

通过上文咱们曾经晓得,Java 要实现同步,须要通过获取对象锁。那么在 JVM 中,是如何晓得哪个线程曾经获取到了锁呢?

要解释这个问题,咱们首先须要理解一个对象的存储散布由以下三局部组成:

  • 对象头(Header):由 Mark WordKlass Pointer 组成
  • 实例数据(Instance Data):对象的成员变量及数据
  • 对齐填充(Padding):对齐填充的字节

Mark Word ** 记录了对象运行时的数据:

  1. identity_hashcode:哈希码,只有获取了才会有
  2. age:GC 分代年龄
  3. biased_lock:1 示意偏差锁,0 示意非偏差锁
  4. lock 锁状态:01 无锁 / 偏差锁;00 轻量级锁;10 重量级锁;11 GC 标记
  5. 偏差线程 ID
128bit(对象头) 状态
64bit Mark Word 64bit Klass Poiter
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2 无锁
threadId:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 偏差锁
ptr_to_lock_record:62 lock:2 轻量级锁
ptr_to_heavyweight_monitor:62 lock:2 重量级锁
lock:2 GC 标记

当线程获取对象锁的时候,须要先通过对象头中的 Mark Word 判断对象锁是否曾经被其余线程获取,如果没有,那么线程须要往对象头中写入一些标记数据,用于示意这个对象锁曾经被我获取了,其余线程无奈再获取到。如果对象锁曾经被其余线程获取了,那么线程就须要进入到期待队列中,直到持有锁的线程开释了锁,它才有机会持续获取锁。

当一个线程领有了锁之后,它便能够屡次进入 。当然,在这个线程开释锁的时候,那么也须要执行雷同次数的开释动作。比方,一个线程先后 3 次取得了锁,那么它也须要开释 3 次,其余线程才能够持续拜访。这也阐明应用 synchronized 获取的锁,都是 可重入锁

字节序

咱们晓得了对象头的内存构造之后,咱们还须要理解一个很重要的概念:字节序。它示意每一个字节之间的数据在内存中是如何寄存的?如果不了解这个概念,那么在之后打印出对象头时,也会无奈跟上述展现的对象头内存构造互相对应上。

字节序:大于一个字节的数据在内存中的寄存程序。

留神!留神!留神!这里应用了大于,也就是说一个字节内的数据,它的程序是固定的。

  • 大端序(BIG_ENDIAN):高位字节排在内存的低地址处,低位字节排在内存的高地址处。合乎人类的读写程序
  • 小端序(LITTLE_ENDIAN):高位字节排在内存的高地址处,低位字节排在内存的低地址处。合乎计算机的读取程序

咱们来举个例子:

有一个十六进制的数字:0x123456789。

应用大端序浏览:高位字节在前,低位字节在后。

内存地址 1 2 3 4 5
十六进制 0x01 0x23 0x45 0x67 0x89
二进制 00000001 00100011 01000101 01100111 10001001

应用小端序浏览:低位字节在前,高位字节在后。

内存地址 1 2 3 4 5
十六进制 0x89 0x67 0x45 0x23 0x01
二进制 10001001 01100111 01000101 00100011 00000001

既然大端序合乎人类的浏览习惯,那么对立应用大端序不就好了吗?为什么还要搞出一个小端序来呢?

这是因为计算机都是先从低位开始解决的,这样解决效率比拟高,所以计算机外部都是应用小端序。其实计算机也不晓得什么是大端序,什么是小端序,它只会按程序读取字节,先读第一个字节,再读第二个字节。

Java 中的字节序

咱们能够通过上面这一段代码打印出 Java 的字节序:

public class ByteOrderPrinter {public static void main(String[] args){System.out.println(ByteOrder.nativeOrder());
    }
}

打印的后果为:LITTLE_ENDIAN。

因而,咱们能够晓得 Java 中的字节序为 小端字节序。

如何浏览对象头

在了解了字节序之后,咱们来看看如何浏览对象头。

首先,咱们应用一个第三方类库 jol-core,我应用的是 0.10 版本,帮忙咱们打印出对象头的数据。

咱们能够通过上面这一段代码打印出 Java 的对象头:

public class ObjectHeaderPrinter {public static void main(String[] args) throws InterruptedException {Test test = new Test();

        System.out.println("===== 打印匿名偏差锁对象头 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){System.out.println("===== 打印偏差锁对象头 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }

}

打印后果如下:

===== 打印匿名偏差锁 / 无锁对象头 =====
Test 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)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 打印偏差锁对象头 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a0 80 4b (00000101 10100000 10000000 01001011) (1266720773)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
复制代码

咱们把对象头的内存构造和对象头独自拿进去对照着解释一下:

128bit(对象头) 状态
64bit Mark Word 64bit Klass Poiter
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2 匿名偏差锁 / 无锁
threadId:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 偏差锁
ptr_to_lock_record:62 lock:2 轻量级锁
ptr_to_heavyweight_monitor:62 lock:2 重量级锁
lock:2 GC 标记
// 匿名偏差锁 / 无锁
// 咱们给每个字节都标上序号。a        b        c        d
05 00 00 00 (00000101 00000000 00000000 00000000) (5)
                e        f        g        h
00 00 00 00 (00000000 00000000 00000000 00000000) (0)
                i        j        k         l
50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
复制代码

unused:25 位,它实际上的字节应该是:hgf + e 的最高位。

identity_hashcode:31 位,它实际上的字节应该是:e 的低 7 位 + dcb。

unused:1 位,它实际上的字节应该是:a 的最高位。

age:4 位,它实际上的字节应该是:a 的第 4-7 位

biased_lock:1 位,它实际上的字节应该是:a 的第 3 位

lock:2 位,它实际上的字节应该是:a 的低 2 位。

unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2
hgf + e 的最高位 e 的低 7 位 + dcb a 的最高位 a 的第 4-7 位 a 的第 3 位 a 的低 2 位
00000000 00000000 00000000 0 0000000 00000000 00000000 00000000 0 0000 1 01

咱们再来看一个加了偏差锁的对象头:

// 偏差锁
                a        b        c        d
05 90 00 13 (00000101 10010000 00000000 00010011) (318803973)
                e        f        g        h
01 00 00 00 (00000001 00000000 00000000 00000000) (1)
                i        j        k        l
50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
复制代码
threadId:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2
hgfedc + b 的高 6 位 b 的低 2 位 a 的最高位 a 的第 4-7 位 a 的第 3 位 a 的低 2 位
00000000 00000000 00000000 00000001 00010011 00000000 100100 00 0 0000 1 01

偏差锁

偏差锁是 Java 为了进步获取锁的效率和升高获取锁的代价,而进行的一个优化。因为 Java 团队发现大多数的锁都只被一个线程获取。基于这种状况,就能够认为锁都只被一个线程获取,那么就不会存在多个线程竞争的条件,因而就能够不须要真正的去获取一个残缺的锁。只须要在对象头中写入获取锁的线程 ID,用于示意该对象锁曾经被该线程获取。

获取偏差锁,只有批改对象头的标记就能够示意线程曾经获取了锁,大大降低了获取锁的代价。

当线程获取对象的偏差锁时,它的对象头:

threadId:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2

threadId:获取了偏差锁的线程 ID

epoch:用于保留偏差工夫戳

age:对象 GC 年龄

biased_lock:偏差锁标记,此时为 1

lock:锁标记,此时为 10

获取偏差锁

线程获取对象锁时,首先查看对象锁是否反对偏差锁,即查看 biased_lock 是否为 1;如果为 1,那么将会查看threadId 是否为 null,如果为 null,将会通过 CAS 操作将本人的线程 ID 写入到对象头中。如果胜利写入了线程 ID,那么该线程就获取到了对象的偏差锁,能够继续执行前面的同步代码。

只有匿名偏差的对象能力进入偏差锁模式,即该对象还没有偏差任何一个线程(不是相对的,存在批量重偏差的状况)。

开释偏差锁

线程是不会被动开释偏差锁的。只有当其它线程尝试竞争偏差锁时,持有偏差锁的线程才会开释偏差锁。

开释偏差锁须要在全局平安点进行。开释的步骤如下:

  1. 暂停领有偏差锁的线程,判断是否处于同步代码块中,如果处于,则进行偏差撤销,并降级为轻量级锁。
  2. 如果不处于,则复原为无锁状态。

由此能够晓得,偏差锁人造是可重入的。

偏差撤销

偏差撤销次要产生在多个线程存在竞争,不再偏差于任何一个线程了。也就是说偏差撤销之后,将不会再应用偏差锁。具体操作就是将 Mark Work 中的 biased_lock 由 1 设置为 0 偏差撤销须要达到全局平安点才能够撤销,因为它须要批改对象头,并从栈中获取数据。因而偏差撤销也会存在较大的资源耗费。

想要撤销偏差锁,还不能对持有偏差锁的线程有影响,所以就要期待持有偏差锁的线程达到一个 safepoint 平安点, 在这个平安点会挂起取得偏差锁的线程。

  1. 如果原持有偏差锁的线程仍然还在同步代码块中,那么就会将偏差锁降级为轻量级锁。
  2. 如果原持有偏差锁的线程曾经死亡,或者曾经退出了同步代码块,那么间接撤销偏差锁状态即可。

对象的偏差锁被撤销之后,对象在将来将不会偏差于任何一个线程。

批量重偏差

咱们能够设想,如果有 100 个对象都偏差于一个线程,此时如果有另外一个线程来获取这些对象的锁,那么这 100 个对象都会产生偏差撤销,而这 100 次偏差撤销都须要在全局平安点下进行,这样就会产生大量的性能耗费。

批量重偏差就是建设在撤销偏差会对性能产生较大影响状况下的一种优化措施。当 JVM 晓得有大量对象的偏差锁撤销时,它就晓得此时这些对象都不会偏差于原线程,所以会将对象从新偏差于新的线程,从而缩小偏差撤销的次数。

当一个类的大量对象被同一个线程 T1 获取了偏差锁,也就是大量对象先偏差于该线程 T1。T1 同步完结后,另一个线程 T2 对这些同一类型的对象进行同步操作,就会让这些对象从新偏差于线程 T2。

在理解批量重偏差前,咱们须要先理解一点其余常识:

JVM 会给对象的类对象 class 赋予两个属性,一个是偏差撤销计数器,一个是 epoch 值。

咱们先来看一个例子:

import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;

/**
*  @author  liuhaidong
*  @date  2023/1/6 15:06
*/
public class ReBiasTest {public static void main(String[] args) throws InterruptedException {

         // 延时产生可偏差对象
        // 默认 4 秒之后能力进入偏差模式,能够通过参数 -XX:BiasedLockingStartupDelay= 0 设置
        Thread.sleep(5000);

        // 发明 100 个偏差线程 t1 的偏差锁
        List<Test> listA = new ArrayList<>();
        Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {Test a = new Test();
                synchronized (a) {listA.add(a);
                }
            }
            try {
                // 为了避免 JVM 线程复用,在创立完对象后,放弃线程 t1 状态为存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        });
        t1.start();

        // 睡眠 3s 钟保障线程 t1 创建对象实现
        Thread.sleep(3000);
        System.out.println("打印 t1 线程,list 中第 20 个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));

        // 创立线程 t2 竞争线程 t1 中曾经退出同步块的锁
        Thread t2 = new Thread(() -> {
            // 这外面只循环了 30 次!!!for (int i = 0; i < 30; i++) {Test a = listA.get(i);
                synchronized (a) {
                    // 别离打印第 19 次和第 20 次偏差锁重偏差后果
                    if (i == 18 || i == 19) {System.out.println("第" + (i + 1) + "次偏差后果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                    if (i == 10) {
                        // 该对象曾经是轻量级锁,无奈降级,因而只能是轻量级锁
                        System.out.println("第" + (i + 1) + "次偏差后果");
                        System.out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
            try {Thread.sleep(10000);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        System.out.println("打印 list 中第 11 个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        System.out.println("打印 list 中第 26 个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        System.out.println("打印 list 中第 41 个对象的对象头:");
        System.out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
    }
}

在 JDK8 中,-XX:BiasedLockingStartupDelay 的默认值是 4000;在 JDK11 中,-XX:BiasedLockingStartupDelay 的默认值是 0

  1. t1 执行完后,100 个对象都会偏差于 t1。
  2. t2 执行结束之后,其中前 19 个对象都会撤销偏差锁,此时类中的偏差撤销计数器为 19。但当撤销到第 20 个的时候,偏差撤销计数器为 20,此时达到 -XX:BiasedLockingBulkRebiasThreshold=20 的条件,于是将类中的 epoch 值 +1,并在此时找到所有处于同步代码块的对象,并将其 epoch 值等于类对象的 epoch 值。而后进行批量重偏差操作,从第 20 个对象开始,将会比拟对象的 epoch 值是否等于类对象的 epoch 值,如果不等于,那么间接应用 CAS 替换掉 Mark Word 中的程 ID 为以后线程的 ID。
  • 论断:
    1. 前 19 个对象撤销了偏差锁,即 Mark Word 中的 biased_lock 为 0,如果有线程来获取锁,那么先获取轻量级锁。
    2. 第 20 – 30 个对象,仍然为偏差锁,偏差于线程 t2。
    3. 第 31 – 100 个对象,仍然为偏差锁,偏差于线程 t1。

批量撤销偏差

当偏差锁撤销的数量达到 40 时,就会产生批量撤销。然而,这是在一个工夫范畴内达到 40 才会产生,这个工夫范畴通过 -XX:BiasedLockingDecayTime设置,默认值为 25 秒。

也就是在产生批量偏差的 25 秒内,如果偏差锁撤销的数量达到了 40,那么就会产生批量撤销,将该类下的所有对象都进行撤销偏差,包含后续创立的对象。如果在产生批量偏差的 25 秒内没有达到 40,就会重置偏差锁撤销数量,将偏差锁撤销数量重置为 20。

Hashcode 去哪了

咱们通过 Mark Word 晓得,在无锁状态下,如果调用对象的 hashcode() 办法,就会在 Mark Word 中记录对象的 Hashcode 值,在下一次调用 hashcode() 办法时,就能够间接通过 Mark Word 来得悉,而不须要再次计算,以此来保障 Hashcode 的一致性。

然而获取了锁之后,就会批改 Mark Word 中的值,那么之前记录下来的 Hashcode 值去哪里了呢?

Lock Record

在解答这个问题之前,咱们须要先晓得一个货色:Lock Record。

当字节码解释器执行 monitorenter 字节码轻度锁住一个对象时,就会在获取锁的线程栈上显式或者隐式调配一个 Lock Record。换句话说,就是在获取轻量级锁时,会在线程栈上调配一个 Lock Record。这个 Lock Record 说直白一点就是栈上的一块空间,次要用于存储相干信息。

Lock Record 只有有三个作用:

  1. 持有 Displaced Word(就是对象的 Mark Word)和一些元信息用于辨认哪个对象被锁住了。
  2. 解释器应用 Lock Record 来检测非法的锁状态
  3. 隐式地充当锁重入机制的计数器

那么这个 Lock Record 跟 Hashcode 有什么关系呢?

场景 1

咱们先来看第一个场景:先获取对象的 hashcode,而后再获取对象的锁。

import org.openjdk.jol.info.ClassLayout;

public class TestObject {public static void main(String[] args) {Test test = new Test();

        // 步骤 1
        System.out.println("===== 获取 hashcode 之前 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        test.hashCode();

        // 步骤 2
        System.out.println("===== 获取 hashcode 之后 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        // 步骤 3
        synchronized (test){System.out.println("===== 获取锁之后 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }

        // 步骤 4
        System.out.println("===== 开释锁之后 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

运行后果:

===== 获取 hashcode 之前 =====
Test 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)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取 hashcode 之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 0c 97 8b (00000001 00001100 10010111 10001011) (-1953035263)
      4     4        (object header)                           76 00 00 00 (01110110 00000000 00000000 00000000) (118)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取锁之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 2a 90 6b (10010000 00101010 10010000 01101011) (1804610192)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 开释锁之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 0c 97 8b (00000001 00001100 10010111 10001011) (-1953035263)
      4     4        (object header)                           76 00 00 00 (01110110 00000000 00000000 00000000) (118)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 步骤一:未获取对象的 hashcode 值之前,对象处于 匿名偏差锁 状态。锁标记为:101
  • 步骤二:获取对象的 hashcode 之后,对象的偏差状态被撤销,处于无锁状态。锁标记为:001。对象头中也存储了 hashcode 值,hashcode 值为 0111011 10001011 10010111 00001100。
  • 步骤三:获取锁之后,对象处于轻量级锁状态。锁标记为:00。其余 62 位为指向 Lock Record 的指针。从这里咱们能够看到,Mark Word 中曾经没有 hashcode 了。整块 Mark Word 的内容曾经被复制到 Lock Word 中。
  • 步骤四:开释锁之后,对象处于无锁状态。锁标记为:001。在 Mark Word 中也能够看到之前生成的 hashcode。与步骤二中的 Mark Word 截然不同。这是因为在开释锁之后,JVM 会将 Lock Record 中的值复制回 Mark Word 中,并删除 Lock Record。

论断:

  1. 当对象生成 hashcode 之后,会撤销偏差,并将 hashcode 记录在 Mark Word 中。
  2. 非偏差的对象获取锁时,会先在栈中生成一个 Lock Record。并将对象的 Mark Word 复制到 Lock Record 中。

场景 2

咱们当初来看第二个场景:先获取对象的锁,而后在同步代码块中生成 hashcode。

import org.openjdk.jol.info.ClassLayout;

public class HashCode2 {public static void main(String[] args) {Test test = new Test();

        // 步骤一
        System.out.println("===== 获取锁之前 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){
            // 步骤二
            System.out.println("===== 获取锁之后,获取 hashcode 之前 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());

            // 步骤三
            test.hashCode();
            System.out.println("===== 获取锁之后,获取 hashcode 之后 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }

        // 步骤四
        System.out.println("===== 开释锁之后 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());
    }
}

运行后果:

===== 获取锁之前 =====
Test 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)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取锁之后,获取 hashcode 之前 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 80 3a (00000101 10010000 10000000 00111010) (981504005)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取锁之后,获取 hashcode 之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 开释锁之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 步骤一:未获取对象的 hashcode 值之前,对象处于 匿名偏差锁 状态。锁标记为:101
  • 步骤二:进入同步代码块,线程获取了偏差锁。锁标记:101
  • 步骤三:对象生成 hashcode,此时锁标记:10,间接从偏差锁降级为重量级锁。 其余 62 位为指向 objectMonitor 的指针。

与轻量级锁存在同样的问题,hashcode 会寄存在哪里?每一个对象在 JVM 中都有一个 objectMonitor 对象,而 Mark Word 就存储在 objectMonitor 对象的 header 属性中。

轻量级锁

轻量级锁解决的场景是:任意两个线程交替获取锁的状况。次要依附 CAS 操作,相比拟于应用重量级锁,能够缩小锁资源的耗费。

获取轻量级锁

应用轻量级锁的状况有以下几种:

  1. 禁用偏差锁。
  2. 偏差锁生效,降级为轻量级锁。

禁用偏差锁导致降级

在启动 Java 程序时,如果增加了 JVM 参数 -XX:-UseBiasedLocking 那么在后续的运行中,就不再应用偏差锁

偏差锁生效,降级为轻量级锁

如果对象产生偏差撤销时:

  1. 首先会查看持有偏差锁的线程是否曾经死亡,如果死亡,则间接降级为轻量级锁,否则,执行步骤 2
  2. 查看持有偏差锁的线程是否在同步代码块中,如果在,则将偏差锁降级为轻量级锁,否则,执行步骤 3
  3. 批改 Mark Word 为非偏差模式,设置为无锁状态。

加锁过程

当线程获取轻量级锁时,首先会在线程栈中创立一个 Lock Record 的内存空间,而后拷贝 Mark Word 中的数据到 Lock Record 中。JVM 中将有数据的 Lock Record 叫做 Displated Mark Word。

Lock Record 在栈中的内存构造:

临时无奈在飞书文档外展现此内容

当数据复制胜利之后,JVM 将会应用 CAS 尝试批改 Mark Word 中的数据为指向线程栈中 Displated Mark Word 的指针,并将 Lock Record 中的 owner 指针指向 Mark Word。

如果这两步操作都更新胜利了,那么则示意该线程取得轻量级锁胜利,设置 Mark Word 中的 lock 字段为 00,示意以后对象为轻量级锁状态。同步,线程能够执行同步代码块。

如果更新操作失败了,那么 JVM 将会查看 Mark Word 是否指向以后线程的栈帧:

  • 如果是,则示意以后线程曾经获取了轻量级锁,会在栈帧中增加一个新的 Lock Record,这个新 Lock Record 中的 Displated Mark Word 为 null,owner 指向对象。这样的目标是为了统计重入的锁数量,因而,在栈中会有一个 Lock Record 的列表。实现这一步之后就能够间接执行同步代码块。

临时无奈在飞书文档外展现此内容

  • 如果不是,那么示意轻量级锁产生竞争,后续将会收缩为重量级锁。

开释轻量级锁

开释轻量级锁时,会在栈中由低到高,获取 Lock Record。查问到 Lock Record 中的 Displated Mark Word 为 null 时,则示意,该锁是重入的,只须要将 owner 设置为 null 即可,示意曾经开释了这个锁。如果 Displated Mark Word 不为 null,则须要通过 CAS 将 Displated Mark Word 拷贝至对象头的 Mark Word 中,而后将 owner 的指针设置为 null,最初批改 Mark Word 的 lock 字段为 01 无锁状态。

重量级锁

重量级锁解锁的场景是:多个线程相互竞争同一个锁。次要通过 park()unpark()办法,联合队列来实现。相较于轻量级锁和偏差锁,须要切换内核态和用户态环境,因而获取锁的过程会耗费较多的资源。

获取重量级锁

应用重量级锁的状况有两种:

  1. 在持有偏差锁的状况下,间接获取对象的 hashcode,将会间接降级为重量级锁。
  2. 在轻量级锁的状况下,存在竞争,收缩为重量级锁。

获取 hashcode,降级为重量级锁

import org.openjdk.jol.info.ClassLayout;

public class HashCode2 {public static void main(String[] args) {Test test = new Test();

        // 步骤一
        System.out.println("===== 获取锁之前 =====");
        System.out.println(ClassLayout.parseInstance(test).toPrintable());

        synchronized (test){
            // 步骤二
            System.out.println("===== 获取锁之后,获取 hashcode 之前 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());

            // 步骤三
            test.hashCode();
            System.out.println("===== 获取锁之后,获取 hashcode 之后 =====");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }
}

执行后的后果

===== 获取锁之前 =====
Test 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)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取锁之后,获取 hashcode 之前 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 80 3a (00000101 10010000 10000000 00111010) (981504005)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

===== 获取锁之后,获取 hashcode 之后 =====
Test object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 e8 83 2a (00000010 11101000 10000011 00101010) (713287682)
      4     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4        (object header)                           50 6a 06 00 (01010000 01101010 00000110 00000000) (420432)
     12     4    int Test.i                                    0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

咱们间接在偏差锁的同步代码块中执行 hashcode(),会发现偏差锁间接收缩为重量级锁了。咱们能够看到 lock 字段为 10。

这里有一个疑难,为什么不是降级为轻量级锁呢?轻量级锁也能够在 Lock Record 中存储生成的 hashcode。而收缩为更为耗费资源的重量级锁。

轻量级锁收缩为重量级锁

当处于轻量级锁的时候,阐明锁曾经不再偏差于任何一个线程,然而也没有产生竞争,能够依附 CAS 获取到轻量级锁。然而当呈现 CAS 获取锁失败时,就会间接收缩为重量级锁。

这里须要留神,只会 CAS 一次,只有一次失败就会间接收缩为重量级锁,而不是达到自旋次数或者自旋工夫才收缩。

收缩过程

在收缩过程中,会有几种标记来示意锁的状态:

  • Inflated:收缩已实现
  • Stack-locked:轻量级锁
  • INFLATING:收缩中
  • Neutral:无锁

收缩步骤:

  1. 查看是否曾经为重量级锁,如果是间接返回。
  2. 查看是否处于收缩中的状态,如果是,循环检测状态。检测出收缩中的状态是因为有其余线程正在进行收缩,因为须要期待收缩实现之后,能力继续执行。
  3. 查看是否为轻量级锁,如果是,则执行以下步骤:

    1. 创立一个 ObjectMonitor 对象。
    2. 通过 CAS 设置 Mark Word 为全 0,用以示意 INFLATING 状态。如果失败,则从步骤 1 从新开始执行。
    3. 将 Mark Word 设置到 ObjectMonitor 对象中。
    4. 设置 owner 属性为 Lock Record
    5. 设置 Mark Word 值
    6. 返回
  4. 断定为无锁状态,执行以下步骤:

    1. 创立一个 ObjectMonitor 对象。
    2. 通过 CAS 间接设置 Mark Word 值。
    3. 返回

竞争锁过程

咱们要了解如何获取重量级锁,须要先理解 ObjectMonitor 对象。顾名思义,这是一个对象监视器。在 Java 中,每个对象都有一个与之对应的 ObjectMonitor。ObjectMonitor 外部有几个重要的字段:

  • cxq:寄存被阻塞的线程
  • EntryList:寄存被阻塞的线程,在开释锁时应用
  • WaitSet:取得锁的线程,如果调用 wait() 办法,那么线程会被寄存在此处,这是一个双向循环链表
  • onwer:持有锁的线程

cxq,EntryList 均为 ObjectWaiter 类型的单链表。

获取锁过程

  1. 通过 CAS 设置 onwer 为以后线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread,如果胜利,则示意取得锁。否则执行步骤 2
  2. 判断以后线程与获取锁线程是否统一,如果统一,则示意取得锁(锁重入)。否则执行步骤 3
  3. 判断以后线程是否为之前持有轻量级锁的线程,如果是,间接设置 onwer 为以后线程,示意取得锁。否则执行步骤 4
  4. 以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤 5
  5. 应用阻塞和唤醒来控制线程竞争锁

    1. 通过 CAS 设置 owner 为以后线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果胜利,则示意取得锁。否则执行步骤 b
    2. 通过 CAS 设置 owner 为以后线程(尝试获取锁)CAS 的原值为 DEFLATER_MARKER,新值为 current_thread。如果胜利,则示意取得锁。否则执行步骤 c。(DEFLATER_MARKER 是一个锁降级的标记,后续会解说。)
    3. 以上步骤都失败,则尝试一轮自旋来获取锁。如果未获取锁,则执行步骤 d。
    4. 为以后线程创立一个 ObjectWaiter 类型的 node 节点。步骤 i 和 ii 是一个循环,直到一个胜利才会跳出这个循环。

      1. 通过 cas 插入 cxq 的头部,如果插入失败,则执行步骤 ii
      2. 通过 CAS 设置 owner 为以后线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果失败,则执行 i。
    5. 通过 CAS 设置 owner 为以后线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果胜利,则示意取得锁。否则执行步骤 f。(该步骤往下开始是一个循环,直到获取到锁为止)
    6. 通过 park(),将线程阻塞。
    7. 线程被唤醒后

      1. 通过 CAS 设置 owner 为以后线程(尝试获取锁),CAS 的原值为 NULL,新值为 current_thread。如果胜利,则示意取得锁。否则执行步骤 ii
      2. 通过 CAS 设置 owner 为以后线程(尝试获取锁)CAS 的原值为 DEFLATER_MARKER,新值为 current_thread。如果胜利,则示意取得锁。否则执行 iii
      3. 尝试一轮自旋来获取锁。如果未获取锁,则跳转回步骤 e 执行。

自适应自旋锁次要是用于重量级锁中,升高阻塞线程概率。而不是用于轻量级锁,这里大家要多多留神。

开释重量级锁

开释锁过程

  1. 判断 _owner 字段是否等于 current_thread。如果等于则判断以后线程是否为持有轻量级锁的线程,如果是的话,示意该线程还没有执行 enter()办法,因而,间接设置 _owner 字段为 current_thread。
  2. 判断 _recursions,如果大于 0,则示意锁重入,间接返回即可,不须要执行后续解锁代码。
  3. 设置 _owner 字段为 NULL,解锁胜利,后续线程能够失常获取到锁。
  4. 唤醒其余正在被阻塞的线程。在执行以下操作之前须要应用该线程从新获取锁。如果获取锁失败,则示意锁曾经被其余线程获取,间接返回,不再唤醒其余线程。(为什么还要获取到锁才能够唤醒其余线程呢?因为唤醒线程时,须要将 cxq 中的节点转移到 EntryList 中,波及到链表的挪动,如果多线程执行,将会出错。)

    1. 如何 _EntryList 非空,那么取 _EntryList 中的第一个元素,将该元素下的线程唤醒。否则执行步骤 b。
    2. 将 _cxq 设置为空,并将 _cxq 的元素依照原程序放入 _EntryList 中。而后取 _EntryList 中的第一个元素,将该元素下的线程唤醒。
    3. 线程唤醒

      1. 设置 _owner 字段为 NULL,解锁胜利,让后续线程能够失常获取到锁。
      2. 而后调用 unpark() 办法,唤醒线程。

wait(),notify(),notifyAll()

咱们须要晓得一个前提,在解决 wait 办法时,必须应用重量级锁。因而,wait 办法会导致锁降级。

咱们先来看一个例子:

public class WaitTest {static final Object lock = new Object();

    public static void main(String[] args) {new Thread(() -> {synchronized (lock){log("get lock");
                try {log("wait lock");
                    lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);
                }
                log("get lock again");
                log("release lock");
            }
        }, "thread-A").start();

        sleep(1000);

        new Thread(() -> {synchronized (lock){log("get lock");
                createThread("thread-C");
                sleep(2000);
                log("start notify");
                lock.notify();
                log("release lock");
            }
        }, "thread-B").start();}

    public static void createThread(String threadName) {new Thread(() -> {synchronized (lock){log("get lock");
                log("release lock");
            }
        }, threadName).start();}

    private static void sleep(long sleepVal){
        try{Thread.sleep(sleepVal);
        }catch(Exception e){e.printStackTrace();
        }
    }
    private static void log(String desc){System.out.println(Thread.currentThread().getName() + ":" + desc);
    }
}

最初打印的后果:

thread-A : get lock
thread-A : wait lock
thread-B : get lock
thread-B : start notify
thread-B : release lock
thread-A : get lock again
thread-A : release lock
thread-C : get lock
thread-C : release lock
  1. 线程 A 首先获取到锁,而后通过 wait() 办法,将锁开释,并且期待告诉。
  2. 睡眠 1 S,这里是确保线程 A 能够顺利完成所有操作。
  3. 因为 A 开释了锁,所以线程 B 能够获取到锁。而后创立了线程 C。
  4. 因为线程 B 睡眠了 2S,仍然持有锁,所以线程 C 无奈获取到锁,只能持续期待。
  5. 线程 B 调用 notify() 办法,线程 A 被唤醒,开始竞争锁。
  6. 线程 A 和线程 C 竞争锁。

然而依据打印后果,无论执行多少次,都是线程 A 先获取锁。

第一个问题:为什么都是线程 A 先获取锁,而不是线程 C 先获取锁?

第二个问题:为什么 wait 办法并没有生成 monitorenter 指令,也能够获取到锁?

第三个问题:执行 wait 之后,线程去哪里了?它的状态是什么?

为了解答这些问题,咱们须要深刻到源码中去。然而这里就不放源码了,我只讲一下关键步骤:

wait()

  1. 收缩为重量级锁
  2. 为 current_thread 创立 ObjectWaiter 类型的 node 节点
  3. 将 node 放入 _waitSet 中
  4. 开释锁
  5. 通过 park() 阻塞 current_thread。

notify()

  1. 查看 _waitSet 是否为 null,如果为 null,间接返回
  2. 获取 _waitSet 的第一个元素 node,并将其从链表中移除。
  3. 此时,存在三个策略:默认应用 policy = 2

    1. 插入到 EntryList 的头部(policy = 1)
    2. 插入到 EntryList 的尾部(policy = 0)
    3. 插入到 cxq 的 头部(policy = 2)
  4. 将 node 插入到 cxq 的头部。

notifyAll()

  1. 循环检测 _waitSet 是否不为空

    1. 如果不为空,则执行 notify() 的步骤。
    2. 否则返回

第一个问题:执行 wait 之后,线程去哪里了?它的状态是什么?

线程 A 调用 wait() 办法后,线程 A 就被 park 了,并被放入到 _waitSet 中。此时他的状态就是 WAITING。如果它从 _waitSet 移除,并被放入到 cxq 之后,那么他的状态就会变为 BLOCKED。如果它竞争到锁,那么他的状态就会变为 RUNNABLE

第二个问题:为什么 wait 办法并没有生成 monitorenter 指令,也能够获取到锁?

线程 A 调用 wait() 办法后,线程 A 被放入到 _waitSet 中。直到有其余线程调用 notify() 之后,线程 A 从 _waitSet 移除,并放入到 cxq 中。

第三个问题:为什么都是线程 A 先获取锁,而不是线程 C 先获取锁?

线程 A 调用 wait() 办法后,线程 A 被放入到 _waitSet 中。线程 B 获取锁,而后创立了线程 C,线程 C 竞争锁失败,被放入到 cxq 中。而后 B 调用 notify() 办法后,线程 A 从 _waitSet 移除,放入到 cxq 的头部。因而目前 cxq 的链表构造为:A -> C -> null。接着线程 B 开释锁,会将 cxq 中的元素依照原程序放入到 EntryList 中,因而目前 cxq 链表构造为:null;EntryList 链表构造为:A -> C -> null。而后唤醒 EntryList 中的第一个线程。

所以,每次都是线程 A 先获取锁。

正文完
 0