1. 特性

synchronized 是用了 JMM8 大操作的 lock 和 unlock 操作,对主内存中的变量进行了独占操作。

并发编程 3 大特性是可见性,原子性,有序性。 而 synchronized 能同时保证。而 volatile 只能保证可见性,有序性,不能保证原子性

1.1. 原子性

原子性指的是在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

1.2. 可见性

可见性是指一个线程对共享变量进行了修改,另一个线程可以立即读取得到修改后的最新值。

1.3. 有序性

有序性是指程序中代码的执行顺序,Java 在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

例如,instance = new Singleton() 实例化对象的语句分为三步:

  • 1、分配对象的内存空间;
  • 2、初始化对象;
  • 3、设置实例对象指向刚分配的内存地址;

上述第二步操作需要依赖第一步,但是第三步操作不需要依赖第二步,所以执行顺序可能为:1->2->3、

1->3->2,当执行顺序为 1->3->2 时,可能实例对象还没正确初始化,我们直接拿到使用的时候可能会报错。

1.4. 可重入特性

synchronized 和 ReentrantLock 都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

1.5. 原子性比较

image.png

2. 深入剖析

2.1. 应用层面

8 锁问题:并发基础-9、Java各种锁

竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。为了避免临界区的竞态条件发生,有多种手段可以达到目的:

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:CAS

synchronized 同步块是 Java 提供的一种原子性 内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁,目的就是保证多个线程在进入 synchronized 代码段或者方法时,将并行变串行。

  1. synchronized 修饰的实例方法,多线程并发访问时,只能有一个线程进入,获得对象内置锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法。
  2. synchronized 修饰的静态方法,多线程并发访问时,只能有一个线程进入,获得类锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法。
  3. synchronized 修饰的代码块,多线程并发访问时,只能有一个线程进入,根据括号中的对象或者是类,获得相应的对象内置锁或者是类锁
  4. 每个类都有一个类锁,类的每个对象也有一个内置锁,它们是互不干扰的,也就是说一个线程可以同时获得类锁和该类实例化对象的内置锁,当线程访问非 synchronzied 修饰的方法时,并不需要获得锁,因此不会产生阻塞。

2.1.1. 三种锁类型

本质上都是依赖对象来锁

  • this 锁:当前实例锁
  • Class 锁:类对象锁
  • Object 锁:对象实例锁

2.1.2. 三种应用方式

  • 修饰实例成员方法:使用 this 锁,线程想要执行被 Synchronized 关键字修饰的成员实例方法必须先获取当前实例对象的锁资源;
  • 修饰静态成员方法:使用 class 锁,线程想要执行被 Synchronized 关键字修饰的静态方法必须先获取当前类对象的锁资源;
  • 修饰代码块:使用 Object 锁,使用给定的对象实现锁功能,线程想要执行被 Synchronized 关键字修饰的代码块必须先获取当前给定对象的锁资源;

链接:https://www.jianshu.com/p/884eb51266e4

2.2. 字节码层面

2.2.1. monitorenter、monitorexit

2.2.1.1. 修饰代码块

1
2
3
4
5
6
7
8
9
10
11
12
public class SynchronizedTest {  

public void doSth(){
synchronized (SynchronizedTest.class){
System.out.println("test Synchronized" );
}
}

// public synchronized void doSth(){
// System.out.println("test Synchronized method" );
// }
}

javap -c SynchronizedTest.class

每个对象有一个监视器锁 monitor,当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:

1.如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者

2.如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1

3.如果其他线程已经占有该 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权

执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。

指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出 Synchronized 的实现原理,Synchronized 的语义底层是通过一个 monitor 的对象来完成,其实 wait/notify 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因

2.2.1.2. 2 个 monitorexit

从上面的中文注释处可以看到,对于 synchronized 关键字而言,javac 在编译时,会生成对应的 monitorentermonitorexit 指令分别对应 synchronized 同步块的进入和退出,有两个 monitorexit 指令的原因是:为了保证抛异常的情况下也能释放锁,所以 javac 为同步代码块添加了一个隐式的 try-finally,在 finally 中会调用 monitorexit 命令释放锁。

2.2.2. ACC_SYNCHRONIZED

2.2.2.1. 修饰方法

1
2
3
4
5
6
7
8
9
10
11
12
public class SynchronizedTest {  

// public void doSth(){
// synchronized (SynchronizedTest.class){
// System.out.println("test Synchronized" );
// }
// }

public synchronized void doSth(){
System.out.println("test Synchronized method" );
}
}

javap -v SynchronizedTest.class

方法级别的同步是隐式的,作为方法调用的一部分。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。

当调用一个设置了 ACC_SYNCHRONIZED 标志的方法,执行线程需要先获得 monitor 锁,然后开始执行方法,方法执行之后再释放 monitor 锁,当方法不管是正常 return 还是抛出异常都会释放对应的 monitor 锁。

在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。

如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

2.3. JDK 源码层面

2.3.1. ObjectMonitor

JVM 的 monitorenter、monitorexit 这种机制底层是什么?
ObjectMonitor 是 JVM 中的对于操作系统 管程 的实现。

管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。

引入管程的原因
信号量机制的缺点:进程自备同步操作,P(S) 和 V(S) 操作大量分散在各个进程中,不易管理,易发生死锁。
管程特点:管程封装了同步操作,对进程隐蔽了同步细节,简化了同步功能的调用界面。

简单地说管程就是一个概念,任何语言都可以实现。目的就是为了简化同步调用的过程。

2.3.1.1. ObjectMonitor 对象

使用 monitor 机制的目的主要是为了互斥进入临界区,为了做到能够阻塞无法进入临界区的进程/线程,还需要一个 monitor object 来协助,这个 monitor object 内部会有相应的数据结构,例如列表,来保存被阻塞的线程;同时由于 monitor 机制本质上是基于 mutex 这种基本原语的,所以 monitor object 还必须维护一个基于 mutex 的锁。 此外,为了在适当的时候能够阻塞和唤醒 进程/线程,还需要引入一个条件变量,这个条件变量用来决定什么时候是“适当的时候”,这个条件可以来自程序代码的逻辑,也可以是在 monitor object 的内部,总而言之,程序员对条件变量的定义有很大的自主性。不过,由于 monitor object 内部采用了数据结构来保存被阻塞的队列,因此它也必须对外提供两个 API 来让线程进入阻塞状态以及之后被唤醒,分别是 wait 和 notify。管程机制中,monitor object 充当着维护 mutex 以及定义 wait/signal API 来管理线程的阻塞和唤醒的角色。

JVM 中的同步是基于进入和退出 monitor object 实现的,每个实例都会有个monitor object ,可以和对象一起创建,销毁。

通常所说的对象的内置锁,是对象头 Mark Word 中的重量级锁指针指向的 monitor 对象,该对象是在 HotSpot 底层 C++ 语言编写的 (openjdk 里面看)

2.3.1.2. OM 创建时机

执行 monitorenter 指令时,线程会为锁对象关联一个 ObjectMonitor 对象

