1、使用方式
1.1 修饰实例方法&方法块实例对象
/**
* 修饰实例方法
* 修饰实例对象的方法块
*
* 是同一把锁
*
* created by guanjian on 2020/11/11 14:18
*/
public class Synchronized01Test {
public synchronized void testIntanceMethod(){
//修饰实例方法,锁定的是this,即当前SynchronizedTest对象
try {
System.out.println("testIntanceMethod执行开始");
Thread.sleep(5000);
System.out.println("testIntanceMethod执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testStaticBlock(){
synchronized(this) {
//修饰方法块的this,锁定的是this,即当前SynchronizedTest对象
System.out.println("testStaticBlock执行开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testStaticBlock执行结束");
}
}
public static void main(String[] args) throws InterruptedException {
Synchronized01Test instance = new Synchronized01Test();
//由于是使用instance对象这同一把锁,因此先持有线程先执行,另一个线程需要等待执行完毕释放锁后方可继续执行
new Thread(()->{
System.out.println("线程-1 访问开始");
instance.testIntanceMethod();
System.out.println("线程-1 访问结束");
}).start();
new Thread(()->{
System.out.println("线程-2 访问开始");
instance.testStaticBlock();
System.out.println("线程-2 访问结束");
}).start();
}
}
1.2 修饰静态方法&方法块实例对象
/**
* 静态方法持有的锁目标是类
* 方法块实例是对象
*
* 彼此不冲突
*
* created by guanjian on 2020/11/11 14:18
*/
public class Synchronized02Test {
public static synchronized void testStaticMethod(){
//修饰静态方法,锁定的是Synchronized02Test.class
try {
System.out.println("testStaticMethod执行开始");
Thread.sleep(5000);
System.out.println("testStaticMethod执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testStaticBlock(){
synchronized(this) {
//修饰方法块的this,锁定的是this,即当前SynchronizedTest对象
System.out.println("testStaticBlock执行开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testStaticBlock执行结束");
}
}
public static void main(String[] args) throws InterruptedException {
Synchronized02Test instance = new Synchronized02Test();
//不是同一把锁,彼此不影响
new Thread(()->{
System.out.println("线程-1 访问开始");
Synchronized02Test.testStaticMethod();
System.out.println("线程-1 访问结束");
}).start();
new Thread(()->{
System.out.println("线程-2 访问开始");
instance.testStaticBlock();
System.out.println("线程-2 访问结束");
}).start();
}
}
1.3 修饰静态方法&方法块类
/**
* 静态方法持有的锁目标是类
* 方法块锁目标也是类
*
* 持有同一把锁
*
* created by guanjian on 2020/11/11 14:18
*/
public class Synchronized03Test {
public static synchronized void testIntanceMethod(){
//修饰静态方法,锁定的是Synchronized02Test.class
try {
System.out.println("testIntanceMethod执行开始");
Thread.sleep(5000);
System.out.println("testIntanceMethod执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testStaticBlock(){
synchronized(Synchronized03Test.class) {
//修饰方法块的Synchronized03Test类
System.out.println("testStaticBlock执行开始");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("testStaticBlock执行结束");
}
}
public static void main(String[] args) throws InterruptedException {
Synchronized03Test instance = new Synchronized03Test();
//不是同一把锁,彼此不影响
new Thread(()->{
System.out.println("线程-1 访问开始");
Synchronized03Test.testIntanceMethod();
System.out.println("线程-1 访问结束");
}).start();
new Thread(()->{
System.out.println("线程-2 访问开始");
instance.testStaticBlock();
System.out.println("线程-2 访问结束");
}).start();
}
}
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的 class 对象
- 同步方法块,锁是括号里面的对象
2、实现方式
2.1 同步方法
- 同步方法:synchronized 方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn 指令,在 VM 字节码层面并没有任何特别的指令来实现被synchronized 修饰的方法,而是在 Class 文件的方法表中将该方法的 access_flags 字段中的 synchronized 标志位置设置为 1,表示该方法是同步方法,并使用调用该方法的对象或该方法所属的 Class 在 JVM 的内部对象表示 Klass 作为锁对象。
2.2 同步代码块
- 同步代码块:monitorenter 指令插入到同步代码块的开始位置,monitorexit 指令插入到同步代码块的结束位置,JVM 需要保证每一个 monitorenter 都有一个 monitorexit 与之相对应。任何对象都有一个 Monitor 与之相关联,当且一个 Monitor 被持有之后,他将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 Monitor 所有权,即尝试获取对象的锁。
3、底层原理
Java 对象头和 Monitor 是实现 synchronized 的基础,而Monitor表面翻译是监听,实际这里是指管程,因此管程机制是解决并发编程的同步、通信两个核心问题的理论基础,Monitor Object设计模式是在此理论上的并发模型的理论实现,对象头是JVM根据自身语言特性提供的一种通过指定数据结构来记录对象数据的方式,而我们当前语境分析的并发编程,对象头也发挥了重要作用,被用来作为实现并发编程的工具或脚手架了。
3.1 对象头
关于对象头内容讲解可以从虚拟机对对象构造的OOP-KLASS二分模型入手进行剖析,这部分可以参考陈涛的《Hotspot实战》第三章的内容。
简单梳理下这部分知识关系从下到上就是:第一层是JVM以OOP-KLASS二分模型进行虚拟机数据构建,第二层是OOP对象结构,基于Java一切皆对象的思想其中涉及到并发编程最相关的就是对象头的设计,第三层就是对象头中各种数据占位存储和标记以及含义。学习知识就是先了解怎么用,再了解原理,再了解实现原理背后的原因和理论依据。
3.2 Monitor Object设计模式
我们在开发并发应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:
- 在任一时间内,只有唯一的公共的成员方法,被唯一的线程所执行。
- 对于对象的调用者来说,如果总是需要在调用方法之前进行拿锁,而在调用方法之后进行放锁,这将会使并发应用编程变得更加困难。合理的设计是,该对象本身确保任何针对它的方法请求的同步被透明的进行,而不需要调用者的介入。
- 如果一个对象的方法执行过程中,由于某些条件不能满足而阻塞,应该允许其它的客户端线程的方法调用可以访问该对象。
在C中使用 Monitor Object设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过 monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个 monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor 对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。
而Java语言正是基于C进行开发的,因此也利用了Monitor Object这一个经典的并发模型作为底层并发模型的支持,synchronized基于Monitor Object设计模式进行实现,是Java对C++这部分并发模型的语义封装。
3.3 管程机制
管程机制为并发编程解决同步、线程通信两大核心问题提供了理论基础,最早可以追溯到20世纪70年代,代表性的是Hansen汉森模型、Hoare霍尔模型以及JVM中Monitor Object设计模式采用的Mesa模型。
三种管程模型在通知线程上的区别
Hasen模型、Hoare模型和MESA模型的一个核心区别是当条件满足后,如何通知相关线程。
管程要求同一时刻只允许一个线程执行,那当线程T2的操作使得线程T1等待的条件满足时,T1和T2究竟谁可以执行呢?
- 1.在Hasen模型里,要求notify()放在代码的最后,这样T2通知完T1后,T2就结束了,然后T1再执行这样就可以保证同一时刻只有一个线程执行。
- 2.在Hoare模型里面,T2通知完T1后,T2阻塞,T1马上执行;等T1执行完,再唤醒t2。比起Hasen模型,T2多了一次阻塞唤醒操作。
- 3.在MESA管程里,T2通知完T1后,T2还是会接着执行,T1并不立即执行,仅仅是从条件变量的等待队列进入到入口等待队列中(但是T1再次执行时,可能条件又不满足了,所以需要循环方式检验条件变量)。这样的好处是:notify()代码不用放到代码的最后,T2也没有多余的阻塞唤醒操作。
这三种模型的一些介绍网上资料没有搜索到太多,可以参考知网这篇文章https://kns.cnki.net/kcms/detail/detail.aspx?dbcode=CJFD&dbname=CJFD7984&filename=JSGG198006005&v=IhRWgtB3ePheMT5fmOobq5TfYSs3THGO1VP1R1QoHV5dXY7iaHiHEziSHS1LduVz。
关于管程机制的描述可能有些抽象了,有位小伙用Java实现了这三种不同和区别,还是建议大家子demo下用自己的理解跑下,光看代码不能完全理解,不过还是可以参考下
https://blog.csdn.net/qq_34666857/article/details/103189107
4、锁优化
4.1 核心态&用户态
用户态切换内核态的过程如下:
JDK早期,synchronized 叫做重量级锁, 因为申请锁资源必须通过kernel, 系统调用。synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。
性能开销分布
- 切换线程上下文,需要保护和恢复寄存器数据
- 切换到执行内核线程的时候,内核代码对用户不信任,需要进行额外的检查。
- 内核线程执行完返回过程有很多额外工作,比如检查是否需要调度等
- 如果被切换的线程属于不同用户程序间切换的话,那么还要更新cr3寄存器,这样会更换每个程序的虚拟内存到物理内存映射表的地址
因此使用synchronized进行资源同步是非常损耗性能的,性能较为低下,因此在JDK1.6版本之后对其进行了优化。综上可知,性能瓶颈存在于用户态与核心态切换上,因此JDK之后对synchronized的优化都基于此进行。
4.2 锁消除
如果通过逃逸分析能够判断出指向某个局部变量的多个引用被限制在同一方法体内,并且所有这些引用都不能“逃逸”到这个方法体以外的地方,那么HotSpot会要求JIT执行一项优化动作 – 将局部变量上拥有的锁省略掉。这就是锁省略(lock elision),如下:
/**
* -XX:+DoEscapeAnalysis 开启逃逸分析
* -XX:-DoEscapeAnalysis 关闭逃逸分析
*/
public static void main(String[] args) {
Vector<String> vector = new Vector<>();
long start = System.currentTimeMillis();
System.out.format("start : %s\n", start);
for (int i = 0; i < 10000000; i++) {
vector.add(i + "");
}
long end = System.currentTimeMillis();
System.out.format("cost : %s\n", end - start);
}
4.3 锁粗化
若在一个栈方法中频繁操作加锁、解锁操作进行循环,会导致操作大部分耗时都集中在对锁的控制上,可以对其进行优化,使用一个较大范围的锁进行控制,减少锁操作次数以达到优化性能的目的。
4.4 锁升级
4.4.1 偏向锁
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
CAS本地延迟
CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:
其意思是所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。
偏向锁获取过程
- 1、访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word),到达安全点safepoint会导致stop the word,时间很短。
- 5、执行同步代码。
偏向锁的释放
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁的适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作; 在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;
查看停顿–安全点停顿日志
要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;
注意:安全点日志不能一直打开:
- 1、 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁
- 2、对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。
- 3、安全点日志是在安全点内打印的,本身加大了安全点的停顿时间
所以安全日志应该只在问题排查时打开。
如果在生产系统上要打开,再再增加下面四个参数:
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。
此日志分三部分:
第一部分是时间戳,VM Operation的类型
第二部分是线程概况,被中括号括起来
total: 安全点里的总线程数
initially_running: 安全点时开始时正在运行状态的线程数
wait_to_block: 在VM Operation开始前需要等待其暂停的线程数
第三部分是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop
- spin: 等待线程响应safepoint号召的时间;
- block: 暂停所有线程所用的时间;
- sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时;
- cleanup: 清理所用时间;
- vmop: 真正执行VM Operation的时间。
可见,那些很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。
偏向锁的开启/关闭
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
4.4.2 自旋锁
自旋锁实现原理
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁优缺点
优点 |
缺点 |
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗 |
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费 |
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要。
自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用 -XX:+UseSpinning 开启。
在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化
- 如果平均负载小于CPUs则一直自旋
- 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
- 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
- 如果CPU处于节电模式则停止自旋
- 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
- 自旋时会适当放弃线程优先级之间的差异
自旋锁的开启/关闭
JDK1.6中-XX:+UseSpinning开启;
JDK1.7后,去掉此参数,由jvm控制
4.4.3 轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程
- 1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
这里写图片描述所示。 - 2、拷贝对象头中的Mark Word复制到锁记录中;
- 3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
这里写图片描述 - 5、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁的释放
释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。
因为重量级锁被修改了,所有display mark word和原来的markword不一样了。
怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其他线程持有。
此时如果线程已经释放了markword,那么通过CAS后就可以直接进入线程,无需进入mutex,就这个作用。
尝试获取锁线程视角:如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。
还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。
这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。
4.4.4 重量级锁
Synchronized的作用
在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;
它可以把任意一个非NULL的对象当作锁。
作用于方法时,锁住的是对象的实例(this);
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
重量级锁的实现
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
5、参考
http://www.iocoder.cn/JUC/sike/synchronized/
https://blog.csdn.net/qq_38832908/article/details/107022233
https://developer.ibm.com/zh/articles/j-lo-synchronized/
https://juejin.im/post/6844903588834066439
https://blog.csdn.net/varyall/article/details/81505055