Synchronized之轻量级锁自旋骗局原创
之前笔者分析了synchronized的偏向锁源码,我们今天继续来看synchronized的轻量级锁逻辑。关于轻量级锁,网上有很多说法都是轻量级锁在发生竞争时会进行自旋,但是经过笔者对源码的学习,并没有发现轻量级锁的自旋逻辑。笔者甚至去jdk6和jdk15中都进行了一番搜索,发现也不存在自旋的逻辑,关于轻量级锁的自旋这个说法,笔者曾经也深信不疑,但是从实际源码中出发,并不存在自旋的逻辑。本篇博客笔者会把整个轻量级锁部分的源码拿出来进行分析,希望能帮助大家以后正确的理解synchronized轻量级锁。
一.偏向锁撤销
我们都知道synchronized中的轻量级锁是由偏向锁升级而来的(关于偏向锁的源码在笔者之前的博客:https://my.oschina.net/u/3645114/blog/5306767中)。所以轻量级锁源码的入口必然在偏向锁后面,事实上再偏向锁升级成轻量级锁之前,还需要进行偏向锁的撤销,偏向锁加锁的方法在interp_masm_x86_64.cpp的lock_object方法中,我们就从这里开始,话不多说,直接看代码:
void InterpreterMacroAssembler::lock_object(Register lock_reg) {
if (UseHeavyMonitors) {
call_VM(noreg,
CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
lock_reg);
} else {
Label done;
const Register swap_reg = rax; // Must use rax for cmpxchg instruction
const Register obj_reg = c_rarg3; // Will contain the oop
const int obj_offset = BasicObjectLock::obj_offset_in_bytes();
const int lock_offset = BasicObjectLock::lock_offset_in_bytes ();
const int mark_offset = lock_offset +
BasicLock::displaced_header_offset_in_bytes();
Label slow_case;
movptr(obj_reg, Address(lock_reg, obj_offset));
//这里是偏向锁逻辑,关于偏向锁的这部分逻辑笔者之前的博客已经分析过了,这里就不进行分析了
if (UseBiasedLocking) {
biased_locking_enter(lock_reg, obj_reg, swap_reg, rscratch1, false, done, &slow_case);
}
// 之前的博客提到过撤销偏向和对象非偏向模式(即已经是轻量级锁)会走这里——升级轻量级锁
// 这里偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态
// 而是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态
// 偏向锁的撤销,是轻量级锁的前提。
// 将1写入swap_reg寄存器
movl(swap_reg, 1);
//将对象的markword 与1或运算并存入swap_reg寄存器
//此处锁标记位是 01 即无锁,因为执行到这里只有撤销偏向锁和对象非偏向锁两种情况
//这两种情况下都为00 | 01,即得无锁
orptr(swap_reg, Address(obj_reg, 0));
//将swap_reg中的数据存到lockrecord中的markword位置
//这里将lockrecord中的displaced header(本质是一个markword)设置为无锁的原因是
//等到解锁时会将displaced header中的markword替换回对象头上
//这时对象应该是无锁的.
movptr(Address(lock_reg, mark_offset), swap_reg);
//汇编lock指令
if (os::is_MP()) lock();
//比较交换(cas) 将对象头替换成指向lockrecord的指针
cmpxchgptr(lock_reg, Address(obj_reg, 0));
//成功则表示已经是轻量级锁且加锁成功直接结束,失败则证明有竞争(可能是偏向锁竞争也可能是轻量级锁竞争)
//进入slow_case逻辑
jcc(Assembler::zero, done);
......
bind(slow_case);
//slow_case逻辑
call_VM(noreg,
CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
lock_reg);
bind(done);
}
}
关于slow_case,笔者之前的博客也提到过,其实是只要发生锁的竞争,就会进入到slow_case部分,我们可以看到,这里的竞争不只是偏向锁竞争,也包括轻量级锁竞争。
还有一点需要注意的,从代码中我们可以看到,持有轻量级锁的lockRecord中的displaced header,即lockRecord中的锁对象(关于这个锁对象,笔者之前的博客也提到过,这里就不展开)按照许多网上的说法,这里应该是保存对象的markword,而轻量级锁对象的markword锁标记位应该是00,但实际上源码在这里将其还原成了无所状态保存到了lockRecord,原因就如笔者注释所说——为了方便解锁时替换回对象的markword。
我们继续看源码:call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),lock_reg) 这个方法(这里是模板解释器调用了c++的方法,后面的源码都是c++相对来说比较好理解一些)可以理解为是调用 InterpreterRuntime::monitorenter方法,其参数为lock_reg寄存器,即之前找到的lockRecord:
//调用了interpreterRuntime.cpp下的方法
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
......
//elem是传入的lockRecord,将线程和lockRecord中的obj封装成句柄
Handle h_obj(thread, elem->obj());
//判断jvm偏向锁参数
if (UseBiasedLocking) {
//会先进入快速处理方法,参数是刚刚封装的句柄和lockRecord中的锁对象
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
......
IRT_END
//继续看ObjectSynchronizer::fast_enter方法
//attempt_rebias参数表示是否接受重偏向,这里是true
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 {
//安全点撤销
BiasedLocking::revoke_at_safepoint(obj);
}
}
//轻量级锁竞争逻辑
slow_enter (obj, lock, THREAD) ;
}
到这里我们可以看到会先执行偏向锁的撤销和重偏向逻辑,然后会根据结果判断是否进入轻量级锁竞争逻辑。我们一步一步来,先看偏向锁的撤销和重偏向逻辑,而偏向锁的撤销和重偏向逻辑又分为两个分支,一个是在全局安全点,一个是在非全局安全点。这两个方法其实逻辑是差不多的,我们重点分析非全局安全点的方法:
//撤销偏向锁 attempt_rebias参数表示是否会重偏向这里传入的是true
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
markOop mark = obj->mark();
//判断是偏向模式,但是尚未偏向其他线程,这里attempt_rebias是true所以不会执行
if (mark->is_biased_anonymously() && !attempt_rebias) {
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED;
}
//判断对象是偏向模式
} else if (mark->has_bias_pattern()) {
Klass* k = obj->klass();
markOop prototype_header = k->prototype_header();
//判断类不是偏向模式,即现在是处于批量撤销延迟阶段,需要修复对象的markword恢复成klass的markword非偏向模式
if (!prototype_header->has_bias_pattern()) {
//将类的markword cas替换到对象的markword,撤销偏向锁
//逻辑还是cas替换,就不再详细分析
markOop biased_value = mark;
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
return BIAS_REVOKED;
//如果类的epoch不等于对象的epoch,表明偏向已经过期
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
//进行重偏向,通常再汇编代码中完成,但是到这里是在运行期间,所以任何时刻可能会产生偏向过期
//逻辑还是cas替换,就不再详细分析
if (attempt_rebias) {
markOop biased_value = mark;
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
//成功则返回撤销并重偏向
if (res_mark == biased_value) {
return BIAS_REVOKED_AND_REBIASED;
}
//因为attempt_rebias是true所以这个分支不会进入
} else {
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED;
}
}
}
}
//这个方法会判断是否需要批量撤销和批量重偏向,这里不进行展开了,有兴趣的读者可以自己展开分析
//主要会返回几个状态
//enum HeuristicsResult {
// HR_NOT_BIASED = 1, 不需要偏向
// HR_SINGLE_REVOKE = 2, 单个撤销
// HR_BULK_REBIAS = 3, 批量重偏向
// HR_BULK_REVOKE = 4 批量撤销
// };
HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);
//不需要偏向
if (heuristics == HR_NOT_BIASED) {
return NOT_BIASED;
//单个撤销
} else if (heuristics == HR_SINGLE_REVOKE) {
Klass *k = obj->klass();
markOop prototype_header = k->prototype_header();
//线程是当前线程,且没有过期,直接撤销
if (mark->biased_locker() == THREAD &&
prototype_header->bias_epoch() == mark->bias_epoch()) {
ResourceMark rm;
if (TraceBiasedLocking) {
tty->print_cr("Revoking bias by walking my own stack:");
}
//撤销方法
BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
return cond;
} else {
//说明是其他线程持有锁,必须等到安全性点执行,声明一个任务类
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}
//批量撤销或重偏向任务类,根据 (heuristics == HR_BULK_REBIAS) 这个条件判断是否是批量撤销或者重偏向
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
VMThread::execute(&bulk_revoke);
return bulk_revoke.status_code();
}
看到这里大家可能对批量撤销和重偏向操作有点疑惑,笔者先说明下关于批量撤销和批量重定向的意义:
1.当一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
二.轻量级锁竞争
我们继续看轻量级锁的竞争方法ObjectSynchronizer::slow_enter:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
//判断是否是无锁(中性)
//到这里无锁状态有两种情况:
//1.偏向锁撤销
//2.曾经是轻量级锁被释放了
if (mark->is_neutral()) {
//替换lockrecord中的displaced_header为对象的markword
lock->set_displaced_header(mark);
//cas替换对象markword为lockRecord地址,成功则返回证明获取轻量级锁成功,失败则进行锁膨胀逻辑
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
//判断是否是轻量级锁,且是否是当前线程持有,是则为重入
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
//重入直接设置displaced_header为null并返回
//表示添加一个Lock Record来表示锁的重入
lock->set_displaced_header(NULL);
return;
}
//锁膨胀逻辑
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
三.总结
通过对源码的学习,我们可以看到轻量级锁和偏向锁是用于处理不同场景的锁,之前大家可能都认为轻量级锁在竞争失败后会自旋,然后多次尝试再获取锁,其实通过本次源码学习,我们可以看到,并没有自旋的逻辑。希望大家再看过本篇博客后,可以正确的认识轻量级锁,明白其设计意义和应对的场景。