许多文章声称一个对象关联到一个 monitor,这个说法不够准确。如果对象已经是重量级锁了,对象头的确指向了一个 monitor。但对于正在膨胀的锁,会先从 线程私有monitor 集合 omFreeList 中分配对象。如果 omFreeList 中已经没有 monitor 对象,再从 JVM 全局gFreeList 中分配一批 monitoromFreeList 中。

2.3.1.3. 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ObjectMonitor() {
_header = NULL;
_count = 0; // 用来记录获取锁的线程数
_waiters = 0;
_recursions = 0; // 线程的重入次数
_object = NULL;
_owner = NULL; // 标识拥有该monitor的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
// 等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁进入时的单向链表
//(ContentionList):当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程插入到cxq队列的队首
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
// _owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

2.3.1.3.1. _count
2.3.1.3.2. _recursions

2.3.1.3.3. 3个队列

https://www.cnblogs.com/kundeg/archive/2018/02/06/8422557.html

简要流程

  • 想要获取 monitor 的线程,首先会进入 _EntryList 队列。
  • 当某个线程获取到对象的 monitor 后,进入 _Owner 区域,设置为当前线程,同时计数器 _count 加 1。此时该线程就持有 mutex 锁
  • 如果线程调用了 wait() 方法,则会进入 _WaitSet 队列。它就会释放持有的 mutex 锁,即将 _owner 赋值为 null,_count自减1,进入_WaitSet队列阻塞等待
  • 如果其他线程调用 notify() / notifyAll() ,会唤醒 _WaitSet 中的某个线程,从 WaitSet 移动到 cxq 或 EntryList 中去 (根据配置策略决定放到哪一个中),该线程再次尝试获取 monitor 锁,成功即进入 _Owner 区域。
  • 同步方法执行完毕了,线程退出临界区,会将 monitor 的 owner 设为 null,并释放 mutex 锁。
  • 新请求锁的线程将首先被加入到 ConetentionList 中,当某个拥有锁的线程(Owner 状态)调用 unlock 之后,如果发现 EntryList 为空则从 ContentionList 中移动线程到 EntryList,下面说明下 ContentionList 和 EntryList 的实现方式:

ContentionList 虚拟队列

ContentionList 并不是一个真正的 Queue,而只是一个虚拟队列,原因在于 ContentionList 是由 Node 及其 next 指针逻辑构成,并不存在一个 Queue 的数据结构。ContentionList 是一个先进先出(FIFO)的队列,每次新加入 Node 时都会在队头进行,通过 CAS 改变第一个节点的的指针为新增节点,同时设置新增节点的 next 指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个 Lock-Free 的队列。
因为只有 Owner 线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了 CAS 的 ABA 问题。

EntryList

EntryList 与 ContentionList 逻辑上同属等待队列,ContentionList 会被线程并发访问,为了降低对 ContentionList 队尾的争用,而建立 EntryList。Owner 线程在 unlock 时会从 ContentionList 中迁移线程到 EntryList,并会指定 EntryList 中的某个线程(一般为 Head)为 Ready(OnDeck)线程。Owner 线程并不是把锁传递给 OnDeck 线程,只是把竞争锁的权利交给 OnDeck,OnDeck 线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot 中把 OnDeck 的选择行为称之为“竞争切换”。
OnDeck 线程获得锁后即变为 owner 线程,无法获得锁则会依然留在 EntryList 中,考虑到公平性,在 EntryList 中的位置不发生变化(依然在队头)。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列;如果在某个时刻被 notify/notifyAll 唤醒,则再次转移到 EntryList。

WaitSet

通过 object 获得内置锁 (objectMonitor),通过内置锁将 Thread 封装成 OjectWaiter 对象,然后 addWaiter 将它插入以 _waitSet 为首结点的等待线程链表中去,最后释放锁。
通过 object 获得内置锁 (objectMonitor),调用内置锁的 notify 方法,通过 _waitset 结点移出等待链表中的首结点,将它置于 _EntrySet 中去,等待获取锁。

2.3.1.4. 工作机制

见下方重量级锁章节

2.3.1.5. 重量级锁的重量

  1. synchronzied 实现同步用到了对象的内置锁 (ObjectMonitor),而在 ObjectMonitor 的函数调用中会涉及到 Mutex lock 等特权指令,那么这个时候就存在操作系统用户态和核心态的转换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,这也是为什么早期的 synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 重量级锁
  2. 处于 ContentionListEntryListWaitSet 中的线程都处于阻塞状态,线程的阻塞或者唤醒都需要操作系统来帮忙,Linux 内核下采用 pthread_mutex_lock 系统调用实现的,进程需要从用户态切换到内核态的 pthread_mutex_lock 系统调用,是内核态为用户态进程提供的 Linux 内核态下互斥锁 (Mutex) 的访问机制,所以使用 pthread_mutex_lock 系统调用时,进程需要从用户态切换到内核态,而这种切换是需要消耗很多时间的,有可能比用户执行代码的时间还要长

[[../../../../cubox/006-ChromeCapture/20221111-httpsdocs.oracle.comcdE19253-01819-70516n919hpagindex.html]]

2.3.2. 对象与 monitor 关联

2.4. 内存屏障层面

synchronized 可见性是通过内存屏障实现的,按可见性划分,内存屏障分为:

  • Load 屏障:执行 refresh,从其他处理器的高速缓冲、主内存,加载数据到自己的高速缓存,保证数据是最新的;
  • Store 屏障:执行 flush 操作,自己处理器更新的变量的值,刷新到高速缓存、主内存去;

获取锁时,会清空当前线程工作内存中共享变量的副本值,重新从主内存中获取变量最新的值;
释放锁时,会将工作内存的值重新刷新回主内存;

1
2
3
4
5
6
7
int a = 0;
synchronize (this){ //monitorenter
// Load内存屏障
int b = a; // 读,通过load内存屏障,强制执行refresh,保证读到最新的
a = 10; // 写,释放锁时会通过Store,强制flush到高速缓存或主内存
} //monitorexit
//Store内存屏障

synchronized 的有序性是依靠内存屏障实现的。

按照有序性,内存屏障可分为:

  • Acquire 屏障:load 屏障之后,加 Acquire 屏障。它会禁止同步代码块内的读操作,和外面的读写操作发生指令重排;
  • Release 屏障:禁止写操作,和外面的读写操作发生指令重排;

在 monitorenter 指令和 Load 屏障之后,会加一个 Acquire 屏障,这个屏障的作用是禁止同步代码块里面的读操作和外面的读写操作之间发生指令重排,在 monitorexit 指令前加一个 Release 屏障,也是禁止同步代码块里面的写操作和外面的读写操作之间发生重排序。如下:

1
2
3
4
5
6
7
8
9
int a = 0;
synchronize (this){ //monitorenter
// Load内存屏障
// Acquire屏障,禁止代码块内部的读,和外面的读写发生指令重排
int b = a;
a = 10; //注意:内部还是会发生指令重排
// Release屏障,禁止写,和外面的读写发生指令重排
} //monitorexit
//Store内存屏障

2.5. 汇编实现层面

3. 重量级锁

3.1. 锁膨胀

3.1.1. 触发条件

轻量级锁的释放也比较简单,就是将当前线程栈帧中锁记录空间中的 Mark Word 尝试 cas 替换到锁对象的对象头中,如果成功表示锁释放成功。否则,锁膨胀成重量级锁,实现重量级锁的释放锁逻辑。

如果当前 mark 处于加锁状态,且 mark 中的 ptr 指针指向当前线程的栈帧,则执行同步代码,否则说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁

当调用一个锁对象的 wait/notify/notifyall 方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

3.1.2. 执行过程

锁膨胀的过程实际上是获得一个 ObjectMonitor 对象监视器,而真正抢占锁的逻辑,在 ObjectMonitor::enter 方法里面。

inflate 其中是一个 for 循环,主要是为了处理多线程同时调用 inflate 的情况。然后会根据锁对象的状态进行不同的处理:

1.已经是重量级状态,说明膨胀已经完成,返回并继续执行 ObjectMonitor::enter 方法。
2.如果是轻量级锁则需要进行膨胀操作。
3.如果是膨胀中状态,则进行忙等待。
4.如果是无锁状态则需要进行膨胀操作

轻量级锁锁膨胀的过程
步骤 1、调用 omAlloc 获取一个可用的 ObjectMonitor 对象。在 omAlloc 方法中会先从 线程私有monitor 集合 omFreeList 中分配对象,如果 omFreeList 中已经没有 monitor 对象,则从 JVM 全局gFreeList 中分配一批 monitoromFreeList 中;

步骤 2、通过 CAS 尝试将 Mark Word 设置为 markOopDesc:INFLATING,标识当前锁正在膨胀中。如果 CAS 失败,说明同一时刻其它线程已经将 Mark Word 设置为 markOopDesc:INFLATING,当前线程进行自旋等待膨胀完成。

步骤 3、如果 CAS 成功,设置 monitor 的各个字段:设置 monitor 的 header 字段为 displaced mark word,owner 字段为 Lock Record,obj 字段为锁对象等;

📢 设置 ObjectMonitor 的 _owner 为拥有对象轻量级锁的线程的 Lock Record,而不是当前正在 inflate 的线程的 Lock Record,用于重量级锁竞争的判断。根本原因是因为锁对象的 Mark Word 里轻量级锁信息变成了重量级锁的信息,如果这个 CAS 操作是其他线程 (比如 B) 完成的,那么 A 是不知情的,而且 B 在获取轻量级锁时也 Blocked 了,所以干脆让 A 去处理轻量级锁膨胀后的逻辑。

步骤 4、设置锁对象头的 mark word 为重量级锁状态,指向第一步分配的 monitor 对象;

3.2. monitor 竞争

https://www.cnblogs.com/father-of-little-pig/p/16314318.html
https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/

当锁膨胀 inflate 执行完并返回对应的 ObjectMonitor 时,并不表示该线程竞争到了锁,真正的锁竞争发生在 ObjectMonitor::enter 方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void ATTR ObjectMonitor::enter(TRAPS) {

Thread * const Self = THREAD ;
void * cur ;
// 步骤1
// owner为null,如果能CAS设置成功,则当前线程直接获得锁
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
...
return ;
}
// 如果是重入的情况
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 步骤2
// 如果当前线程是之前持有轻量级锁的线程
// 上节轻量级锁膨胀将owner指向之前Lock Record的指针
// 这里利用owner判断是否第一次进入。
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
// 重入计数重置为1
_recursions = 1 ;
// 设置owner字段为当前线程
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
...
// 步骤3
// 在调用系统的同步操作之前,先尝试自旋获得锁
if (Knob_SpinEarly && TrySpin (Self) > 0) {
...
//自旋的过程中获得了锁,则直接返回
Self->_Stalled = 0 ;
return ;
}
...
{
...
// 步骤4
for (;;) {
jt->set_suspend_equivalent();
// 在该方法中调用系统同步操作
EnterI (THREAD) ;
...
}
Self->set_current_pending_monitor(NULL);
}
...
}


