关于深入理解并发锁机制之synchronized
一、Java共享内存模型带来的线程安全问题
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做5000 次,结果并不一定为0:
public class SyncDemo {
private static int counter = 0;
public static void increment() {
counter++;
}
public static void decrement() {
counter--;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(">>>>>>>>>> " + counter);
}
}
执行结果可能是正数、负数、零,因为 Java 中对静态变量的自增、自减并不是原子操作。
JVM 字节码指令查看 ( 在 idea 中安装 jclasslib 插件,之后在 View 打开查看)
i++ 的 JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
i-- 的 JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
isub // 自减
putstatic i // 将修改后的值存入静态变量i
若为单线程,上述8行字节码指令是顺序执行(不会交错),不会产生问题。
但多线程下此8行字节码指令可能交错运行:
临界区( Critical Section)
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
//临界资源
private static int counter = 0;
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter‐‐;
}
竞态条件( Race Condition )
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
为了避免临界区的竞态条件发生,有多种手段可以达到目的:
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量(CAS)
注意:
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码;
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。
二、synchronized的使用
synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
加锁方式
解决上述的共享问题
方式一:
public synchronized static void increment() {
counter++;
}
public synchronized static void decrement() {
counter--;
}
方式二:
private static String lock = "";
public synchronized static void increment() {
synchronized (lock) {
counter++;
}
}
public synchronized static void decrement() {
synchronized (lock) {
counter--;
}
}
synchronized 实际是用对象锁保证了临界区内代码的原子性:
三、synchronized底层原理
synchronized 是 JVM 内置锁,基于 Monitor(操作系统里称为管程,Java里称为监视器)机制实现,依赖底层操作系统的互斥原语 Mutex(互斥量),它是一个重量级锁,性能较低。
JVM 内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与 Lock 持平。
The Java® Language Specification
Each object is associated with a monitor (§17.1), which is used by synchronized methods (§8.4.3) and the synchronized statement (§14.19) to provide control over concurrent access to state by multiple threads (§17 (Threads and Locks)).
The Java® Virtual Machine Specification
The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.
JVM 通过一个同步结构支持方法和方法中的指令序列的同步:monitor。
同步方法是通过方法中的 access_flags 中设置 ACC_SYNCHRONIZED 标志来实现;
同步代码块是通过 monitorenter 和 monitorexit 来实现。
两个指令的执行是 JVM 通过调用操作系统的互斥原语 mutex 来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
查看synchronized的字节码指令序列
Method access and property flags:
Monitor(管程/监视器)
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。
管程是指管理共享变量以及对共享变量操作的过程,让其支持并发。
在 Java 1.5之前,Java 语言提供的唯一并发语言就是管程,Java 1.5 之后提供的 SDK 并发包也是以管程为基础的。除了 Java 之外,C/C++、C# 等高级语言也都是支持管程的。
synchronized 关键字和 wait()、notify()、notifyAll() 这三个方法是 Java中 实现管程技术的组成部分。
MESA模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare 模型和 MESA 模型。
现在正在广泛使用的是 MESA 模型:
管程中引入了条件变量的概念,且每个条件变量都对应有一个等待队列。
条件变量和等待队列的作用是解决线程之间的同步问题。
wait()的正确使用
对于 MESA 管程来说,有一个编程范式:
while(条件不满足) {
wait();
}
唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足,所以循环检验条件。
MESA 的 wait() 还有一个超时参数,为了避免线程进入等待队列永久阻塞。
notify()和notifyAll()使用条件
满足以下三个条件时,可以使用 notify(),其余情况尽量使用 notifyAll():
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需唤醒一个线程。
Java语言的内置管程synchronized
Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量:
Monitor机制在Java中的实现
java.lang.Object 类定义了 wait(),notify(),notifyAll(),这些方法的具体实现依赖于 ObjectMonitor 实现,为 JVM 内部基于 C++ 实现的一套机制。
hotspot 源码 ObjectMonitor.hpp:
ObjectMonitor() {
_header = NULL; // 对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; // 存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 多线程竞争锁会先存到这个单向链表中(FILO栈结构)
FreeNext = NULL;
_EntryList = NULL; // 存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_SpinFreq = 0
_SpinClock = 0;
OwnerIsThread = 0;
_previous_owner_tid = 0;
}
在获取锁时,将当前线程插入到 _cxq 的头部。
释放锁时,默认策略(QMode=0):
若 _EntryList 为空,则将 _cxq 中的元素按原有顺序插入到 _EntryList,并唤醒第一个线程,即当 _EntryList 为空时,后来的线程先获取锁。
若 _EntryList 不为空,直接从 _EntryList 中唤醒线程。
对象的内存布局
Hotspot 虚拟机中,对象在内存中存储的布局分为三块区域:
- 对象头(Header):
如 hash 码、对象所属的年代、对象锁、锁状态标志、偏向锁(线程)ID、偏向时间、数组长度(数组对象才有)等。
- 实例数据(Instance Data):
存放类的属性数据信息,包括父类的属性信息;
- 对齐填充(Padding):
由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头详解
HotSpot 虚拟机的对象头:
- Mark Word
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为 32bit 和 64bit。
- Klass Pointer
对象头的另外一部分是 klass 类型指针,即对象指向其类元数据的指针,虚拟机通过此指针来确定此对象属于哪个类的实例。
32位为4字节,64位开启指针压缩或最大堆内存<32g时也为4字节,否则为8字节。JDK8 默认开启指针压缩后为4字节,当在 JVM 参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度则为8字节。
- 数组长度(只有数组对象有)
若对象是一个数组,则对象头中还会有一块数据用于记录数组长度,大小为4字节:
使用JOL工具查看内存布局
可以查看普通 Java 对象的内部布局工具 JOL(JAVA OBJECT LAYOUT),使用此工具可以查看 new 出来的一个 Java 对象的内部布局,及一个普通的 Java 对象占用多少字节。
引入maven依赖:
<!‐‐ 查看Java 对象布局、大小工具 ‐‐>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.10</version>
</dependency>
使用方法:
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
测试:
public static void main(String[] args) throws InterruptedException {
bject obj = new Object();
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
- 利用 jol 查看64位系统 Java 对象(空对象),默认开启指针压缩,总大小显示16字节,前12字节为对象头
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位为字节;
- TYPE DESCRIPTION:类型描述,其中 object header 为对象头;
- VALUE:对应内存中当前存储的值,二进制32位;
- 关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops
下述 obj 对象占多少个字节?
public class ObjectTest {
public static void main(String[] args) throws InterruptedException {
Object obj = new Test();
//查看对象内部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
class Test{
private long p;
}
Mark Word 8字节 + Klass Pointer 4字节 + long 8字节 = 20字节,不为8的倍数,因此会填充 4字节,最终结果为 24字节。
Mark Word记录锁状态
锁状态被记录在每个对象的对象头中的 Mark Word 里。
Hotspot 通过 markOop 类型实现 Mark Word,具体实现位于 markOop.hpp 文件中。
由于对象需要存储的运行时数据很多,需考虑到虚拟机的内存使用,markOop 被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。
简单来说:MarkWord 结构之所以复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。
Mark Word的结构:
// 32 bits:
// ‐‐‐‐‐‐‐‐
// hash:25 ‐‐‐‐‐‐‐‐‐‐‐‐>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
// PromotedObject*:29 ‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
//
// 64 bits:
// ‐‐‐‐‐‐‐‐
// unused:25 hash:31 ‐‐>| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| promo_bits:3 ‐‐‐‐‐>| (CMS promoted object)
// size:64 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (CMS free block)
//
// unused:25 hash:31 ‐‐>| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ‐‐‐‐‐>| (COOPs && CMS promoted object)
// unused:21 size:35 ‐‐>| cms_free:1 unused:7 ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐>| (COOPs && CMS free block)
。。。。。。
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
[0 | epoch | age | 1 | 01] lock is anonymously biased
//
// ‐ the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
- hash: 保存对象的哈希码。
运行期间调用 System.identityHashCode() 来计算,延迟计算,并把结果赋值到此处。 - age: 保存对象的分代年龄。
表示对象被 GC 的次数,当该次数到达阈值时,对象就会转移到老年代。 - biased_lock: 偏向锁标识位。
由于无锁和偏向锁的锁标识都是 01,无法区分,因此引入一位的偏向锁标识位。 - lock: 锁状态标识位。
区分锁状态,比如11时表示对象 GC 待回收状态,只有最后2位锁标识(11)有效。 - JavaThread*: 保存持有偏向锁的线程 ID。
偏向模式时,若某个线程持有对象,则对象就被置为该线程的 ID。 在之后的操作中,就无需再进行尝试获取锁的动作。此线程 ID 并不是 JVM 分配的线程 ID 号,和 Java Thread 中的 ID 是两个概念。 - epoch: 保存偏向时间戳。
偏向锁在 CAS 锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
32位JVM下的对象结构描述:
64位JVM下的对象结构描述:
-
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
当锁获取是无竞争时,JVM 使用原子操作而不是 OS 互斥,此技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的 Mark Word 中设置指向锁记录的指针。
-
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。
若两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁升级到 Monitor 重量级锁以管理等待的线程。在重量级锁的情况下,JVM 在对象的 ptr_to_heavyweight_monitor 设置指向 Monitor 的指针。
Mark Word中锁标记枚举:
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
};
四、利用JOL工具跟踪锁标记变化
偏向锁
偏向锁是一种针对加锁操作的优化手段,经研究发现,大多数情况下,锁不仅不存在多线程竞争,且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。
对于没有锁竞争的场合,偏向锁有很好的优化效果。
/***StringBuffer内部同步***/
public synchronized int length() {
return count;
}
//System.out.println 无意识的使用锁
public void println(String x) {
synchronized (this) {
print(x); newLine();
}
}
当 JVM 启用了偏向锁模式(JDK 1.6 默认开启),新创建对象的 Mark Word 中的Thread ID 为 0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。
偏向锁延迟偏向
偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM 启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。
为了减少初始化时间,JVM默认延时加载偏向锁。
//关闭延迟开启偏向锁
‐XX:BiasedLockingStartupDelay=0
//禁止偏向锁
‐XX:‐UseBiasedLocking
//启用偏向锁
‐XX:+UseBiasedLocking
验证:
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
Thread.sleep(4000);
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
}
4s后偏向锁为可偏向或者匿名偏向状态
偏向锁状态跟踪
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
//Hotspot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
当对象调用了 hashCode 时,是否还会开启偏向锁模式:
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
//Hotspot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
//若对象调用了hashCode,还会开启偏向锁模式吗
obj.hashCode();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
偏向锁撤销之调用对象HashCode
调用锁对象的 obj.hashCode() 或 System.identityHashCode(obj) 方法会导致该对象的偏向锁被撤销。
因为对于一个对象,其 HashCode 只会生成一次并保存,偏向锁是没有地方保存 hashcode 的。
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
当对象处于可偏向(即线程ID为0)和已偏向的状态下,调用 HashCode 计算将会使对象再也无法偏向:
- 当对象可偏向时,MarkWord 将变成未锁定状态,并只能升级成轻量锁;
- 当对象正处于偏向锁时,调用 HashCode 将使偏向锁强制升级成重量锁。
偏向锁撤销之调用wait/notify
偏向锁状态执行 obj.notify() 会升级为轻量级锁,调用 obj.wait(timeout) 会升级为重量级锁:
synchronized (obj) {
//偏向锁执行过程中,调用hashCode会发生什么?
//obj.hashCode();
//obj.notify();
try {
obj.wait(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
测试结果:
轻量级锁
若偏向锁失败,虚拟机不会立即升级为重量级锁,而是尝试使用轻量级锁的优化手段,此时 Mark Word 的结构也变为轻量级锁的结构。
轻量级锁适用于线程交替执行同步块的场合
,若存在同一时间多个线程访问同一把锁,则会导致轻量级锁膨胀为重量级锁。
轻量级锁跟踪
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
//Hotspot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
//若对象调用了hashCode,不会开启偏向锁模式,
//因为偏向锁的Mark Word没有地方存放hashCode,而无锁的Mark Word有专门存放hashCode的地方,
//因此会撤销偏向锁变成无锁状态
obj.hashCode();
//System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + ClassLayout.parseInstance(obj).toPrintable());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
轻量级锁是否可以降级为偏向锁:
五、锁升级场景
偏向锁升级轻量级锁
模拟两个线程轻微竞争场景:
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
//Hotspot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1");
thread1.start();
// 控制线程竞争时机
Thread.sleep(1);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread2");
thread2.start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
轻量级锁膨胀为重量级锁
public class LockEscalationDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
//Hotspot虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
Thread.sleep(4000);
Object obj = new Object();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread1").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "开始执行……"
+ ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "获得锁执行中……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>" + Thread.currentThread().getName() + "释放锁……"
+ ClassLayout.parseInstance(obj).toPrintable());
}
}, "thread2").start();
Thread.sleep(5000);
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
总结:锁对象状态转换
六、synchronized锁优化
偏向锁 批量重偏向 & 批量撤销
从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但当有其他线程尝试获得锁时,就需要等到 safe point(即)时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,反而降低性能
。因此才有了批量重偏向与批量撤销的机制。
原理
以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器 +1,当这个值达到重偏向阈值(默认20)时,JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。
每个 class 对象会有一个对应的 epoch 字段,每个处于偏向锁状态对象的 Mark Word 中也有该字段,其初始值为创建该对象时 class 中的 epoch 的值。每次发生批量重偏向时,就将该值 +1,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其 epoch 字段改为新值。下次获得锁时,发现当前对象的 epoch 值和 class 的 epoch 不相等,则表示当前已经偏向了其他线程,即不执行撤销操作,而是直接通过 CAS 操作将其 Mark Word 的 Thread ID 改成当前的线程 ID。
当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
应用场景
批量重偏向(bulk rebias)机制是为了解决:
一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
批量撤销(bulk revoke)机制是为了解决:
在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
JVM的默认参数值
设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出 JVM 的默认参数值
intx BiasedLockingBulkRebiasThreshold = 20 //默认偏向锁批量重偏向阈值
intx BiasedLockingBulkRevokeThreshold = 40 //默认偏向锁批量撤销阈值
可通过上述两个参数来手动设置阈值
测试:批量重偏向
当撤销偏向锁阈值超过 20 次后,JVM 会认为偏向是否出错,给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象的 Thread ID。
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
// 延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
// 为了防止 JVM 线程复用,创建完对象后,保持线程thread1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1").start();
// 睡眠3s,保证线程thread1创建对象完成
Thread.sleep(3000);
System.out.println(">>>>>>>>> 打印thread1,list中第20个对象的对象头:");
System.out.println((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if (i >= 15 && i <= 21 || i >= 38) {
System.out.println(">>>>>>>>>> thread2-第" + (i + 1) + "次加锁执行中" + ClassLayout.parseInstance(obj).toPrintable());
}
}
if (i == 17 || i == 19) {
System.out.println(">>>>>>>>>> thread2-第" + (i + 1) + "次释放锁" + ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
LockSupport.park();
}
}
测试结果:
thread1:
创建 50 个偏向线程 thread1 的偏向锁
1-50 偏向锁
thread2:
1-18 偏向锁撤销,升级为轻量级锁 (thread1释放锁之后为偏向锁状态)
19-40 偏向锁撤销达到阈值(20),执行了批量重偏向(测试结果在第19就开始批量重偏向了)
测试:批量撤销
当撤销偏向锁阈值超过 40 次后,JVM 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
注意:时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清0,重新计时。
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
// 延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
// 为了防止 JVM 线程复用,创建完对象后,保持线程thread1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1").start();
// 睡眠3s,保证线程thread1创建对象完成
Thread.sleep(3000);
System.out.println(">>>>>>>>> 打印thread1,list中第20个对象的对象头:");
System.out.println((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if (i >= 15 && i <= 21 || i >= 38) {
System.out.println(">>>>>>>>>> thread2-第" + (i + 1) + "次加锁执行中" + ClassLayout.parseInstance(obj).toPrintable());
}
}
if (i == 17 || i == 19) {
System.out.println(">>>>>>>>>> thread2-第" + (i + 1) + "次释放锁" + ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
Thread.sleep(3000);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock = list.get(i);
if (i >= 17 && i <= 21 || i >= 35 && i <= 41) {
System.out.println(">>>>>>>>>> thread3-第" + (i + 1) + "次准备加锁" + ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock) {
if (i >= 17 && i <= 21 || i >= 35 && i <= 41) {
System.out.println(">>>>>>>>>> thread3-第" + (i + 1) + "次加锁执行中" + ClassLayout.parseInstance(lock).toPrintable());
}
}
}
}, "thread3").start();
Thread.sleep(3000);
System.out.println(">>>>>>>>>> 查看创建的对象:");
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
LockSupport.park();
}
}
测试结果:
thread3:
1-18 从无锁状态直接获取轻量级锁(thread2释放锁之后变为无锁状态)
19-40 偏向锁撤销,升级为轻量级锁(thread2释放锁之后为偏向锁状态)
41-50 达到偏向锁撤销的阈值40,批量撤销偏向锁,升级为轻量级锁(thread1释放锁之后为偏向锁状态)
新创建的对象: 无锁状态
总结
- 批量重偏向和批量撤销是针对类的优化,和对象无关。
- 偏向锁重偏向一次之后不可再次重偏向。
- 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,若当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),此时当前线程就可以避免阻塞。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋是自适应的,若对象刚刚的一次自旋操作成功过,则判定此次自旋成功的可能性偏高,便会多自旋几次;反之,则少自旋甚至不自旋。
- Java 7 之后不能控制是否开启自旋功能
注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
若 JVM 检测到有一连串零碎的操作都是对同一对象的加锁,则会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
上述代码每次调用 buffer.append 方法都需要加锁和解锁,若 JVM 检测到有一连串的对同一个对象加锁和解锁的操作,则会将其合并成一次范围更大的加锁和解锁操作,即在第一次 append 方法时进行加锁,最后一次 append 方法结束后进行解锁。
锁消除
锁消除即删除不必要的加锁操作。
JVM 在 JIT 编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
public class LockEliminationTest {
/**
* 锁消除
* ‐XX:+EliminateLocks 开启锁消除(jdk8默认开启)
* -XX:-EliminateLocks 关闭锁消除
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer buffer = new StringBuffer();
buffer.append(str1).append(str2);
}
public static void main(String[] args) {
LockEliminationTest test = new LockEliminationTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
test.append("aaa", "bbb");
}
long end = System.currentTimeMillis();
System.out.println(">>>>>>>>>> 执行时间:" + (end - start) + " ms");
}
}
StringBuffer 的 append 是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。
测试结果: 关闭锁消除执行时间3784 ms 开启锁消除执行时间:1455 ms
逃逸分析(Escape Analysis)
一种可有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot 编译器能分析出一个新的对象的引用的使用范围,从而决定是否要将此对象分配到堆上。逃逸分析的基本行为即分析对象动态作用域。
方法逃逸(对象逃出当前方法)
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他作用域里。
线程逃逸(对象逃出当前线程)
此对象甚至可能被其它线程访问到,如赋值给类变量或可以在其它线程中访问的实例变量。
使用逃逸分析,编译器可对代码进行优化:
-
同步省略或锁消除(Synchronization Elimination)
若一个对象被发现只能从一个线程被访问到,则对于此对象的操作可以不考虑同步。
-
将堆分配转化为栈分配(Stack Allocation)
若一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的,而不是堆分配的。
-
分离对象或标量替换(Scalar Replacemen)
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
JDK 6 才开始引入该技术,JDK 7 开始默认开启逃逸分析。
在 Java 代码运行时,可通过JVM参数指定是否开启逃逸分析:
‐XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
‐XX:‐DoEscapeAnalysis //表示关闭逃逸分析。
‐XX:+EliminateAllocations //开启标量替换(默认打开)
‐XX:+EliminateLocks //开启锁消除(jdk1.8默认开启)
测试:
/**
* 进行两种测试
* 关闭逃逸分析,同时调大堆空间,避免堆内 GC 发生,若有 GC 信息则会打印出来
* VM 运行参数:
* -Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
* 开启逃逸分析,JDK8 默认开启
* VM 运行参数:
* -Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
* 执行 main 方法后
* jps 查看进程
* jmap -histo 进程 ID
*/
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println(">>>>>>>>>> 执行时间:" + (end - start) + " ms");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* JIT 编译时会对代码进行逃逸分析
* 并不是所有对象都存放在堆区,有的一部分存放在线程栈空间
* Point 没有逃逸
*
* @return
*/
private static String alloc() {
Point point = new Point();
return point.toString();
}
/**
* 同步省略(锁消除)JIT 编译阶段优化,JIT 经过逃逸分析后发现无线程安全问题,则会消除锁
*
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer buffer = new StringBuffer();
buffer.append(str1).append(str2);
}
/**
* 标量替换
*/
private static void test() {
Point point = new Point(1, 2);
System.out.println(">>>>>>>>>> point.x=" + point.getX() + ";point.y=" + point.getY());
// point在方法内部,没有逃逸出方法
// 此时getter时,x和y不会在堆内另外分配内存存放
// 即进行标量替换,相当于直接替换成:
// int x = 1;
// int y = 2;
// System.out.println(">>>>>>>>>> point.x=" + x + ";point.y=" + y);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Point {
private int x;
private int y;
}
测试结果:开启逃逸分析,部分对象会在栈上分配
七、synchronized锁对象升级过程
八、synchronized轻量级锁加锁过程
- 偏向锁等待安全点进行撤销后才能转为轻量级锁
- 判断 mark 是否为无锁状态:mark 的偏向锁标志位为 0,锁标志位为01
- 若为无锁,则将 Mark Word 拷贝到锁记录(Lock Record)里
- 通过 CAS 尝试将 Mark Word 更新为指向锁记录的指针,更新成功则表示竞争到锁,直接返回
- 若更新失败,则进行膨胀获取 monitor 对象,升级为重量级锁,轻量级锁不存在自旋
- 若 mark 处于加锁状态,则先判断是否为轻量级锁状态(01),再判断是否为重入操作(ptr 指针指向当前线程的栈帧),若同时满足则不需要竞争锁,直接入栈一个 null 的锁记录到栈帧,再将 Mark Word 更新为指向新入栈的这个锁记录的指针
- 最终释放锁时,将最底下的锁记录的 Mark Word 拷贝回锁对象,变回无锁状态
- 若没有同时满足 mark 加锁状态下的两个判断条件,则会膨胀为重量级锁,在锁膨胀的过程中,会返回一个 ObjectMonitor 对象,调用其 enter 方法,重量级锁的锁机制就是发生在 enter 方法中
膨胀过程:
过程为一个for循环(自旋),为了处理多线程同时调用的情况。
- 判断是否为重量级锁状态,即 Mark Word 的锁标识位是否为 10,若是,则说明已经是重量级锁状态,直接返回
- 若不是重量级锁状态,则判断是否为膨胀中状态(inflating),若是,则进行自旋
- 若不是膨胀中状态,则判断是否为轻量级锁状态,即锁标识位是否为 00,若是,则开始膨胀,创建 ObjectMonitor 对象,并初始化,通过 CAS 设置状态为膨胀中,若 CAS 成功,则升级为重量级锁,若失败则释放 monitor 重试(自旋)
- 若不是轻量级锁状态,说明是无锁状态,创建 ObjectMonitor 对象,并初始化,通过 CAS 设置状态为重量级锁,若 CAS 成功,则升级为重量级锁,若失败则释放 monitor 重试(自旋)
轻量级锁膨胀流程:
- 调用 omAlloc 分配一个 ObjectMonitor 对象,在 omAlloc 方法中会先从线程私有的 monitor 集合 omFreeList 中分配对象,若 omFreeList 中已无 monitor 对象,则从 JVM 全局的 gFreeList 中分配一批 monitor 到 omFreeList 中
- 初始化 monitor 对象
- 将状态设置为膨胀中(INFLATING)状态
- 设置 monitor 的 header 字段为 displaced word,owner 字段为 Lock Record,obj 字段为锁对象
- 设置锁对象头的 Mark Word 为重量级锁,指向第 1 步分配的 monitor 对象
无锁状态下的膨胀流程:
- 调用 omAlloc 分配一个 ObjectMonitor 对象
- 初始化 monitor 对象
- 设置 monitor 的 header 字段为 mark word,owner 字段为 null,obj 字段为锁对象
- 设置锁对象头的 Mark Word 为重量级锁状态,指向第 1 步分配的 monitor 对象
九、synchronized重量级锁加锁过程
- ObjectMonitor 对象调用其 enter 方法
- 通过 CAS 尝试获取锁,即将 monitor 对象的 _owner 指针指向当前线程,若 CAS 成功则直接返回,获取锁
- 若 CAS 失败,则判断是否是当前线程获取锁,若是,则说明是重入锁,可重入次数加1 ,返回,获取锁
- 若不是重入锁,则判断当前线程是否是之前持有的轻量级锁的线程,即由轻量级锁膨胀且第一次调用 enter 方法,指向锁记录的指针,若是,则将重入计数置为 1,设置 owner 字段为当前线程(owner 之前是指向锁记录的指针)
- 若不是,则在调用系统同步操作之前,先尝试自旋获取锁,目的是减少执行操作系统同步操作带来的开销,即自适应自旋(TrySpin),自旋过程通过 CAS 尝试获取锁,若 CAS 成功则获取锁,若失败则继续自旋,直到终止自旋。
终止自旋的场景:①成功获取锁;②达到自旋次数;③自旋过程中会判断是否进入安全点,若进入则终止自旋(每自旋 256 次便检查一次)
若自旋结束,获取锁失败,线程不会直接被挂起,而是多次再尝试获取锁,即循环调用 enter 方法,直到得到两种结果:①获取锁成功;②竞争失败,线程被挂起,等待唤醒。
- 尝试获取锁,调用 TryLock方法,通过 CAS 尝试获取锁,若 CAS 成功则获取锁
- 若失败则返回 -1,再次使用自旋,尝试获取锁,调用自适应自旋(TrySpin),通过 CAS 尝试获取锁,若 CAS 成功则获取锁
- 若失败则准备在 _cxq 上排队,将当前线程封装到 node 节点中,类型为 ObjectWaiter,设置状态为 TS_CXQ,将 node 节点插入到 _cxq 队列的头部(头插法),期间也会重试重新获取锁。
即自旋入队,并发场景入队操作 CAS 的引用:通过 CAS 修改 _cxq 指向 node,将 head 节点指向当前节点,若 CAS 成功,则入队成功则跳出自旋;若失败,则再次尝试获取锁,调用 TryLock 方法,若获取锁成功则返回,若失败则再次尝试入队
入队成功后,进入另一个循环去获取锁,若获取锁失败则调用 park 方法,挂起当前线程,若成功则跳出循环
- 挂起前再次尝试获取锁,调用 TryLock 方法,若获取锁成功,则跳出循环
- 若失败,则调用 park 方法挂起当前线程
此处为重量级锁中最大的开销,系统调用会设计到用户态和内核态的切换(在 Linux 系统中会调用 pthread_cond_wait 或 pthread_cond_timewait 挂起线程)
- 当前线程被唤醒(被唤醒的线程不一定在 _cxq 队列,也可能在 EntryList 队列中),线程被唤醒的第一时间就会尝试获取锁,调用 TryLock 方法,若获取锁成功则跳出循环
- 若失败,则通过自旋尝试获取锁,调用自适应自旋(TrySpin),通过 CAS 尝试获取锁,若 CAS 成功则获取锁
返回
- 若失败,则在最后调用内存屏障(fence)保证可见性
- 上方循环中只有获取锁成功才能跳出循环,接着将当前线程的 node 从 cxq 或 EntryList 中移除(出队)
线程唤醒并成功获取锁之后,会从 cxq 或 EntryList 队列中移除对应的 node
重量级锁解锁(线程唤醒)
- ObjectMonitor 对象调用其 exit 方法
- 判断字段 _owner 是否为当前线程,若是,则直接下一步;
若不是,则判断是否有其他线程占有该锁,若是,则直接返回,
若不是,则说明当前线程是之前持有轻量级锁的线程,
若 owner 位于当前线程调用的栈帧,则说明该锁是轻量级锁膨胀而来,修改 owner 属性,进入下一步 - 判断是否为重入锁(重入计数器不为 0),若是,则减 1 后返回
- 若不是,则将 _owner 属性值值为 null,释放锁
非公平锁优化:若某个线程正在自旋抢占该说,则会抢占成功,此策略优先保证通过自旋抢占锁的线程获取锁,而其他处于等待队列中的线程则靠后。
- 加入一个写屏障(storeload),让修改立即生效
- 根据 QMode 的不同会有不同的唤醒策略,默认为0
a. QMode = 2,且 cxq 非空:取 cxq 队列队首的 OBjectWaiter 对象,调用 ExitEpilog 方法,唤醒 ObjectWaiter 对象的线程,然后立即返回
b. QMode = 3,且 cxq 非空:将 cxq 队列插入到 EntryList 的尾部
c. QMode = 4,且 cxq 非空:将 cxq 队列插入到 EntryList 的头部
d. 只有 QMode = 2 时,会提前返回,等于 0、3、4 时,会继续往下执行
ⅰ. 若 EntryList 的首元素非空,则取出来调用 ExitEpilog 方法,唤醒 ObjectWaiter 对象的线程,然后立即返回
ⅱ. 若 EtntryList 的首元素为空,则将 cxq 的所有元素放入到 EntryList 中(若QMode = 1 则会反转顺序),再从 EntryList 中取出队首元素执行 ExitEpilog 方法,唤醒 ObjectWaiter 对象的线程,然后立即返回
ExitEpilog 方法会通过 unpark 唤醒 cxq 或 EntryList 队首元素中对应的线程,将 _onwer 属性值置为 null,释放锁,调用内存屏障(fence),唤醒目标线程(unpark),此时会发生系统调用,涉及到用户态和内核态直接的切换,Linux 系统会调用 pthread_cond_signal 唤醒挂起的线程,被唤醒的线程可能是 cxq 或 EntryList 队首元素对应的线程
在获取锁时,是将当前线程插入到 cxq 的头部;
释放锁时,默认策略为:若 EntryList 为空,则将 cxq 中的元素按原有顺序插入到 EntryList 中,并唤醒第一个线程。
简单来说:EntryList 为空时,后来的线程先获取锁;EntryList 不为空时,则直接从 EntryList 中唤醒线程。