上一篇:《Java并发系列(13)——线程池的选择与参数设置》
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 指令的实现入口:
图上左边可以看到所有的 JVM 指令,每个 JVM 指令用一个字节表示,所以 JVM 指令最多 256 个。
搜索 monitorenter 指令,就可以找到它的实现:
此处暂时不深入 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 包:
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</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