JUC源码解析:深入解读偏向锁
本文使用 jdk8
几种锁状态介绍
先介绍一下锁状态吧
看偏向锁这一栏, 它的内存存储了 线程ID 和 Epoch , 这一点尤为关键, 意味着偏向锁没有内存可以存储对象头的 hashCode, 而其他锁是有地方存的.。也就意味着,,当锁对象被隐式(父类)或显试调用了 hashcode 时, 就不能进入偏向锁! , 下一节中我会着重介绍
深入偏向锁
- 偏向锁默认在 jdk 启动几秒后才激活,可以使用JVM参数取消启动延迟:
-XX:BiasedLockingStartupDelay=0
为了便于实验,我之后的所有代码都会使用此参数
-XX:-UseBiasedLocking=false
可取消偏向锁
我先来一段代码,用实例看看Mark Word
static Object yyLock = new Object();
public static void main(String[] args) {
System.out.println("=========== 进入 sync 代码块之前 =========");
System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
synchronized (yyLock) {
System.out.println("=========== 进入 sync 代码块之后 =========");
System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
}
}
看看结果: 这两段输出分别是进入 sync 前和进入 sync 后的,因为输出太长我做了截选
新对象的mark Word默认就是偏向锁状态, 不过并没有存储偏向线程的id,而只是把值设置为了101
注意观察黄色框框
在进入同步块之前,偏向锁对象没有存储线程id,所以内存是空, 进入同步块之后,偏向锁对象存储了当前的线程ID
线程的id会保存在两个地方:
- 锁的对象头:存储偏向线程id
- 线程自己的栈帧,LOCK RECORD:存储自己的id
线程进入同步块时(重点!):会先把锁对象当成偏向锁,检测这两个地方的线程id
-
如果测试成功,线程会直接获得锁。
-
如果测试失败,会检查该锁在Mark Word 里是否为101偏向锁:
-
如果不是偏向锁,CAS竞争锁
-
如果是偏向锁,则尝试CAS将当前线程作为偏向线程!
-
值得一提的是,Mark Word中可以看到,偏向锁没有内存空间来保存对象的hashcode
偏向锁中能存储线程id和Epoch,但没有存储hashcode的位置了
所以说,如果一个对象显式或隐式地调用方法生成了 hashcode(),那么,那么偏向锁只能被迫升级为轻量级锁
举一个代码示例:
static Object yyLock = new Object();
public static void main(String[] args) {
System.out.println("=========== 进入 sync 代码块之前 =========");
System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
// 隐式生成了hashcode
HashMap map = new HashMap<>();
map.put(yyLock, "");
synchronized (yyLock) {
System.out.println("=========== 进入 sync 代码块之后 =========");
System.out.println(ClassLayout.parseInstance(yyLock).toPrintable());
}
}
这时候看看输出:
锁升级了,偏向锁成为了轻量级锁
偏向锁的锁升级
偏向锁使用了一直到竞争出现才会释放锁的机制,一旦出现竞争环境,偏向锁就会升级为轻量级锁
但是
一定会升级吗?一定是竞争才会升级吗?一定会升级为轻量级锁吗?
一、不出现竞争也可能升级
若有锁对象lock、线程A、线程B。线程B要在线程A死亡后执行。
线程A获取lock轻量级锁,lock对象头存储A的线程ID,线程A释放轻量级锁,线程A死亡。lock仍存储着A的线程ID
线程B在线程A死亡后拿到lock偏向锁,lock对象头仍存储着A的线程ID,这时,没有发生锁争抢,但偏向锁lock升级为轻量级锁。
线程B释放lock锁后,lock由轻量级锁释放为无锁状态。
二、偏向锁直接升级为重量级锁
若有锁对象lock,线程A、线程B。
线程A获取lock轻量级锁,线程A还没有释放lock轻量级锁,这时线程B来争抢lock,lock会直接升级成重量级锁。
三、出现竞争时升级为轻量级锁
线程A获取偏向锁,线程A释放了偏向锁,但线程A还没有死亡,这时,线程B来争抢偏向锁,部分偏向锁会升级为轻量级锁、部分偏向锁仍保持偏向锁
只理解为升级为轻量级锁就行了。保持偏向锁的部分建议忽视不做深究。
四、升级为重量级锁后又变成轻量级锁(不建议深究了)
若有锁对象lock,线程A、线程B。
线程A获取lock轻量级锁,线程A还没有释放lock,线程B去争抢lock,这时,lock轻量级锁升级为重量级锁
待线程A、线程B均释放lock且死亡,lock重量级锁变成lock轻量级锁。
偏向锁的撤销
若有偏向锁 lock、线程A、线程B
线程A访问同步块,检查lock对象头中是否存储了A的线程ID,如果有,就获取偏向锁,如果没有,通过CAS自旋将自己的线程ID写入lock MarkWord中。
此时,线程A已经获取到了lock偏向锁,线程B要争抢偏向锁,首先,线程B会检查lock对象头是否存储了自己的线程ID,发现没有存储,尝试CAS替换lock MarkWord,失败了,会发起撤销偏向锁请求。
B发起的撤销请求会使线程A暂停,让线程A进入安全点,这时线程A会解锁,线程A移除掉自己栈帧里的 LOCK RECORD(存储线程id),同时删除MarkWord里的线程ID,最后恢复线程。
此时线程B就有机会争抢锁了,会将其争抢为轻量级锁。
批量重偏向 和 批量锁撤销
若有一批以相同Class new 出来的对象作为锁,让线程A、B、C 争抢这一批对象锁
A线程获取这一批偏向锁,之后A释放这一批偏向锁,并且A不再是活跃线程。
A不再活跃后,有B线程来获取这一批偏向锁,这些偏向锁会升级成轻量级锁。
批量重偏向:
如果B一直获取,达到大约20次阈值,此时会触发批量重偏向,jvm会让之后的锁对象直接偏向线程B,B之后获取到的回是偏向锁。
批量撤销:
在基于批量重偏向的基础上,还在继续进行争抢这批锁达到约40次阈值,并且有第三条线程C加入,这时会触发批量撤销。JVM会标记该Class的对象不能使用偏向锁,以后新创建的对象直接以轻量级锁开始。这是真正完成了锁升级。
真正的锁升级是依赖于 Class 的,而不是依赖于某一个对象的。 发生批量撤销后,使用这个Class new出来的对象,都不能使用偏向锁,而是直接以轻量级锁开始的。
标签:JUC,存储,对象,lock,源码,线程,轻量级,解析,偏向 From: https://www.cnblogs.com/yangruomao/p/18177089