其实很早之前通过一些资料,就对偏向锁稍微有些了解,周六准备看看Hotspot中关于偏向锁的实现,本以为应该畅通无阻,没想到处处都是拦路虎,细节比较多,真是硬着头皮看了一整天,才大概懂了点。笔者还在不断学习,只是想把自己的笔记分享出来,理解能力有限,可能有不正确的地方,还望指正,别让我误导了他人😭。
一:锁的表示
Java里的锁,主要都是对对象进行加锁,如普通的synchronized非静态方法,就是对当前执行方法的对象进行加锁。那么怎么对对象进行加锁呢?对象的锁其实主要就是通过对象头的markOop进行表示的。markOop其实不是一个对象,只是一个字长的数据,在32为机器上,markOop为32个位,在64位上为64个位。markOop中不同的位区域存储着不同的信息,但是需要注意的一点是,markOop每个位区域表示的信息不是一定的,在不同状态下,markWord中存着不同的信息。接下来盗图一张:
584866-20170420091115212-1624858175.jpg由上图可知在markWord在对象的不同状态下,会有5种表示形式。
二:何为偏向锁
很多情况下,一个锁对象并不会发生被多个线程访问得情况,更多是被同一个线程进行访问,如果一个锁对象每次都被同一个线程访问,根本没有发生并发,但是每次都进行加锁,那岂不是非常耗费性能。所以偏向锁就被设计出来了。
偏向,也可以理解为偏心。当锁对象第一次被某个线程访问时,它会在其对象头的markOop中记录该线程ID,那么下次该线程再次访问它时,就不需要进行加锁了。但是这中间只要发生了其他线程访问该锁对象的情况,证明这个对象会发生并发,就不能对这个对象再使用偏向锁了,会进行锁的升级,这是后话,我们这里还是主要讨论下偏向锁。
三:源码探究
我们就以synchronized方法为入口吧。
之前在《JVM方法执行的来龙去脉》中提到过,JVM执行方法最后会以对应的entry_point例程作为入口。entry_point例程不仅会进行java方法栈帧的创建,如果是同步方法,还会进行加锁:
address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
......
if (synchronized) {
// Allocate monitor and lock method
lock_method();
} else {
......
}
// 下面会开始执行方法的字节码
......
}
可见在执行方法的字节码之前,对于同步方法,entry_point例程插入了一道关卡:lock_method():
void TemplateInterpreterGenerator::lock_method() {
.......
// get synchronization object
{
Label done;
__ movl(rax, access_flags);
__ testl(rax, JVM_ACC_STATIC);
// get receiver (assume this is frequent case)
// 局部变量表中第一个变量,存放着即将锁的对象指针,移动到rax中
__ movptr(rax, Address(rlocals, Interpreter::local_offset_in_bytes(0)));
__ jcc(Assembler::zero, done);
__ load_mirror(rax, rbx);
__ bind(done);
}
// add space for monitor & lock
// 在当前栈帧中分配一个空间,用于分配一个BasicObjectLock对象
__ subptr(rsp, entry_size); // add space for a monitor entry
__ movptr(monitor_block_top, rsp); // set new monitor block top
// store object
// 将要锁的对象指针移动到分配的BasicObjectLock中的obj变量
__ movptr(Address(rsp, BasicObjectLock::obj_offset_in_bytes()), rax);
const Register lockreg = NOT_LP64(rdx) LP64_ONLY(c_rarg1);
// 将分配的BasicObjectLock的指针移动到lockreg寄存器中
__ movptr(lockreg, rsp); // object address
// 加锁
__ lock_object(lockreg);
}
在上面的lock_method()中,会在当前方法栈帧中分配一段空间,用于分配一个BasicObjectLock对象,这个对象主要干两件事,一是记录将要锁的对象指针,而是用一个字长的空间,复制锁对象的markOop。现在我们可能不知道这么做是为什么,但是后面就会清楚了。主要上面最后一步,调用了lock_object()进行加锁:
void InterpreterMacroAssembler::lock_object(Register lock_reg) {
......
// 如果使用重量级锁,直接进入InterpreterRuntime::monitorenter()执行
if (UseHeavyMonitors) {
call_VM(noreg,
CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
lock_reg);
} else {
Label done;
// cmpxchg其实就是CAS操作,必须使用rax寄存器作为老数据的存储。
const Register swap_reg = rax; // Must use rax for cmpxchg instruction
const Register tmp_reg = rbx; // Will be passed to biased_locking_enter to avoid a problematic case where tmp_reg = no_reg.
const Register obj_reg = LP64_ONLY(c_rarg3) NOT_LP64(rcx); // Will contain the oop
......
Label slow_case;
// Load object pointer into obj_reg
movptr(obj_reg, Address(lock_reg, obj_offset));
//如果虚拟机参数允许使用偏向锁,那么进入biased_locking_enter()中
if (UseBiasedLocking) {
// lock_reg :存储的是分配的BasicObjectLock的指针
// obj_reg :存储的是锁对象的指针
// slow_case :即InterpreterRuntime::monitorenter();
// done :标志着获取锁成功。
// slow_case 和 done 也被传入,这样在biased_locking_enter()中,就可以根据情况跳到这两处了。
biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
}
......
......
// 直接跳到这,需要进入InterpreterRuntime::monitorenter()中去获取锁。
bind(slow_case);
// Call the runtime routine for slow case
call_VM(noreg,
CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
lock_reg);
// 直接跳到这表明获取锁成功,接下来就会返回到entry_point例程进行字节码的执行了。
bind(done);
}
}
上面可知,如果虚拟机参数允许使用偏向锁,那么会进入biased_locking_enter()中,biased_locking_enter()这个方法涉及到了很多细节,说实话在不了解这些细节的情况下直接看代码,简直是一头雾水。接下来还是一边看代码一边去讲解细节吧。
四:偏向锁的获取
biased_locking_enter()也比较长,就不直接贴方法块了,一步步分析比较好。
1:判断锁对象是否为偏向锁状态
// mark_addr:锁对象头中的markOop指针。
Address mark_addr (obj_reg, oopDesc::mark_offset_in_bytes());
NOT_LP64( Address saved_mark_addr(lock_reg, 0); )
if (PrintBiasedLockingStatistics && counters == NULL) {
counters = BiasedLocking::counters();
}
Label cas_label;
int null_check_offset = -1;
// 如果swap_reg中没存mark_addr,那么就先将mark_addr存入swap_reg中。
if (!swap_reg_contains_mark) {
null_check_offset = offset();
movptr(swap_reg, mark_addr);
}
// 将对象的mark_addr,即markOop指针移入tmp_reg中
movptr(tmp_reg, swap_reg);
// 将tmp_reg和biased_lock_mask_in_place进行与操作,biased_lock_mask_in_place为111,和它进行与就可以取出markOop中后三位,即(是否偏向锁+锁标志位)
andptr(tmp_reg, markOopDesc::biased_lock_mask_in_place);
// 将上面结果,即(是否偏向锁+锁标志位)和biased_lock_pattern再次比较(biased_lock_pattern为5,即101),如果不相等,则表明不为偏向锁状态,需要进行CAS操作,跳往cas_label;否则即为偏向锁状态,接着往下走。
cmpptr(tmp_reg, markOopDesc::biased_lock_pattern);
jcc(Assembler::notEqual, cas_label);
2:走到这,表明锁对象已经为偏向锁态,需要判断锁对象之前是否已经偏向当前线程。
// 将锁对象所属类的prototype_header移动至tmp_reg中,prototype_header中存储的也是markOop。
// prototype_header是专门为偏向锁打造的,初始时类的prototype_header为偏向锁态,即后三位为101,一旦发生了bulk_revoke,那么就会设为无锁态,即001。
// bulk_revoke为批量撤销,每次类发生bulk_rebais时(类的所有对象重设偏向锁),类prototype_header中的epoch就会+1,当epoch达到一个阈值时,就会发生bulk_revoke,撤销该类每个对象的偏向锁,这样该类的所有对象以后都不能使用偏向锁了,其实也就是虚拟机认为该对象不适合偏向锁。
load_prototype_header(tmp_reg, obj_reg);
// 将当前线程id和类的prototype_header相或,这样得到的markOop为(当前线程id + prototype_header中的(epoch + 分代年龄 + 偏向锁标志 + 锁标志位)),注意prototype_header的分代年龄那4个字节为0
orptr(tmp_reg, r15_thread);
// 将上面计算得到的结果与锁对象的markOop进行异或,tmp_reg中相等的位全部被置为0,只剩下不相等的位。
xorptr(tmp_reg, swap_reg);
Register header_reg = tmp_reg;
// 对((int) markOopDesc::age_mask_in_place)进行按位取反,age_mask_in_place为...0001111000,取反后,变成了...1110000111,除了分代年龄那4位,其他位全为1;
// 将取反后的结果再与header_reg相与,这样就把header_reg中除了分代年龄之外的其他位取了出来,即将上面异或得到的结果中分代年龄给忽略掉。
andptr(header_reg, ~((int) markOopDesc::age_mask_in_place));
// 如果除了分代年龄,对象的markOop和(当前线程id+其他位)相等,那么上面与操作的结果应该为0,表明对象之前已经偏向当前线程,即markOop中存放有当前线程id,那么跳到done处,直接进入方法执行即可;否则表明当前线程还不是偏向锁的持有者,会接着往下走。
jcc(Assembler::equal, done);
3:走到这,表明锁对象并没有偏向当前线程,接下来判断是否需要撤销锁对象的偏向。
// 将header_reg和111相与,如果结果不为0,则表明header_reg后三位存在不为0的位,证明之前进行异或时,类的prototype_header后面三位与对象markOop的后三位不相等,但是能走到这,表明对象markword后三位为101,即偏向模式。现在类的prototype_header和对象markOop后三位不相等,即对象所属类不再支持偏向,发生了bulk_revoke,所以需要对当前对象进行偏向锁的撤销;否则表明目前该类还支持偏向锁,接着往下走。
testptr(header_reg, markOopDesc::biased_lock_mask_in_place);
jccb(Assembler::notZero, try_revoke_bias);
4:走到这,表明锁对象还支持偏向锁,需要判断当前对象的epoch是否合法,如果不合法,需要取进行重偏向。合法的话接着往下走。
// 测试对象所属类的prototype_header中epoch是否为0,不为0的话则表明之前异或时,类的prototype_header中epoch和对象markOop的epoch不相等,表明类在对象分配后发生过bulk_rebais()(前面提到过,每次发生bulk_rebaise,类的prototype header中的epoch都会+1),所以之前对象的偏向就无效了,需要进行重偏向。否则接着往下走。
testptr(header_reg, markOopDesc::epoch_mask_in_place);
jccb(Assembler::notZero, try_rebias);
5:走到这,表明锁对象的偏向态合法,可以尝试去获取锁,使对象偏向当前线程。
// 取出对象markOop中除线程id之外的其他位
andptr(swap_reg,
markOopDesc::biased_lock_mask_in_place | markOopDesc::age_mask_in_place | markOopDesc::epoch_mask_in_place);
// 将其他位移动至 tmp_reg。
movptr(tmp_reg, swap_reg);
// 将其他位和当前线程id进行或,构造成一个新的完整的32位markOop,存入tmp_reg中。新的markOop因为保存了当前线程id,所以会偏向当前线程。
orptr(tmp_reg, r15_thread);
// 尝试利用CAS操作将新构成的markOop存入对象头的mark_addr处,如果设置成功,则获取偏向锁成功。
// 这里说明下,cmpxchgptr操作会强制将rax寄存器(swap_reg)中内容作为老数据,与第二个参数,在这里即mark_addr处的内容进行比较,如果相等,则将第一个参数的内容,即tmp_reg中的新数据,存入mark_addr。
cmpxchgptr(tmp_reg, mark_addr); // compare tmp_reg and swap_reg
// 上面CAS操作失败的情况下,表明对象头中的markOop数据已经被篡改,即有其他线程已经获取到偏向锁,因为偏向锁不容许多个线程访问同一个锁对象,所以需要跳到slow_case处,去撤销该对象的偏向锁,并进行锁升级。
if (slow_case != NULL) {
jcc(Assembler::notZero, *slow_case);
}
// 上面CAS成功的情况下,直接就跳往done处,回去执行方法的字节码了。
jmp(done);
6:其实到这里,biased_locking_enter()已经结束了,不过上面多处提到了try_rebais和try_revoke,这两个其实就是汇编里的标号,它们对应的代码也定义在biased_locking_enter中。
bind(try_rebias);
// 将锁对象所属类的prototype_header送入tmp_reg。
load_prototype_header(tmp_reg, obj_reg);
// 尝试用CAS操作,使对象的markOop重置为无线程id的偏向锁态,即不偏向任何线程。
cmpxchgptr(tmp_reg, mark_addr);
// 和第5步一样,如果CAS失败,则表明对象头的markOop数据已经被其他线程更改,需要跳往slow_case进行撤销偏向锁,否则跳往done处,执行字节码。
if (slow_case != NULL) {
jcc(Assembler::notZero, *slow_case);
}
jmp(done);
bind(try_revoke_bias);
// 走到这,表明这个类的prototype_header中已经没有偏向锁的位了,即这个类的所有对象都不再支持偏向锁了,但是当前对象仍为偏向锁状态,所以我们需要重置下当前对象的markOop为无锁态。
// 将锁对象所属类的prototype_header送入tmp_reg。
load_prototype_header(tmp_reg, obj_reg);
// 尝试用CAS操作,使对象的markOop重置为无锁态。这里是否失败无所谓,即使失败了,也表明其他线程已经移除了对象的偏向锁标志。
cmpxchgptr(tmp_reg, mark_addr);
//接下来会回到lock_object()方法中,继续轻量级锁的获取。
五:总结
上面根据同步方法讲了一下偏向锁,笔者在这上面也啃了差不多整个周六,原理看似很简单,但是在很多细节不清楚的情况下去看源码,尤其是这种全是汇编代码时,往往是一脸懵逼。而且HotSpot用一个并不是对象的markOop去表示锁,涉及到计算时更让人糊涂。如果大家只是想稍微了解下原理,建议还是不要太深入源码细节。。。。
网友评论