锁粗化过程
偏向锁
①:markword中保存的线程ID是自己且epoch等于class的epoch,则说明是偏向锁重入。
②:偏向锁若已禁用,进行撤销偏向锁。
③:偏向锁开启,都进行进行重偏向操作。
④:若进行了锁撤销操作或重偏向操作失败,则需要升级为轻量级锁或者进一步升级为重量级锁。
匿名偏向
锁对象在发送锁竞争后会升级为偏向锁,不过当不发生锁竞争时,锁对象依然会升级为偏向锁,这种情况叫匿名偏向。
当jvm启动4s后,会默认给新建的对象加上偏向锁。
上代码:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.8</version> </dependency>
这个包下的工具类的功能有:
// 查看对象内部结构 System.out.println(ClassLayout.parseInstance(bingo).toPrintable()); // 查看对象外部信息 System.out.println(GraphLayout.parseInstance(bingo).toPrintable()); // 查看对象总大小 System.out.println(GraphLayout.parseInstance(bingo).totalSize());
默认JVM是开启指针压缩,可以通过vm参数开启关闭指针压缩: -XX:-UseCompressedOops
。
当创建锁对象前不进行休眠4s的操作:
@Test public void mark() throws InterruptedException { Bingo bingo = new Bingo(); bingo.setP(1); bingo.setB(false); // 查看对象内部结构 System.out.println(ClassLayout.parseInstance(bingo).toPrintable()); System.out.println("\n++++++++++++++++++++++++++\n"); synchronized (bingo) { // 查看对象内部结构 System.out.println(ClassLayout.parseInstance(bingo).toPrintable()); } }
看我标红线的后三位的值,由于启动过快,锁直接从无锁升级成了轻量级锁。
当创建锁对象前进行休眠4s的操作:
@Test public void mark() throws InterruptedException { TimeUnit.SECONDS.sleep(4); Bingo bingo = new Bingo(); bingo.setP(1); bingo.setB(false); // 查看对象内部结构 System.out.println(ClassLayout.parseInstance(bingo).toPrintable()); System.out.println("\n++++++++++++++++++++++++++\n"); synchronized (bingo) { // 查看对象内部结构 System.out.println(ClassLayout.parseInstance(bingo).toPrintable()); } }
当在程序启动4s后创建锁对象,就会默认偏向。
重偏向
因为偏向锁不会自动释放,因此当锁对象处于偏向锁时,另一个线程进来只能依托VM判断上一个获取偏向锁的线程是否存活、是否退出持有锁来决定是锁升级还是进行重偏向。
锁撤销
①:偏向锁的撤销必须等待VM全局安全点(安全点指所有java线程都停在安全点,只有vm线程运行)。
②:撤销偏向锁恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态。
③:只要发生锁竞争,就会进行锁撤销。
备注:
当开启偏向锁时,若持有偏向锁的线程仍然存活且未退出同步代码块,锁升级为轻量级锁/重量级锁之前会进行偏向锁撤销操作。
如果是升级为轻量级锁,撤销之后需要创建Lock Record 来保存之前的markword信息。
批量偏向/撤销概念:
参考1: 盘一盘 synchronized (二)—— 偏向锁批量重偏向与批量撤销 - 柠檬五个半 - 博客园
- 批量重偏向
当一个线程同时持有同一个类的多个对象的偏向锁时(这些对象的锁竞争不激烈),执行完同步代码块后,如果另一个线程也要持有这些对象的锁,当对象数量达到一定程度时,会触发批量重偏向机制(进行过批量重偏向的对象不可再进行批量重偏向)。 - 批量锁撤销
当触发批量重偏向后,会触发批量撤销机制。
阈值定义在globals.hpp中:
// 批量重偏向阈值 product(intx, BiasedLockingBulkRebiasThreshold, 20) // 批量锁撤销阈值 product(intx, BiasedLockingBulkRevokeThreshold, 40)
可以在VM启动参数中通过 -XX:BiasedLockingBulkRebiasThreshold
和 -XX:BiasedLockingBulkRevokeThreshold
来手动设置阈值。
偏向锁的撤销和重偏向的代码(过于复杂)在biasedLocking.cpp中:
void BiasedLocking::revoke_at_safepoint(Handle h_obj) { assert(SafepointSynchronize::is_at_safepoint(), "must only be called while at safepoint"); oop obj = h_obj(); HeuristicsResult heuristics = update_heuristics(obj, false); if (heuristics == HR_SINGLE_REVOKE) { // 重偏向 revoke_bias(obj, false, false, NULL); } else if ((heuristics == HR_BULK_REBIAS) || (heuristics == HR_BULK_REVOKE)) { // 批量撤销或重偏向 bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL); } clean_up_cached_monitor_info(); }
参考2:
对于存在明显多线程竞争的场景下使用偏向锁是不合适的,比如生产者-消费者队列。生产者线程获得了偏向锁,消费者线程再去获得锁的时候,就涉及到这个偏向锁的撤销(revoke)操作,而这个撤销是比较昂贵的。那么怎么判断这些对象是否适合偏向锁呢?jvm采用以类为单位的做法,其内部为每个类维护一个偏向锁计数器,对其对象进行偏向锁的撤销操作进行计数。当这个值达到指定阈值的时候,jvm就认为这个类的偏向锁有问题,需要进行重偏向(rebias)。对所有属于这个类的对象进行重偏向的操作叫批量重偏向(bulk rebias),之前的做法是对heap进行遍历,后来引入epoch。当需要bulk rebias时,对这个类的epoch值加1,以后分配这个类的对象的时候mark字段里就是这个epoch值了,同时还要对当前已经获得偏向锁的对象的epoch值加1,这些锁数据记录在方法栈里。这样判断这个对象是否获得偏向锁的条件就是:mark字段后3位是101,thread字段跟当前线程相同,epoch字段跟所属类的epoch值相同。如果epoch值不一样,即使thread字段指向当前线程,也是无效的,相当于进行过了rebias,只是没有对对象的mark字段进行更新。如果这个类的revoke计数器继续增加到一个阈值,那个jvm就认为这个类不适合偏向锁了,就要进行bulk revoke。于是多了一个判断条件,要查看所属类的字段,看看是否允许对这个类使用偏向锁。
轻量级锁
轻量级体现在线程会尝试在自己的堆栈中创建Lock Record存储锁对象的相关信息,不需要在内核态和用户态之间进行切换,不需要操作系统进行调度。
加锁
拿到轻量级锁线程堆栈:
Lock Record主要分为两部分:
- obj
指向锁对象本身。重入时也如此。 - displaced header(缩写为hdr)
第一次拿到锁时hdr存放的是encode加密后的markword,重入时存放null。
思考:为什么锁重入时hdr存放的是null,而不是用计数器来实现呢?
假设一个场景,当一个线程同时拿到A、B、C...N 多个锁的时候,那么线程的堆栈中,肯定有多个锁对象的Lock Record,
如:
synchronized(a){ synchronized(b){ synchronized(c){ // do something synchronized(a){ // do something } } } }
当锁a重入时,如果用计数器,还得遍历当前线程堆栈拿到第一次的Lock Record,解锁时也要遍历,效率必然低下。作为jdk底层代码必然讲究效率。
以上纯属个人看法(欢迎交流)。
解锁
①:使用遍历方式将当前线程堆栈中属于该锁对象的Lock Record 指向Null。
②:CAS还原markword为无锁状态。
③:第②步失败需要升级为重量级锁。
优缺点
-
优点
在线程接替/交替执行的情况下,锁竞争比较小,可以避免成为重量级锁而引起的性能问题。
-
缺点
当锁竞争比较激烈、多线程同事竞争锁的时候,需要从轻量级升级为重量级,产生了额外的开销。
源码分析
加锁
加锁、解锁流程的代码在InterpreterRuntime.cpp中。
这是我从github拉下来的源码:
/** * (轻量级锁