开篇语

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。

tech.youzan.com/javasuo-yu-…

暂时无法在飞书文档外展现此内容

批量吊销倾向

当倾向锁吊销的数量到达 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 先获取锁。