步骤 1Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; 通过 CAS 尝试将 owner 设置为当前线程。如果当前是无锁、返回 null,说明获取锁成功,简单操作后返回。如果是重入,则 _recursions ++

步骤 2、当前线程是之前持有轻量级锁的线程,则为首次进入,设置 recursions 为 1,owner 为当前线程,该线程成功获得锁并返回。

步骤 3、在调用系统的同步操作之前,先通过 TrySpin 方法 自旋尝试 获得锁,尽可能减少同步操作带来的开销。

步骤 4、获取锁失败,调用 EnterI 方法,在该方法中调用系统同步操作。

这里注意,轻量级锁膨胀成功时,会把 owner 字段设置为膨胀之前轻量级锁持有线程的栈中的 Lock Record 的指针,并在竞争时判断,具体是用在了步骤 2 的判断里。这么做的原因是,假设当前线程 A 持有锁对象的锁,线程 B 进入同步代码块,并把锁对象升级为重量级锁。但此时,线程 A 可能还在执行,并无法感知其持有锁对象的变化。因此,需要线程 B 在执行 ObjectMonitor::enter 时,将自己放入到阻塞等列等待。并需要线程 A 第二次进入、或者退出的时候对 monitor 进行一些操作,以此保证代码块的同步。

3.3. monitor 等待

https://www.cnblogs.com/father-of-little-pig/p/16314318.html
https://www.cnblogs.com/kundeg/archive/2018/02/06/8422557.html

ObjectMonitor 竞争失败的线程,通过自旋执行 ObjectMonitor::EnterI 方法等待锁的释放,EnterI 方法的部分逻辑实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 省略部分代码
void ATTR ObjectMonitor::EnterI (TRAPS) {
Thread * Self = THREAD ;
assert (Self->is_Java_thread(), "invariant") ;
assert (((JavaThread *) Self)->thread_state() == _thread_blocked , "invariant") ;

// Try lock 尝试获取锁
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
// 如果获取成功则退出,避免 park unpark 系统调度的开销
return ;
}

// 自旋获取锁
if (TrySpin(Self) > 0) {
assert (_owner == Self, "invariant");
assert (_succ != Self, "invariant");
assert (_Responsible != Self, "invariant");
return;
}

// 当前线程被封装成 ObjectWaiter 对象 node, 状态设置成 ObjectWaiter::TS_CXQ
ObjectWaiter node(Self) ;
Self->_ParkEvent->reset() ;
node._prev = (ObjectWaiter *) 0xBAD ;
node.TState = ObjectWaiter::TS_CXQ ;

// 通过 CAS 把 node 节点 push 到_cxq 列表中
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

// 再次 tryLock
// CAS失败的话 再尝试获得锁,这样也可以降低插入到_cxq队列的频率
if (TryLock (Self) > 0) {
assert (_succ != Self , "invariant") ;
assert (_owner == Self , "invariant") ;
assert (_Responsible != Self , "invariant") ;
return ;
}
}

