2000字范文,分享全网优秀范文,学习好帮手!
2000字范文 > 【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读\ Hotspot源

【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读\ Hotspot源

时间:2023-10-05 23:04:59

相关推荐

【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读\ Hotspot源

目录

一、ObjectMonitor 源码解读1. 锁池2. 等待池3. wait与notify原理分析 二、Hotspot源码解读1. synchronized底层实现原理总结2. 轻量锁原理分析3. 偏向锁原理分析3.1 偏向锁原理3.2 偏向锁撤销3.3 批量重偏向3.4 批量撤销 4. 重量锁原理分析5. 锁粗化6. 锁消除7. JDK15 默认关闭偏向锁优化原因

一、ObjectMonitor 源码解读

Java底层使用 C++ hotspot虚拟机

http://hg./jdk8 下载hotspot虚拟机

Objectmonitor 底层基于C++实现。

Hotspot 源码位置:

hotspot\src\share\vm\runtime\objectMonitor.hpp

ObjectMonitor() {_header = NULL;_count = 0; // 记录个数_waiters= 0,_recursions = 0; // 递归次数/重入次数_object = NULL; // 存储Monitor关联对象_owner = NULL; // 记录当前持有锁的线程ID_WaitSet= NULL; // 等待池:处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq= NULL ; // 多线程竞争锁时的单向链表FreeNext= NULL ;_EntryList = NULL ; // 锁池:处于等待锁block状态的线程,会被加入到该列表_SpinFreq= 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}

1. 锁池

锁池: 假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),

由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,

所以这些线程就进入了该对象的锁池中。

EntryList (锁池) 当前的线程获取锁失败,阻塞 链表数据结构存放

2. 等待池

WaitSet----主动释放锁 阻塞等待-----wait方法 等待池中

等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,

这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程

调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。

如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

1.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

2.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。

3.优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

3. wait与notify原理分析

调用wait方法,即可进入WaitSet变为WAITING状态

BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片

BLOCKED线程会在Owner线程释放锁的时候被唤醒

WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

锁池: 没有获取到锁的线程

等待池:调用wait 方法

相同点: 都会阻塞

等待池的线程被唤醒之后 等待池转移到锁池----从新竞争锁的资源。

Notify()----只会唤醒等待中一个线程

NotifyAll()-----唤醒所有的线程

二、Hotspot源码解读

1. synchronized底层实现原理总结

Synchronized 偏向锁(101)、轻量锁(000)、重量级(010)

Synchronized 锁的升级状态存放在 java对象头中markword中,64位存放

偏向锁: 当前线程从对象头中markword获取是否是为偏向锁,如果是为偏向锁,则判断线程的id===当前线程id

如果等于当前的线程id,则不会重复的执行CAS操作,而是直接进入到

我们的同步代码快如果不等于当前的线程id 如果是为无锁的情况,没有其他的线程

与我竞争的话,直接使用CAS修改markword中锁的标识位状态为101

同时也存放当前线程的id在markword中。

其他的线程与偏向锁线程开始竞争,撤销偏向锁次数达到了20次,则后面

开始直接批量重偏向T2线程(注意事项:没有其他的线程与t2做竞争),如果撤销

偏向锁次数达到了40次,则后面开始批量撤销

撤销偏向锁需要在一个全局的安全点 停止我们偏向锁线程,在修改我们markword

中为轻量级锁,在唤醒偏向锁的线程

轻量级锁获取锁与释放锁 (用户态中 一直自旋的形式 消耗cpu的资源)

多个线程同时竞争同一把锁,则升级到轻量锁 使用CAS(修改markword 锁的状态=00)

如果成功,则与markword 替换 将HashCode值等 直接存放在我们的栈帧中,而当前markword 中存放锁记录地址。

当我们使用轻量级锁释放锁时,则还原markword 值内容。

重量级

当前我们的线程重试了多次还是没有获取到锁,则当前锁会升级为重量级锁,

没有获取到锁的线程 会存放在C++Monitor EntryList 集合中 同时当前线程会直接阻塞释放了cpu执行权,在后期唤醒从新进入竞争锁的流程成本是非常高的,因为需要发生cpu上下文切换 用户态到内核切换 改我们对象头中markword 值为C++Monitor 内存地址指针

