【JUC并发编程】synchronized原理分析(下)(ObjectMonitor 源码解读\ Hotspot源码解读\ synchronized底层实现原理总结\ 轻量锁、偏向锁、重量锁原理分析)
目录
- 一、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.openjdk.java.net/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中。
- 如果等于当前的线程id,则不会重复的执行CAS操作,而是直接进入到
-
其他的线程与偏向锁线程开始竞争,撤销偏向锁次数达到了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)过高,所以不如废弃
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
