Synchronized偏向锁升级
Posted juniorma
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Synchronized偏向锁升级相关的知识,希望对你有一定的参考价值。
网上对于 Synchronized偏向锁升级 真的是错误多多,漏洞多多。包括网上的一些公开课也是讲的很浅。好在我找到一篇不错的文章,特此记录下
https://www.jianshu.com/p/4758852cbff4
给大佬跪了,真的牛逼。
一 偏向锁的获取
首先抛出来2个问题
1 LockRecord 在偏向锁的过程中是否参与
2 偏向锁是否是从无锁竞争而来的
因为我发现真的好多文章都没有提到这两个很基础又很重要的知识点。
1 LockRecord 不是专门为某一个锁对象创建的,而是线程栈就有的,甚至还不止一个
2 偏向锁不是从无锁竞争而来,JVM默认开启偏向锁,所以一个对象的对象头里的锁状态就是偏向锁,只不过是匿名偏向(ThreadId字段是NULL)
这两点我们从源码来证明
CASE(_monitorenter): { // lockee 就是锁对象 oop lockee = STACK_OBJECT(-1); // derefing‘s lockee ought to provoke implicit null check CHECK_NULL(lockee); // code 1:找到一个空闲的Lock Record BasicObjectLock* limit = istate->monitor_base(); BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base(); BasicObjectLock* entry = NULL; while (most_recent != limit ) { if (most_recent->obj() == NULL) entry = most_recent; else if (most_recent->obj() == lockee) break; most_recent++; } //entry不为null,代表还有空闲的Lock Record if (entry != NULL) { // code 2:将Lock Record的obj指针指向锁对象 entry->set_obj(lockee); int success = false; uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place; // markoop即对象头的mark word markOop mark = lockee->mark(); intptr_t hash = (intptr_t) markOopDesc::no_hash; // code 3:如果锁对象的mark word的状态是偏向模式 if (mark->has_bias_pattern()) { uintptr_t thread_ident; uintptr_t anticipated_bias_locking_value; thread_ident = (uintptr_t)istate->thread(); // code 4:这里有几步操作,下文分析 anticipated_bias_locking_value = (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ~((uintptr_t) markOopDesc::age_mask_in_place); // code 5:如果偏向的线程是自己且epoch等于class的epoch if (anticipated_bias_locking_value == 0) { } // code 6:如果偏向模式关闭,则尝试撤销偏向锁 else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) { } // code 7:如果epoch不等于class中的epoch,则尝试重偏向 else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) { // 构造一个偏向当前线程的mark word } else { // 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程) // code 8:下面构建一个匿名偏向的mark word,尝试用CAS指令替换掉锁对象的mark word markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |(uintptr_t)markOopDesc::age_mask_in_place |epoch_mask_in_place)); if (hash != markOopDesc::no_hash) { header = header->copy_set_hash(hash); } markOop new_header = (markOop) ((uintptr_t) header | thread_ident); // debugging hint DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);) if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) { // CAS修改成功 if (PrintBiasedLockingStatistics) (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++; } else { // 如果修改失败说明存在多线程竞争,所以进入monitorenter方法 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } success = true; } } // 如果偏向线程不是当前线程或没有开启偏向模式等原因都会导致success==false if (!success) { // 轻量级锁的逻辑 //code 9: 构造一个无锁状态的Displaced Mark Word,并将Lock Record的lock指向它 markOop displaced = lockee->mark()->set_unlocked(); entry->lock()->set_displaced_header(displaced); //如果指定了-XX:+UseHeavyMonitors,则call_vm=true,代表禁用偏向锁和轻量级锁 bool call_vm = UseHeavyMonitors; // 利用CAS将对象头的mark word替换为指向Lock Record的指针 if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // 判断是不是锁重入 if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { //code 10: 如果是锁重入,则直接将Displaced Mark Word设置为null entry->lock()->set_displaced_header(NULL); } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } } } UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1); } else { // lock record不够,重新执行 istate->set_msg(more_monitors); UPDATE_PC_AND_RETURN(0); // Re-execute } }
代码做了大量删减,但是红色文字就是我提出问题的答案。
偏向锁的获取一定是从匿名偏向到偏向到某一个线程。
- 从当前线程的栈中找到一个空闲的Lock Record(即代码中的BasicObjectLock,下文都用Lock Record代指),判断Lock Record是否空闲的依据是其obj字段 是否为null。注意这里是按内存地址从低往高找到最后一个可用的Lock Record,换而言之,就是找到内存地址最高的可用Lock Record。
- 获取到Lock Record后,首先要做的就是为其obj字段赋值。
- 判断锁对象的mark word是否是偏向模式,即低3位是否为101。
- CAS将偏向线程改为当前线程,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑。
另外有一点要格外的注意,第一个线程获取到偏向锁的逻辑其实并不在 InterpreterRuntime::monitorenter 网上很多分析文章,都觉得获取偏向锁是在这里方法里。
尤其是好多人觉得在fast_enter方法中真是大错特错。
二 偏向锁的撤销
假设ThreadA拿到的偏向锁,此时ThreadB获取偏向锁时会失败。此时才会进入fast_enter
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { if (UseBiasedLocking) { if (!SafepointSynchronize::is_at_safepoint()) { BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { return; } } else { assert(!attempt_rebias, "can not rebias toward VM thread"); BiasedLocking::revoke_at_safepoint(obj); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } slow_enter (obj, lock, THREAD) ; }
BiasedLocking::revoke_and_rebias
方法。这个方法的主要作用像它的方法名:撤销或者重偏向。
整个方法很多,直接说结论。
- 查看偏向的线程是否存活,如果已经不存活了,则直接撤销偏向锁。JVM维护了一个集合存放所有存活的线程,通过遍历该集合判断某个线程是否存活。
- 偏向的线程是否还在同步块中,如果不在了,则撤销偏向锁。我们回顾一下偏向锁的加锁流程:每次进入同步块(即执行
monitorenter
)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record
,将其obj字段指向锁对象。每次解锁(即执行monitorexit
)的时候都会将最低的一个相关Lock Record
移除掉。所以可以通过遍历线程栈中的Lock Record
来判断线程是否还在同步块中。 - 将偏向线程所有相关
Lock Record
的Displaced Mark Word
设置为null,然后将最高位的Lock Record
的Displaced Mark Word
设置为无锁状态,最高位的Lock Record
也就是第一次获得锁时的Lock Record
(这里的第一次是指重入获取锁时的第一次),然后将对象头指向最高位的Lock Record
,这里不需要用CAS指令,因为是在safepoint
。 执行完后,就升级成了轻量级锁。原偏向线程的所有Lock Record都已经变成轻量级锁的状态。
总结下上面原作者的话:
1 如果原来的线程不存活了,锁对象变成无锁状态,方法return。这样,ThreadB就能进入slow_enter了。slow_enter就是获取轻量级锁,获取不到才是锁膨胀。
2 如果原线程存在,那么原线程直接获取该轻量级锁,同时ThreadB还是进入slow_enter,参与到轻量级锁的竞争。
1 从当前线程的栈中找到一个空闲的Lock Record(即代码中的BasicObjectLock,下文都用Lock Record代指),判断Lock Record是否空闲的依据是其obj字段 是否为null。注意这里是按内存地址从低往高找到最后一个可用的Lock Record,换而言之,就是找到内存地址最高的可用Lock Record。
2 获取到Lock Record后,首先要做的就是为其obj字段赋值。
3 判断锁对象的mark word是否是偏向模式,即低3位是否为101。
4 CAS将偏向线程改为当前线程,如果当前是匿名偏向则能修改成功,否则进入锁升级的逻辑。
以上是关于Synchronized偏向锁升级的主要内容,如果未能解决你的问题,请参考以下文章
Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