Java对象与C++Monitor关联起来。

2. 轻量锁原理分析

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

注意:

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

演示代码:

private static Object objectLock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (objectLock) {System.out.println("线程1代码");}}).start();new Thread(() -> {synchronized (objectLock) {System.out.println("线程2代码");}}).start();}

创建锁记录(Lock Record)对象,每个线程的栈帧(方法)都会包含一个锁记录的结构,内部可以储存锁定关联对象的Mark Word锁记录中Object reference (对象引用)指向锁对象,采用CAS算法 替换Object锁对象 的Mark Word,将Mark Word 的值存入锁记录如果CAS执行成功,则对象头中存储了锁记录地址和状态00,表示该线程获取到锁

演示:

如果是其它线程已经持有了该Object对象的轻量级锁,表示多个线程开始竞争,进入锁

升级过程(膨胀/膨化)如果当前线程已经获取到了锁,则在新增一条Lock Record 作为重入次数。当退出synchronized代码块(解锁时)Lock Record 地址指向为null,代表锁记录有重入,这时重置记录,表示重入计数减一。当退出synchronized代码块(解锁时)Lock Record 地址指向锁值不为null,这时使用cas将Mark Word的值恢复给对象头

如果成功,则解锁成功

演示代码:

public class Test1000 {public synchronized static void main(String[] args) {DemoLock demoLock = new DemoLock();// //调用hashCodeSystem.out.println(Integer.toHexString(demoLock.hashCode()));synchronized (demoLock) {System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());}try {Thread.sleep(4000);System.out.println(ClassLayout.parseInstance(demoLock).toPrintable());} catch (Exception e) {}}static class DemoLock {int i = 2028; // 4个字节 4+开启指针压缩对象头12个字节boolean b = true; // 1个字节 16+1=17}}

3. 偏向锁原理分析

偏向锁在没有竞争时,(就自己这个线程),每次重入仍然执行CAS操作.

Java6中引入了偏向锁来做进一步优化;只有第一次使用CAS将线程ID设置到对象的Mark Word,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。

3.1 偏向锁原理

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则,使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

3.2 偏向锁撤销

由于偏向锁使用了一种直到竞争发生时才会释放的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会去释放锁。

3.3 批量重偏向

批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。

批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

1.启动设置参数:

通过JVM的默认参数值,批量重偏向和批量撤销的阈值。

设置JVM参数-XX:+PrintFlagsFinal,在项目启动时即可输出JVM的默认参数值

intx BiasedLockingBulkRebiasThreshold = 20默认偏向锁批量重偏向阈值

intx BiasedLockingBulkRevokeThreshold = 40默认偏向锁批量撤销阈值

当然我们可以通过-XX:BiasedLockingBulkRebiasThreshold

-XX:BiasedLockingBulkRevokeThreshold来手动设置阈值

2.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID

相关演示代码:

public class Thread02 {static class A {}public static void main(String[] args) throws InterruptedException {//延时产生可偏向对象 演示 批量偏向锁Thread.sleep(5000);//创造100个偏向线程t1的偏向锁List<A> listA = new ArrayList<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {A a = new A();synchronized (a) {listA.add(a);}}try {//为了防止JVM线程复用,在创建完对象后,保持线程t1状态为存活Thread.sleep(100000000);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();//睡眠3s钟保证线程t1创建对象完成Thread.sleep(3000);// 对象头中 19个对象-- 偏向锁 指向T1 101System.out.println("打印t1线程,list中第20个对象的对象头:");System.out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));System.out.println((ClassLayout.parseInstance(listA.get(21)).toPrintable()));//创建线程t2竞争线程t1中已经退出同步块的锁Thread t2 = new Thread(() -> {//这里面只循环了30次for (int i = 0; i < 24; i++) {A a = listA.get(i);synchronized (a) {//分别打印第19次和第20次偏向锁重偏向结果 -323248123 升级轻量级锁if (i == 18 || i == 19) {System.out.println("第" + (i + 1) + "次偏向结果");System.out.println((ClassLayout.parseInstance(a).toPrintable()));}}}try {Thread.sleep(10000000);} catch (InterruptedException e) {e.printStackTrace();}});t2.start();Thread.sleep(3000);System.out.println("打印list中第21个对象的对象头:");System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));System.out.println("打印list中第29个对象的对象头:");System.out.println((ClassLayout.parseInstance(listA.get(29)).toPrintable()));System.out.println((ClassLayout.parseInstance(listA.get(30)).toPrintable()));}}

3.4 批量撤销

当一个偏向锁如果撤销次数到达40的时候就认为这个对象设计的有问题;那么JVM会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。

public class Thread03 {static class A {}public static void main(String[] args) throws InterruptedException {Thread.sleep(5000);List<A> listA = new ArrayList<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {A a = new A();synchronized (a) {listA.add(a);}}try {Thread.sleep(100000000);} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(5000);Thread t2 = new Thread(() -> {//这里循环了40次。达到了批量撤销的阈值for (int i = 0; i < 40; i++) {A a = listA.get(i);synchronized (a) {}}try {Thread.sleep(10000000);} catch (InterruptedException e) {e.printStackTrace();}});t2.start();//———————————分割线,前面代码不再赘述——————————————————————————————————————————Thread.sleep(5000);// System.out.println("打印list中第21个对象的对象头:");// System.out.println((ClassLayout.parseInstance(listA.get(20)).toPrintable()));Thread t3 = new Thread(() -> {for (int i = 20; i < 40; i++) {A a = listA.get(i);synchronized (a) {if (i == 20 || i == 22) {System.out.println("thread3 第" + i + "次");System.out.println((ClassLayout.parseInstance(a).toPrintable()));}}}});t3.start();Thread.sleep(10000);System.out.println("重新输出新实例A");System.out.println((ClassLayout.parseInstance(new A()).toPrintable()));}}

4. 重量锁原理分析

如果其他的线程尝试轻量级的过程中,CAS多次还是失败,则轻量级会升级为重量级锁。

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

源码相关:ObjectMonitor::enter

5. 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

private static Object lock = new Object();private static int count = 0;private static int j = 0;public static void a() {synchronized (lock) {b();}synchronized (lock) {c();}}public static void b() {count++;}public static void c() {j++;}public static void a1() {synchronized (lock) {b();c();}}/*** 改成:** @param args*/public static void main(String[] args) {a();}

6. 锁消除

锁消除是发生在编译器级别的一种锁优化方式。

有时候我们写的代码完全不需要加锁,却执行了加锁操作。

比如,StringBuffer类的append操作:

public static void main(String[] args) {long start = System.currentTimeMillis();int size = 10000;for (int i = 0; i < size; i++) {createStringBuffer("demo", "demo01");}long timeCost = System.currentTimeMillis() - start;System.out.println("createStringBuffer:" + timeCost + " ms");}public static String createStringBuffer(String str1, String str2) {StringBuffer sBuf = new StringBuffer();sBuf.append(str1);// append方法是同步操作sBuf.append(str2);return sBuf.toString();}

代码中createStringBuffer方法中的局部对象sBuf,就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,都会创建不同的sBuf对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源。

7. JDK15 默认关闭偏向锁优化原因

JDK15默认关闭偏向锁优化,如果要开启可以使用XX:+UseBiasedLocking,但使用偏向锁相关的参数都会触发deprecate警告

原因

1 偏向锁导致synchronization子系统的代码复杂度过高,并且影响到了其他子系统,导致难以维护、升级

2 在现在的jdk中,偏向锁带来的加锁时性能提升从整体上看并没有带来过多收益(撤销锁的成本过高 需要等待全局安全点,再暂停线程做锁撤销)

3 官方说明中有这么一段话: since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true.,原子指令成本变化(我理解是降低),导致自旋锁需要的原子指令次数变少(或者cas操作变少 个人理解),所以自旋锁成本下降,故偏向锁的带来的优势就更小了。

维持偏向锁的机会成本(opportunity cost)过高,所以不如废弃

【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读\ Hotspot源码解读\ synchronized底层实现原理总结\ 轻量锁 偏向锁 重量锁原理分析)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。