Java并发系列(14)——synchronized之HotSpot源码解读(上)

上一篇:《Java并发系列(13)——线程池的选择与参数设置》

文章目录

    • 10 synchronized 实现原理
      • 10.1 研究思路
        • 10.1.1 输出 JVM 指令
        • 10.1.2 跟踪 JVM 源码
      • 10.2 预备知识
        • 10.2.1 对象头
          • 10.2.1.1 什么是对象头
          • 10.2.1.2 打印对象头
          • 10.2.1.3 小端存储
        • 10.2.2 用户态与内核态
          • 10.2.2.1 用户态与内核态
          • 10.2.2.2 用户线程与内核线程
      • 10.3 Hashtable 性能测试
        • 10.3.1 测试一
        • 10.3.2 测试二
        • 10.3.3 测试三
        • 10.3.4 测试四
        • 10.3.5 测试结果汇总
      • 10.4 偏向锁
        • 10.4.1 第一次加锁
        • 10.4.2 释放锁
        • 10.4.3 mark word
        • 10.4.4 hash code
        • 10.4.5 再次加锁
        • 10.4.6 重入
        • 10.4.7 示意图

10 synchronized 实现原理

10.1 研究思路

synchronized 实现已经不属于 Java 层面了,它是 JVM 的范畴,而且不是 JVM 规范而是 JVM 实现的范畴,比如:HotSpot,JRocket,J9 等。

所以要研究 synchronized 实现,有能力最好是研读 JVM 源码。

10.1.1 输出 JVM 指令
  • 进入 synchronized 块:monitorenter;
  • 退出 synchronized 块:monitorexit;

有一个 monitorenter 至少有一个 monitorexit,通常会有多个。

Java 代码:

package per.lvjc.concurrent.synchro;

public class SynchroTest {
   

    public static void main(String[] args) {
   
        synchronized (SynchroTest.class) {
   
            System.out.println();
            synchronized (SynchroTest.class) {
   
                try {
   
                    System.out.println();
                } catch (Exception e) {
   
                    System.out.println();
                }
            }
        }
    }
}

javap:

javap -v -l -c -p C:\Users\lvjc\IdeaProjects\concurrent\target\classes\per\lvjc\concurrent\synchro\SynchroTest.class > D:
\data\jvm\SynchroTest_javap.txt

输出的 SynchroTest_javap.txt 文件,节选其中 main 方法:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=6, args_size=1
         0: ldc           #2                  // class per/lvjc/concurrent/synchro/SynchroTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: invokevirtual #4                  // Method java/io/PrintStream.println:()V
        11: ldc           #2                  // class per/lvjc/concurrent/synchro/SynchroTest
        13: dup
        14: astore_2
        15: monitorenter
        16: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: invokevirtual #4                  // Method java/io/PrintStream.println:()V
        22: goto          32
        25: astore_3
        26: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: invokevirtual #4                  // Method java/io/PrintStream.println:()V
        32: aload_2
        33: monitorexit
        34: goto          44
        37: astore        4
        39: aload_2
        40: monitorexit
        41: aload         4
        43: athrow
        44: aload_1
        45: monitorexit
        46: goto          56
        49: astore        5
        51: aload_1
        52: monitorexit
        53: aload         5
        55: athrow
        56: return
      Exception table:
         from    to  target type
            16    22    25   Class java/lang/Exception
            16    34    37   any
            37    41    37   any
             5    46    49   any
            49    53    49   any

这里面就有 2 个 monitorenter 和 4 个 monitorexit。

下面列举 4 种场景,把这 4 个 monitorexit 都用到:

  • 正常运行:偏移 4 字节处的 monitorenter -> 偏移 15 字节的 monitorenter -> 偏移 33 字节的 monitorexit -> 偏移 45 字节的 monitorexit;
  • 里面的 synchronized 块抛 Exception:4 monitorenter -> 15 monitorenter -> 16 ~ 22(不含) 之间抛 Exception,根据异常表 goto 25 -> 33 monitorexit -> 45 monitorexit;
  • 外面的 synchronized 块抛 Throwable:4 monitorenter -> 5 ~ 11 之间抛 Throwable,根据异常表 goto 49 -> 52 monitorexit;
  • 里面的 synchronized 块抛 Error:4 monitorenter -> 15 monitorenter -> 16 ~ 34 之间抛 Error,根据异常表 goto 37 -> 40 monitorexit -> 45 monitorexit。
10.1.2 跟踪 JVM 源码

进一步,monitorenter 和 monitorexit 指令是怎么实现的,那就要深入到 JVM 源码。

虽然 JVM 是 JDK 的一部分,但 Java 开发所使用的 JDK 是经过编译的,里面已经看不到 JVM 源码了。

OpenJDK 是开源的,可以在官网下载到未编译的源码:OpenJDK8 源码下载。