for (;;) {
// 本段代码的主要思想和 AQS 中相似可以类比来看
// 再次尝试
if (TryLock (Self) > 0) break ;
assert (_owner != Self, "invariant") ;

if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}

// 满足条件则 park self
if (_Responsible == Self || (SyncFlags & 1)) {
TEVENT (Inflated enter - park TIMED) ;
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
// Increase the RecheckInterval, but clamp the value.
RecheckInterval *= 8 ;
if (RecheckInterval > 1000) RecheckInterval = 1000 ;
} else {
TEVENT (Inflated enter - park UNTIMED) ;
// 通过 park 将当前线程挂起,等待被唤醒
Self->_ParkEvent->park() ;
}

if (TryLock(Self) > 0) break ;
// 再次尝试自旋
if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break;
}
return ;
}

EnterI 大致原理:一个 ObjectMonitor 对象包括两个同步队列(_cxq_EntryList) ,以及一个等待队列 _WaitSet。cxq、EntryList 、WaitSet 都是由 ObjectWaiter 构成的链表结构。其中,_cxq 为单向链表,_EntryList 为双向链表。

  1. 当一个线程尝试获得重量级锁且没有竞争到时(又经过 TryLock、TrySpin 努力),该线程会被封装成一个 ObjectWaiter 的 node 对象 通过CAS 插入到 cxq 的队列的队首,状态设置成 ObjectWaiter::TS_CXQ。此过程中还会 tryLock,也能起到降低插入到 _cxq 队列的频率的作用。
  2. 在将 CXQ 挂起的 for 循环中,node 对象还 通过自旋尝试获取锁,如果在指定的阈值范围内没有获得锁,则通过 park 将当前线程挂起,进入BLOCKED状态,等待被唤醒。
  3. 当线程释放锁时,会根据唤醒策略,从 cxq 或 EntryList 中挑选一个线程 unpark 唤醒。唤醒时会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
  4. 如果线程获得锁后调用 Object#wait 方法,则会将线程加入到 WaitSet 中,进入WAITING 或 TIMED_WAITING状态。
  5. 当被 Object#notify 唤醒后,会将线程从 WaitSet 移动到 cxq 或 EntryList 中去,进入BLOCKED状态。
  6. 需要注意的是,当调用一个锁对象的 waitnotify 方法时,若当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

线程状态温习

并发基础-7、Thread

3.4. monitor 释放

https://www.cnblogs.com/kundeg/archive/2018/02/06/8422557.html
https://juejin.cn/post/6977744259688939551#heading-10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
if (THREAD != _owner) {//如果当前锁对象中的_owner没有指向当前线程
//如果_owner指向的BasicLock在当前线程栈上,那么将_owner指向当前线程
if (THREAD->is_lock_owned((address) _owner)) {
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// NOTE: we need to handle unbalanced monitor enter/exit
// in native code by throwing an exception.
// TODO: Throw an IllegalMonitorStateException ?
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
//如果当前,线程重入锁的次数,不为0,那么就重新走ObjectMonitor::exit,直到重入锁次数为0为止
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
...//此处省略很多代码
for (;;) {
if (Knob_ExitPolicy == 0) {
OrderAccess::release_store(&_owner, (void*)NULL); //释放锁
OrderAccess::storeload(); // See if we need to wake a successor
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT(Inflated exit - simple egress);
return;
}
TEVENT(Inflated exit - complex egress);
//省略部分代码...
}
//省略部分代码...
ObjectWaiter * w = NULL;
int QMode = Knob_QMode;
//根据QMode的模式判断,
//如果QMode == 2则直接从_cxq挂起的线程中唤醒
if (QMode == 2 && _cxq != NULL) {
w = _cxq;
ExitEpilog(Self, w);
return;
}
//省略部分代码... 省略的代码为根据QMode的不同,不同的唤醒机制
}
}

当某个持有锁的线程执行完同步代码块时,会进行锁的释放。在 HotSpot 中,通过退出 monitor 的方式实现锁的释放,并通知被阻塞的线程,具体实现位于 ObjectMonitor::exit 方法中。

  • 判断当前锁对象中的 owner 没有指向当前线程,如果 owner 指向的 BasicLock 在当前线程栈上,那么将 _owner 指向当前线程。
  • 如果当前锁对象中的 _owner 指向当前线程,则判断当前线程重入锁的次数,如果不为 0,继续执行 ObjectMonitor::exit(),直到重入锁次数为 0 为止。
  • 释放当前锁,唤醒操作。根据不同的策略(由 QMode 指定),从 cxq 或 EntryList 中获取头节点,通过 ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由 unpark 完成。

根据 QMode 的不同 (默认为 0),有不同的处理方式:

QMode = 0:暂时什么都不做;
QMode = 2 且 cxq 非空:取 cxq 队列队首的 ObjectWaiter 对象,调用 ExitEpilog 方法,该方法会唤醒 ObjectWaiter 对象的线程,然后立即返回,后面的代码不会执行了;
QMode = 3 且 cxq 非空:把 cxq 队列插入到 EntryList 的尾部;
QMode = 4 且 cxq 非空:把 cxq 队列插入到 EntryList 的头部;

只有 QMode=2 的时候会提前返回,等于 0、3、4 的时继续执行:

1.如果 EntryList 的首元素非空,就取出来调用 ExitEpilog 方法,该方法会唤醒 ObjectWaiter 对象的线程,然后立即返回;
2.如果 EntryList 的首元素为空,就将 cxq 的所有元素放入到 EntryList 中,然后再从 EntryList 中取出来队首元素执行 ExitEpilog 方法,然后立即返回;
3.被唤醒的线程,继续竞争 monitor;

3.5. wait 和 notify

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230607-2118%%
❕ ^shroec

  • wait,notify 必须是持有当前对象锁 Monitor 的线程才能调用 (对象锁代指 ObjectMonitor/Monitor,锁对象代指 Object)
  • 上面有说到,当在 sychronized 中锁对象 Object 调用 wait 时会加入 waitSet 队列,WaitSet 的元素对象就是 ObjectWaiter
    调用对象锁的 wait() 方法时,线程会被封装成 ObjectWaiter,最后使用 park 方法挂起,而当对象锁使用 notify() 时
  • 如果 waitSet 为空,则直接返回
  • waitSet 不为空从 waitSet 获取一个 ObjectWaiter,然后根据不同的 Policy 加入到 EntryList 或通过 Atomic::cmpxchg_ptr 指令自旋操作加入 cxq 队列 或者直接 unpark 唤醒
  • Object 的 notifyAll 方法则对应 voidObjectMonitor::notifyAll(TRAPS),流程和 notify 类似。不过会通过 for 循环取出 WaitSet 的 ObjectWaiter 节点,再依次唤醒所有线程

3.6. 源码分析

