- 一、ObjectMonitor 源码解读
- 1. 锁池
- 2. 等待池
- 3. wait与notify原理分析
- 二、Hotspot源码解读
- 1. synchronized底层实现原理总结
- 2. 轻量锁原理分析
- 3. 偏向锁原理分析
- 3.1 偏向锁原理
- 3.2 偏向锁撤销
- 3.3 批量重偏向
- 3.4 批量撤销
- 4. 重量锁原理分析
- 5. 锁粗化
- 6. 锁消除
- 7. JDK15 默认关闭偏向锁优化原因
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 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
调用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关联起来。
引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。
注意:
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
演示代码:
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();
// //调用hashCode
System.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,以后只要不发生竞争,这个对象就归该线程所有。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程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 101
System.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
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步 *** 作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
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)过高,所以不如废弃
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)