下载到源码之后,可以在 bytecodeInterpreter.cpp 文件里面看到所有 JVM 指令的实现入口:

Java并发系列(14)——synchronized之HotSpot源码解读(上)_第1张图片

图上左边可以看到所有的 JVM 指令,每个 JVM 指令用一个字节表示,所以 JVM 指令最多 256 个。

搜索 monitorenter 指令,就可以找到它的实现:

Java并发系列(14)——synchronized之HotSpot源码解读(上)_第2张图片

此处暂时不深入 JVM 源码,仅提供一种研究思路,有能力有时间的读者可自行研究源码。

10.2 预备知识

synchronized 实现其实很容易猜出来,与 Java 实现的 AQS 原理几乎是一样的。

10.2.1 对象头

根据 AQS 的实现思路,要实现一个锁,只需要存储三个信息即可:

  • 锁状态:由当前锁状态信息计算可以知道当前是否允许获得锁,在 AQS 体系中是 state 和 exclusiveOwnerThread 变量;
  • 竞争队列:存储在竞争同一个锁对象的所有线程,以便释放锁时唤醒,在 AQS 中是 head 和 tail 变量以及 Node 内部类的 pre,next 变量记录的一个双向链表;
  • 休眠队列:存储主动让出 CPU 资源和锁资源进入休眠暂时不参与 CPU 调度的所有线程,在 AQS 中是 ConditionObject 内部类中 firstWaiter 和 lastWaiter 变量以及 Node 内部类的 nextWaiter 变量记录的一个单向链表。

队列的实现很简单,锁状态的存储与计算是一件比较麻烦的事情,而且会随着锁的特性变多而变得更复杂,比如:

  • ReentrantLock 用 exclusiveOwnerThread 变量存储当前持有锁的线程,用 state 存储重入次数;
  • ReentrantReadWriteLock 由于区分读写锁,就要把 state 变量一分为二,高 16 位存储共享锁,低 16 位存储独占锁,同时还需要一个 ThreadLocal 存储各个读线程重入共享锁的次数。

而 synchronized 是怎么存储锁状态的呢?答案是对象头。

后面可以看到 synchronized 加锁与解锁其实就是在玩转对象头,这也是为什么说 synchronized 是对象锁,它锁的一定是对象,因为没有对象就没有对象头,以当前 synchronized 实现来说是加不了锁的。

10.2.1.1 什么是对象头

一个 Java 对象里面包含的数据主要有三部分:

  • 对象头;
  • 实例变量,比如类里面定义了一个 int,那么这里就有 4 个字节;
  • 对齐填充:一个对象占用的总内存必须是 8 的整数倍,不足的部分以空位填充整齐。

对象头又分为两个部分(也可以说是三部分):

  • mark word,32 位系统占用 4 字节,64 位系统占用 8 字节;
  • class pointer,指向当前对象在方法区中的 class 定义的首地址,32 位系统占用 4 字节,64 位系统不开启指针压缩占用 8 字节,开启指针压缩占用 4 字节;
  • 数组长度,数组对象才会有,因为是 int,所以占用 4 字节。

mark word 比较复杂,它可能会存储下面这些数据:

  • 偏向锁当前偏向的线程 id;
  • 偏向锁过期标识;
  • 指向栈帧中 lock record(锁记录)对象(C++对象)的指针;
  • 指向 ObjectMonitor 对象(C++对象)的指针;
  • hash code;
  • 分代年龄;
  • 是否可偏向标识;
  • 锁标识;
  • gc 标识。

为什么说“可能”呢,因为 mark word 空间有限,这里面有些数据是互斥的,“有你没我”。

关于 mark word 暂时只提这么多,后面会结合具体的场景来详细说明。

10.2.1.2 打印对象头

后面会经常把锁对象的对象头打印出来查看,所以这里先讲一下打印对象头的方法。

引入 OpenJDK 提供的一个 jol 包:

        
        <dependency>
            <groupId>org.openjdk.jolgroupId>
            <artifactId>jol-coreartifactId>
            <version>0.14version>
        dependency>

非数组对象的对象头:

package per.lvjc.concurrent.synchro;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

@Slf4j(topic = "synchro")
public class CommonObjectHeader {
   

    private static int i = 1;

    private boolean bo = false;
    private long lo = 6;
    private Object o = new Object();

    public static void main(String[] args) {
   
        log.debug(ClassLayout.parseInstance(new CommonObjectHeader()).toPrintable());
    }
}

运行结果(64 位系统,默认开启指针压缩):

22:30:38.243 [main] DEBUG synchro - per.lvjc.concurrent.synchro.CommonObjectHeader 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     1            boolean CommonObjectHeader.bo                     false
     13     3                    (alignment/padding gap)                  
     16     8               long CommonObjectHeader.lo                     6
     

你可能感兴趣的:(并发,java,synchronized,多线程,并发,偏向锁)