[[../../../../cubox/006-ChromeCapture/20221113-从 Monitorenter 源码看 Synchronized 锁优化的过程 - 掘金]]
[[../../../../cubox/006-ChromeCapture/20221112-Java锁与线程的那些事]]
[[../../../../cubox/006-ChromeCapture/20221113-(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现 - 掘金]]

4. 锁优化

[[../../../../cubox/006-ChromeCapture/20221111-synchronized 实现原理 小米信息部技术团队]]

[[../../../../cubox/006-ChromeCapture/20221112-[博客大赛] 图文并茂!!讲解 JUC 重量级锁、轻量级锁、自旋、锁膨胀…._qq60751173d6bae 的技术博客 _51CTO 博客]]

[[../../../../cubox/006-ChromeCapture/20221112-Synchronized 偏向锁升级 - MaXianZhe - 博客园]]

https://www.bilibili.com/video/av70549061?p=180

事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 偏向锁轻量级锁:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

所以锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6 中默认是开启偏向锁和轻量级锁的,我们也可以通过 -XX:-UseBiasedLocking=false 来禁用偏向锁

image-20200131213915151
image-20200131214209117

4.1. 偏向锁

4.1.1. 优点作用 - 减少轻量级锁不必要的 CAS 线程 ID 的替换⭐️🔴

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS.

如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 记录该线程的 ID;当该线程再次请求锁时,无需做任何同步操作,即需要在获取锁的时候检查一下 Mark Word 的锁标记位 = 偏向锁 (01),并且 threadID = 该线程 ID 即可,因此,省去了锁申请的操作。

image-20200131213740074

4.1.2. 缺点问题

撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
撤销的场景很多:访问对象的 hashCode、调用 wait/notify
可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁

4.1.3. 延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。因为 JVM 启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。待启动完成后再延迟打开偏向锁。
可以使用 -XX:BiasedLockingStartupDelay=0 参数关闭延迟,让其在程序启动时立刻启动。

4.1.4. 匿名偏向

匿名偏向状态:锁对象 mark word 标志位为 101,且存储的 Thread ID 为空 (源码中按 null 进行判断的,JOL 打印是 54 位都是 0) 时的状态 (即锁对象为偏向锁,且没有线程偏向于这个锁对象)。

为什么上述偏向锁逻辑没有判断 无锁状态的锁对象(001)?

只有匿名偏向的对象才能进入偏向锁模式。偏向锁是延时初始化的,默认是 4000ms。初始化后会将所有加载的 Klass 的 prototype header 修改为匿名偏向样式。当创建一个对象时,会通过 Klass 的 prototype_header 来初始化该对象的对象头。简单的说,偏向锁初始化结束后,后续所有对象的对象头都为 匿名偏向 样式,在此之前创建的对象则为 无锁状态。而对于无锁状态的锁对象,如果有竞争,会直接进入到轻量级锁。这也是为什么 JVM 启动前 4 秒对象会直接进入到轻量级锁的原因。

4.1.5. 加锁流程⭐️🔴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
CASE(_monitorenter): {
// lockee 就是锁对象
oop lockee = STACK_OBJECT(-1);
// derefing's lockee ought to provoke implicit null check
CHECK_NULL(lockee);
// code 1:找到一个空闲的Lock Record
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
//entry不为null,代表还有空闲的Lock Record
if (entry != NULL) {
// code 2:将Lock Record的obj指针指向锁对象
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
// markoop即对象头的mark word
markOop mark = lockee->mark();
intptr_t hash = (intptr_t) markOopDesc::no_hash;
// code 3:如果锁对象的mark word的状态是偏向模式
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
// code 4:这里有几步操作,下文分析
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
// code 5:如果偏向的线程是自己且epoch等于class的epoch
if (anticipated_bias_locking_value == 0) {

}
// code 6:如果偏向模式关闭,则尝试撤销偏向锁
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {

}
// code 7:如果epoch不等于class中的epoch,则尝试重偏向
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 构造一个偏向当前线程的mark word

}
else {
// 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)
// code 8:下面构建一个匿名偏向的mark word,尝试用CAS指令替换掉锁对象的mark word
markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |(uintptr_t)markOopDesc::age_mask_in_place |epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
// debugging hint
DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
// CAS修改成功
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
// 如果修改失败说明存在多线程竞争,所以进入monitorenter方法
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
}

// 如果偏向线程不是当前线程或没有开启偏向模式等原因都会导致success==false
if (!success) {
// 轻量级锁的逻辑
//code 9: 构造一个无锁状态的Displaced Mark Word,并将Lock Record的lock指向它
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
//如果指定了-XX:+UseHeavyMonitors,则call_vm=true,代表禁用偏向锁和轻量级锁
bool call_vm = UseHeavyMonitors;
// 利用CAS将对象头的mark word替换为指向Lock Record的指针
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
// 判断是不是锁重入
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { //code 10: 如果是锁重入,则直接将Displaced Mark Word设置为null
entry->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
// lock record不够,重新执行
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}

偏向锁的获取一定是从 匿名偏向 状态开始,然后 偏向 到某一个线程。

  1. 从当前线程的栈中找到一个空闲的 Lock Record(即代码中的 BasicObjectLock,下文都用 Lock Record 代指),判断 Lock Record 是否空闲的依据是其 obj 字段是否为 null。注意这里是按内存地址从低往高找到最后一个可用的 Lock Record,换而言之,就是找到内存地址最高的可用 Lock Record。
  2. 获取到 Lock Record 后,首先要做的就是为其 obj 字段赋值。
  3. 判断锁对象的 mark word 是否是偏向模式,即低 3 位是否为 101

5. CAS 将偏向线程 id 改为当前线程 id,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑。
6. 若对象头 mark word 显示存储 thread id 为当前线程 id,表明这是一次锁重入操作,会向当前线程的栈帧中 添加一条 displaced mark word 为空的 lock record 记录(与轻量级锁记录重入记录原理相同)。
7. 若对象头 mark word 显示已存储其它 thread id,表明该锁对象已偏向其它线程,进入偏向锁撤销流程。等待 safepoint 检查偏向线程是否存活(遍历 jvm 线程栈检查),若偏向线程存活且还在执行同步代码块则进入升级轻量锁流程,否则将对象头 mark word 置为无锁 mark word,进入升级轻量级锁流程。

即不论偏向线程是否存活,只要该锁对象偏向过一个非当前线程的线程,在当前线程访问时一定会升级为轻量级锁。

CAS 获取偏向锁的过程中存在并发冲突||已偏向其他线程||epoch 值过期||class 偏向模式关闭,都会进入到 InterpreterRuntime::monitorenter 方法, 在该方法中会进行偏向锁撤销和升级。

https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/
http://northsea.top/?p=195#3-%E5%81%8F%E5%90%91%E9%94%81

4.1.6. 撤销流程

https://www.cnblogs.com/FraserYu/p/15743542.html

撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态
偏向锁的 撤销(revoke)是一个很特殊的操作,为了执行撤销操作,需要等待 全局安全点,此时所有的工作线程都停止了执行。偏向锁的撤销操作并不是将对象恢复到无锁可偏向的状态,而是在偏向锁的获取过程中,发现竞争时,直接将一个被偏向的对象 升级到 被加了 轻量级锁 的状态。

https://www.cnblogs.com/juniorMa/articles/13845491.html
https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/
https://juejin.cn/post/6977744259688939551#heading-4

  • 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁。JVM 维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活 [[性能调优专题-基础-1、Java-相关名词#线程集合]]。
  • 偏向的线程是否还在同步块中,如果不在了,则撤销偏向锁。我们回顾一下偏向锁的加锁流程:每次进入同步块(即执行 monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的 Lock Record,将其 obj 字段指向锁对象。每次解锁(即执行 monitorexit)的时候都会将最低的一个相关 Lock Record 移除掉。所以可以通过遍历线程栈中的 Lock Record 来判断线程是否还在同步块中。
  • 将偏向线程所有相关 Lock RecordDisplaced Mark Word 设置为 null,然后将最高位的 Lock RecordDisplaced Mark Word 设置为无锁状态,最高位的 Lock Record 也就是第一次获得锁时的 Lock Record(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的 Lock Record,这里不需要用 CAS 指令,因为是在 safepoint。 执行完后,就升级成了轻量级锁。原偏向线程的所有 Lock Record 都已经变成轻量级锁的状态。

  总结下上面原作者的话:

  1 如果原来的线程不存活了,锁对象变成无锁状态,方法 return。这样,ThreadB 就能进入 slow_enter 了。slow_enter 就是获取轻量级锁,获取不到才是锁膨胀。

  2 如果原线程存在,这是要构造一个无锁状态的 mark word,放到最开始的那个 Lock Record 里。这是因为此时要进行轻量级锁升级,轻量级锁释放就会把 Lock record 里的 mark word 写回对象头里。

    那么原线程直接获取该轻量级锁,同时 ThreadB 还是进入 slow_enter,参与到轻量级锁的竞争

4.1.7. 释放流程

锁释放是指线程执行完毕,退出同步代码块。偏向锁的释放并不是偏向锁的撤销,对象头还是偏向锁。most_recent->set_obj(NULL); 这就是释放偏向锁,其实啥也没干。还要把这条 LockRecord 删除掉 每次解锁(即执行 monitorexit)的时候都会将最低的一个相关 Lock Record 移除掉。

偏向锁的释放可参考 bytecodeInterpreter.cpp#1923,这里也不贴了。偏向锁的释放只要将对应 Lock Record 释放就好了,但这里的释放并不会将 mark word 里面的 thread ID 去掉,这样做是为了下一次更方便的加锁。而轻量级锁则需要将 Displaced Mark Word 替换到对象头的 mark word 中。如果 CAS 失败或者是重量级锁则进入到 InterpreterRuntime::monitorexit 方法中。

4.1.8. 锁重入

该线程 ID 是自己的,则表示可重入,直接可以获取 (此时在自己的线程栈中继续生成一条新的 Lock Record)

4.1.9. 其他撤销

https://www.cnblogs.com/father-of-little-pig/p/16314318.html
状态跟踪

4.1.9.1. 偏向锁撤销之调用对象 HashCode

%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230604-0833%%
❕ ^6jfu5x




4.1.9.1.1. 为什么调用 hashcode 会撤销偏向锁

[[20221112-Java GC详解 - 最全面的理解Java对象结构 - 对象指针 OOPs HeapDump性能社区#^jy0jkr]]

4.1.9.1.2. hashcode 存储位置

4.1.9.2. 偏向锁撤销之调用 wait/notify

偏向锁状态执行 obj.notify() 会升级为轻量级锁,调用 obj.wait(timeout) 会升级为重量级锁

4.1.10. 批量重偏向和批量撤销

[[../../../../cubox/006-ChromeCapture/偏向锁理论太抽象,实战了解下偏向锁如何发生以及如何升级【实战篇】 - 掘金]]

撤销偏向和重偏向都是 以类为单位批量 进行的
依赖三个阈值作出判断:

1
2
3
4
5
6
# 批量重偏向阈值 
-XX:BiasedLockingBulkRebiasThreshold=20
# 重置计数的延迟时间
-XX:BiasedLockingDecayTime=25000
# 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40

简单总结,对于 一个类,按默认参数来说:
单个偏向撤销的计数达到 20,就会进行批量重偏向。
距上次批量重偏向 25 秒内,计数达到 40,就会发生批量撤销。

每隔 (>=) 25 秒,会重置在 [20, 40) 内的计数为 0,这意味着可以发生多次批量重偏向。

注意:对于一个类来说,批量撤销只能发生一次,因为批量撤销后,该类禁用了可偏向属性,后面该类的对象都是不可偏向的,包括新创建的对象。

[[../../../../cubox/006-ChromeCapture/20221112-[博客大赛] 图文并茂!!讲解 JUC 重量级锁、轻量级锁、自旋、锁膨胀…._qq60751173d6bae 的技术博客 _51CTO 博客]]

4.1.11. 源码解析

[[../../../../cubox/006-ChromeCapture/20221112-死磕Synchronized底层实现–偏向锁-Java知音]]
[[../../../../cubox/006-ChromeCapture/20221112-Java锁与线程的那些事]]
[[../../../../cubox/006-ChromeCapture/20221112-Synchronized 偏向锁升级 - MaXianZhe - 博客园]]

4.2. 轻量级锁

4.2.1. 概念作用 - 多个线程交替执行⭐️🔴

轻量级锁是 JDK 6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用 monitor 的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。
引入轻量级锁的目的:在 多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

4.2.2. 加锁流程 - 轻量级锁加锁及锁膨胀⭐️🔴⭐️🔴

https://www.javazhiyin.com/24364.html
https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/
https://dandelioncloud.cn/article/details/1403089140002131970

HotSpot 中偏向锁的具体实现可参考 bytecodeInterpreter.cpp#1816

多个线程竞争偏向锁导致偏向锁升级为轻量级锁

  1. JVM 在当前线程的栈帧中创建 Lock Reocrd,将内部 obj (ptr_to_obj)指针指向锁对像。然后构造一个无锁状态的 Mark Word(相当于复制无锁状态的 Mark Word 到锁记录中),并将 lock record 中的 Displaced Mark Word 指向它。(Displaced Mark Word,供解锁时恢复锁对象对象头)
  2. 线程尝试使用 CAS 将对象头中的 Mark Word 中的 ptr_to_lock_record(指向栈中锁记录的指针) 替换为 上面第一步刚在栈中创建的 Lock Reocrd 的指针。如果成功则获得锁
  3. CAS 失败时判断检查已获取轻量级锁线程是否为自己(遍历栈帧查找其它 obj* 指向同个对象且 mark word 不为空的 lock record),如果是则代表当前为一次锁重入,就将当前 lock record 的 mark word (displaced mark word)置为 null(上面第一步设置的),即当前 lock record 即作为一个计数器使用,结束。
  4. 若 CAS 失败且当前线程栈帧中不存在其它指向该锁对象的 lock record,说明存在其它线程竞争,锁膨胀至重量级锁。

Mark Word 布局温习

对象创建-1、对象内存

4.2.3. 解锁流程

https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/
https://dandelioncloud.cn/article/details/1403089140002131970

轻量级锁释放的入口在 bytecodeInterpreter.cpp#1923

  1. 遍历线程栈帧,找多所有 obj* 指向该锁对象的 lock record。
  2. 如果 lock record 的 displaced mark word 为 null,代表这是一条重入所记录,将 obj 置为 null 后 continue。
  3. 如果 lock record 的 displaced mark word 不为 null,则利用 CAS 操作 将锁对象对象头的 mark word 恢复成 displaced mark word,这一步成功则说明解锁成功,失败则膨胀至重量锁。

轻量级锁释放时需要将 Displaced Mark Word 替换到对象头的 mark word 中。如果 CAS 失败或者是重量级锁则进入到 InterpreterRuntime::monitorexit 方法中。

4.2.4. 优点

正常交替流程

其性能提升的依据是对于绝大部分的锁在整个生命周期内都是不会存在竞争。在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

4.2.5. 缺点⭐️🔴

在有多线程竞争的情况下轻量级锁增加了额外开销。

4.2.6. 锁重入

轻量级锁在拷贝 mark word 到线程栈 Lock Record 中时,如果有重入锁,则在线程栈中继续压栈 Lock Record 记录,只不过 mark word 的值为空,等到解锁后,依次弹出,最终将 mard word 恢复到对象头中,如图所示

[[20221111-轻量级锁(锁重入的实现方式)-蒲公英云]]
[[Java锁与线程状态的那些事.pdf]]

4.2.7. 源码

https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/
[[轻量级锁加锁 & 解锁过程 gorden5566]]
[[轻量级锁(锁重入的实现方式)-蒲公英云]]

4.3. 自旋锁

线程的阻塞和唤醒需要 CPU 从用户态转为核心态,频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用 -XX:+UseSpinning 开开启,在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整;
如果通过参数 -XX:preBlockSpin 来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为 10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是 JDK1.6 引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁

JDK 1.6 引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

4.4. 锁消除

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM 检测到不可能存在共享数据竞争,这是 JVM 会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些 JDK 的内置 API 时,如 StringBuffer、Vector、HashTable 等,这个时候会存在隐形的加锁操作。比如 StringBuffer 的 append() 方法,Vector 的 add() 方法:

1
2
3
4
5
6
7
8
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}

System.out.println(vector);
}

在运行这段代码时,JVM 可以明显检测到变量 vector 没有逃逸出方法 vectorTest() 之外,所以 JVM 可以大胆地将 vector 内部的加锁操作消除。

4.5. 锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,LZ 也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector 每次 add 的时候都需要加锁操作,JVM 检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到 for 循环之外。

4.6. 应用场景

  • 偏向锁 : 偏向锁适合在只有一个线程访问锁的场景,在此种场景下,线程只需要执行一次 CAS 获取偏向锁,后续该线程可重入访问该锁时仅仅只需要简单的判断 Mark Word 的线程 ID 即可
  • 轻量级锁 : 轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争,此种场景下,线程每次获取锁只需要执行一次 CAS 即可
  • 重量级锁 : 重量级锁适合在多线程竞争环境下访问锁,执行临界区的时间比较长,由于竞争激烈,自旋后未获取到锁的线程将会被挂起进入等待队列,等待持有锁的线程释放锁后唤醒它.此种场景下,线程每次都需要进行多次 CAS 操作,操作失败将会被放入队列里等待唤醒.

5. 锁升级

image-20200129142133191

无锁是锁升级前的一个中间态,必须要恢复到无锁才能进行升级,因为需要有拷贝 mark word 的过程,并且修改指针。

思考1:重量级锁释放之后变为无锁,此时有新的线程来调用同步块,会获取什么锁?

通过实验可以得出,后面的线程会获得轻量级锁,相当于线程竞争不激烈,多个线程通过 CAS 就能轮流获取锁,并且释放。

思考2:为什么有轻量级锁还需要重量级锁?

因为轻量级锁时通过 CAS 自旋的方式获取锁,该方式消耗 CPU 资源的,如果锁的时间长,或者自旋线程多,CPU 会被大量消耗;而重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗 CPU 资源。

思考3:偏向锁是否一定比轻量级锁效率高吗?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,需要暂停线程,回到安全点,并检查线程是否活着,故撤销需要消耗性能,这时候直接使用轻量级锁。
JVM 启动过程,会有很多线程竞争,所以默认情况启动时不打开偏向锁,过一段儿时间再打开。

5.1. 锁记录 ( LockRecord)

线程栈的栈帧中,可以有多个 LockRecord

[[20221111-Synchronized 探秘 - 软件即世界]]

5.2. 锁对象 (ObjectRef)

作为锁的对象

5.3. 对象锁 (ObjectMonitor)

ObjectMonitor

5.4. 升级图示

[[../../../../cubox/006-ChromeCapture/20221112-[博客大赛] 图文并茂!!讲解 JUC 重量级锁、轻量级锁、自旋、锁膨胀…._qq60751173d6bae 的技术博客 _51CTO 博客]]

5.5. 锁升级原理 (流程)⭐️🔴⭐️🔴

Synchronized 在 jdk1.6 版本之前,是通过重量级锁的方式来实现线程之间锁的竞争。之所以称它为重量级锁,是因为它的底层底层依赖操作系统的 Mutex Lock 来实现互斥功能。 Mutex 是系统方法,由于权限隔离的关系,应用程序调用系统方法时需要切换到内核态来执行。这里涉及到用户态向内核态的切换,这个切换会带来性能的损耗。

在 jdk1.6 版本中,synchronized 增加了锁升级的机制,来平衡数据安全性和性能。简单来说,就是线程去访问 synchronized 同步代码块的时候,synchronized 根据线程竞争情况,会先尝试在不加重量级锁的情况下去保证线程安全性。所以引入了偏向锁和轻量级锁的机制。

偏向锁,就是直接把当前锁偏向于某个线程,简单来说就是通过 CAS 修改偏向锁标记,这种锁适合同一个线程多次去申请同一个锁资源并且没有其他线程竞争的场景。
轻量级锁也可以称为自旋锁,基于自适应自旋的机制,通过多次自旋重试去竞争锁。自旋锁优点在于它避免避免了用户态到内核态的切换带来的性能开销。

Synchronized 引入了锁升级的机制之后,如果有线程去竞争锁: 首先,synchronized 会尝试使用偏向锁的方式去竞争锁资源,如果能够竞争到偏向锁,表示加锁成功直接返回。

如果竞争锁失败,说明当前锁已经偏向了其他线程。需要将锁升级到轻量级锁,在轻量级锁状态下,竞争锁的线程根据自适应自旋次数去尝试抢占锁资源,如果在轻量级锁状态下还是没有竞争到锁,就只能升级到重量级锁
在重量级锁状态下,没有竞争到锁的线程就会被阻塞,线程状态是 Blocked。处于锁等待状态的线程需要等待获得锁的线程来触发唤醒。

image.png

总的来说,Synchronized 的锁升级的设计思想,在我看来本质上是一种性能和 安全性的平衡,也就是如何在不加锁的情况下能够保证线程安全性。 这种思想在编程领域比较常见,比如 Mysql 里面的 MVCC 使用版本链的方式来 解决多个并行事务的竞争问题。

6. 为什么要废弃偏向锁

[[../../../../cubox/006-ChromeCapture/20221110-你知道 Java 的偏向锁被废弃掉了吗?_杏仁技术站的博客-CSDN博客]]

在 Java15 后默认禁用偏向锁可能会导致一些 Java 应用的性能下降,所以 HotSpot 提供了显示开启偏向锁的命令

1
# 在 Java15 后,手动开启偏向锁在启动的时候会收到警告信息-XX:+UseBiasedLocking

https://developer.aliyun.com/article/916746#slide-8

7. 对比

8. 实战经验

8.1. 减少 synchronized 的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

1
2
3
synchronized (Demo01.class) {
System.out.println("aaa");
}

8.2. 降低 synchronized 锁的粒度

将一个锁拆分为多个锁提高并发度

8.3. 读写分离

读取时不加锁,写入和删除时加锁
ConcurrentHashMap,CopyOnWriteArrayList 和 ConyOnWriteSet

8.4. 常见错误

8.4.1. synchronized (new Object())

1
synchronized (new Object())

每次调用创建的是不同的锁,相当于无锁

8.4.2. synchronized (享元)

1
2
private Integer count;
synchronized (count)

String,Boolean 在实现了都用了享元模式,即值在一定范围内,对象是同一个。所以看似是用了不同的对象,其实用的是同一个对象。会导致一个锁被多个地方使用

8.4.3. 正确的加锁姿势

1
2
3
4
// 普通对象锁
private final Object lock = new Object();
// 静态对象锁
private static final Object lock = new Object();

9. 源码入口

https://github.com/farmerjohngit/myblog/issues/13
偏向锁入口,肯定是要在源码中找到对 monitorenter 指令解析的地方。在 HotSpot 的中有两处地方对 monitorenter 指令进行解析:一个是在 bytecodeInterpreter.cpp#1816 ,另一个是在 templateTable_x86_64.cpp#3667。其中,bytecodeInterpreter 是 JVM 中的字节码解释器, templateInterpreter 为模板解释器。HotSpot 对运行效率有着极其执着的追求,显然会倾向于用模板解释器来实现。R 大的 读书笔记 中有说明,HotSpot 中只用到了模板解释器,并没有用到字节码解释器。因此,本文认为 montorenter 的解析入口为 templateTable_x86_64.cpp#3667。但模板解释器 templateInterpreter 都是汇编代码,不易读,且实现逻辑与字节码解释器 bytecodeInterpreter 大体一致。因此本文的源码都以 bytecodeInterpreter 来说明,借此窥探 synchronized 的实现原理。

10. 面试题

10.1. wait 和 notify 这个为什么要在 synchronized 代码块中

在多线程里面,要实现多个线程之间的通信,除了管道流以外,只能通过共享变量的方法来实现,也就是线程 t1 修改共享变量 s,线程 t2 获取修改后的共享变量 s,从而完成数据通信。但是多线程本身具有并行执行的特性,也就是在同一时刻,多个线程可以同时执行。在这种情况下,线程 t2 在访问共享变量 s 之前,必须要知道线程 t1 已经修改过了共享变量 s,否则就需要等待。同时,线程 t1 修改过了共享变量 S 之后,还需要通知在等待中的线程 t2。所以要在这种特性下要去实现线程之间的通信,就必须要有一个竞争条件控制线程在什么条件下等待,什么条件下唤醒。

image.png

而 Synchronized 同步关键字就可以实现这样一个互斥条件,也就是在通过共享变量来实现多个线程通信的场景里面,参与通信的线程必须要竞争到这个共享变量的锁资源,才有资格对共享变量做修改,修改完成后就释放锁,那么其他的线程就可以再次来竞争同一个共享变量的锁来获取修改后的数据,从而完成线程之前的通信。
所以这也是为什么 wait/notify 需要放在 Synchronized 同步代码块中的原因,有了 Synchronized 同步锁,就可以实现对多个通信线程之间的互斥,实现条件等待和条件唤醒。
另外,为了避免 wait/notify 的错误使用,jdk 强制要求把 wait/notify 写在同步代码块里面,否则会抛出 IllegalMonitorStateException
最后,基于 wait/notify 的特性,非常适合实现生产者消费者的模型,比如说用 wait/notify 来实现连接池就绪前的等待与就绪后的唤醒。

wait 和 notify 的实现逻辑:并发基础-2、Synchronized

10.1.1. 管道流

https://www.cnblogs.com/qlqwjy/p/10118733.html
在 Java 语言中提供了各种各样的输入/输出流 Stream, 使我们能够方便地对数据进行操作,其中管道流 (pipeStream) 是一种特殊的流,用于在不同线程间直接传送数据。一个发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信,而无需借助于临时文件之类的动西。

  在 Java 的 JDK 中提供了 4 个类来使线程间可以通信:

  (1)PipedInputStream 和 PipedOutputStream

  (2)PipedReader 和 PipedWriter

11. 参考与感谢

11.1. 黑马

11.1.1. 视频

https://www.bilibili.com/video/av70549061?p=180

11.2. 其他

https://juejin.im/post/5c37377351882525ec200f9e
https://www.cnblogs.com/paddix/p/5367116.html
https://juejin.cn/post/6844903918653145102
[[../../../../cubox/006-ChromeCapture/20221109-Synchronized解析——如果你愿意一层一层剥开我的心 - 掘金]]
[[../../../../cubox/006-ChromeCapture/20221110-由Java 15废弃偏向锁,谈谈Java Synchronized 的锁机制 - Yano_nankai - 博客园]]

https://www.bilibili.com/video/BV168411e7wr/?spm_id_from=333.337.search-card.all.click&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
https://www.yuque.com/qieshuyuni/ws3p3g/ai3b4s

https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/

:[[../../../../cubox/006-ChromeCapture/20221111-(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现 - 掘金]]

https://juejin.cn/post/6995156191286394888

https://www.jianshu.com/p/46a874d52b71

https://www.cnblogs.com/father-of-little-pig/p/16314318.html

源码 https://github.com/openjdk/jdk/blob/jdk8-b120/hotspot/src/share/vm/runtime/objectMonitor.hpp
http://60.205.225.95/?p=49

11.3. 源码分析

https://juejin.cn/post/7104638789456232478#heading-12
https://xiaomi-info.github.io/2020/03/24/synchronized/
https://juejin.cn/post/6977744259688939551#heading-9
https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/

应用:[[../../../../cubox/006-ChromeCapture/(二)深入理解Java并发编程之Synchronized关键字实现原理剖析 - 简书]]