引言
Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意。
原因是:monitorenter与monitorexit这两个控制多线程同步的bytecode原语是JVM依赖操作系统互斥(mutex)来实现的。互斥是一种会导致线程挂起、并在较短的时间内又需要重新调度回原线程的较为消耗资源的操作。
所以为了优化Java的Lock机制,从Java6开始引入了轻量级锁的概念。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了CPU原语Compare-And-Swap(CAS,汇编指令CMPXCHG),尝试在进入互斥前,进行补救。
轻量级锁
Java Object Model中定义,Object Header是一个2word(1 word = 4 byte)长度的存储区域。
第一个word长度的区域用来标记同步,GC以及hash code等,官方称之为 mark word 。
第二个word长度的区域是指向到对象的Class。
在2个word中,mark word是轻量级锁实现的关键。它的结构见下表:
上表显示,标志位为00则代表轻量级锁,此时其余bit表示指向lock record的指针。
lock record其实是一块分配在线程堆栈上的空间区域 。用于CAS前,拷贝object上的mark word。
那么为什么要拷贝mark word呢?
其实很简单,原因是为了不想在lock与unlock这种底层操作上再加同步。引用别人的图来说明整个流程:
在拷贝完object mark word之后,JVM做了一步 交换指针的操作 ,即流程中第一个橙色矩形框内容所述。
将object mark word里的轻量级锁指针指向lock record所在的stack指针,作用是让其他线程知道,该object monitor已被占用。
lock record里的owner指针指向object mark word的作用是为了在接下里的运行过程中,识别哪个对象被锁住了。
在unlock中,我们发现,JVM同样使用了CAS来验证object mark word在持有锁到释放锁之间,有无被其他线程访问。如果其他线程在持有锁这段时间里,尝试获取过锁,则可能自身被挂起,而mark word的重量级锁指针也会被相应修改。此时,unlock后就需要唤醒被挂起的线程。
偏向锁
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。(偏向锁只能在单线程下起作用)
通过下图可以更直观的理解偏向锁:
- 作用:偏向锁是为了消除无竞争情况下的同步原语,进一步提升程序性能。
- 与轻量级锁的区别:轻量级锁是在无竞争的情况下使用CAS操作来代替互斥量的使用,从而实现同步;而偏向锁是在无竞争的情况下完全取消同步。
- 与轻量级锁的相同点:它们都是乐观锁,都认为同步期间不会有其他线程竞争锁。
- 原理:当线程请求到锁对象后,将锁对象的状态标志位改为01,即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的Mark Word中。以后该线程可以直接进入同步块,连CAS操作都不需要。但是,一旦有第二条线程需要竞争锁,那么偏向模式结束,进入轻量级锁的状态。
- 优点:偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竞争,那偏向锁就是多余的。
- 偏向锁可以通过虚拟机的参数来控制它是否开启。
偏向锁使用注意
在JDK6之后,偏向锁是默认启用的。它提高了单线程访问同步资源的性能。
但如果同步资源或代码一直都是多线程访问(竞争频繁)的,那么消除偏向锁这一步骤就是多余的。事实上,消除偏向锁的开销还是蛮大的。
此时可以选择禁用偏向锁 -XX:-UseBiasedLocking 来提高性能。
偏向锁撤销会导致STW,如果发生频繁的偏向锁撤销,会导致应用程序较长时间STW。
为什么频繁的偏向锁撤销会导致STW时间增加呢?
因为偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");
//非fast_enter的调用可能会走if或者else if分支,此时attempt_rebias都是false,即这种情形下都是撤销偏向锁
markOop mark = obj->mark();
if (mark->is_biased_anonymously() && !attempt_rebias) {
//如果obj的偏向锁未被占有且不需要获取偏向锁,则尝试将其恢复成默认的
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()) {
//如果obj的偏向锁已经被占用了或者attempt_rebias为true
Klass* k = obj->klass();
//prototype_header变更只能在安全点下,变更时会将位于栈上的该klass的所有锁对象oop都做同样的变更
//如果某个锁对象oop不在当时的调用栈帧中,则可能出现不一致
markOop prototype_header = k->prototype_header();
if (!prototype_header->has_bias_pattern()) {
//如果prototype_header中没有偏向锁标识
markOop biased_value = mark;
//将当前对象的对象头原子的修改成prototype_header,将其恢复成无锁状态
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
return BIAS_REVOKED;
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
//如果两个对象头的epoch的值不等,说明更新prototype_header的epoch值时该对象不在任何Java线程栈帧中,即该对象的偏向锁实际已经释放了
if (attempt_rebias) {
//如果需要重新偏向
assert(THREAD->is_Java_thread(), "");
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;
}
} else {
//attempt_rebias为false,则将对象头恢复成无锁状态
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;
}
}
}
}
//进入此逻辑的有以下几种情形:目标对象处于无锁状态;目标对象有偏向锁,其Klass的prototype_header也有偏向锁,且两者epoch一致。
//如果是并发抢占偏向锁,则抢占失败的线程会进入此逻辑,通过VM_RevokeBias将偏向锁膨胀成轻量级锁,
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);
//将cached_monitor_info置为NULL,revoke_bias方法会设置该属性
((JavaThread*) THREAD)->set_cached_monitor_info(NULL);
assert(cond == BIAS_REVOKED, "why not?");
return cond;
} else {
//如果占用偏向锁的不是当前线程,底层也是调用revoke_bias,撤销偏向锁或者将其膨胀成轻量级锁
//因为是修改其他线程占用的obj,为了安全需要在安全点下执行,借此暂停持有该偏向锁的线程,注意这个任务并不是立即执行,而是有一个短暂的延时
VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);
VMThread::execute(&revoke);
return revoke.status_code();
}
}
assert((heuristics == HR_BULK_REVOKE) ||
(heuristics == HR_BULK_REBIAS), "?");
//执行批量撤销或者重偏向,底层调用bulk_revoke_or_rebias_at_safepoint
//同一个Klass的多个锁对象oop累计调用update_heuristics超过一定次数就会返回HR_BULK_REBIAS,通过VM_BulkRevokeBias将Klass和对应的多个位于调用栈帧中的锁对象oop的epoch值增加,这样没有为调用栈帧中也是该Klass的锁对象oop就会被用于重新获取偏向锁,减少update_heuristics的调用
//如果调用更加频繁,则返回HR_BULK_REVOKE,将该Klass的prototype_header和对应的多个位于调用栈帧中的锁对象oop恢复成无锁状态,因为频繁的执行此逻辑说明当前的并发调用场景已经不适用于偏向锁了
VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,
(heuristics == HR_BULK_REBIAS),
attempt_rebias);
VMThread::execute(&bulk_revoke);
return bulk_revoke.status_code();
}
禁用偏向锁优化
已知在高并发系统中锁竞争激烈的情况下,偏向锁(偏向锁原理见参考wiki)可能会频繁得撤销,升级成轻量级锁或者是重量级锁。
但是撤销偏向锁需要等到全局安全点才能进行,安全点会导致应用系统STW(Stop The World),进而使得性能下降。在这种情况下应当禁用偏向锁,减少频繁撤销偏向锁带来的额外损耗。
分析偏向锁带来的STW过程
增加如下JVM参数
-XX:+PrintGCApplicationStoppedTime # 打印应用程序停顿时间(输出位置在gc.log, 挨着gc日志的输出即为gc引起的停顿,其他则不一定是gc引起)
-XX:+PrintGCApplicationConcurrentTime # 打印应用程序已运行时间(输出在gc.log,一般配合PrintGCApplicationStoppedTime参数使用)
-XX:+PrintSafepointStatistics # 打印安全点统计信息
-XX:PrintSafepointStatisticsCount=1 # 配合PrintSafepointStatistics参数使用, 统计信息输出到LogFile中, 不是gc.log
-XX:+UnlockDiagnosticVMOptions # 为了使LogVMOutput生效
-XX:+LogVMOutput
-XX:LogFile=/tmp/vm.log
分析由于撤销偏向锁造成的应用程序停顿时间:
# 从vm.log中获取RevokeBias的时间
grep RevokeBias vm.log | awk '{split($1, a, ":"); print a[1]}' >> revoke_bias_times.txt
# 从gc.log中根据发生RevokeBias的时间关联出停顿时间日志
cat revoke_bias_times.txt | while read line; do grep " $line:" gc.log | grep "Stopping threads took" >> revoke_bias_stoptimes.txt; done
# 打印应用程序运行时间:停顿时长
awk 'BEGIN {print "ApplicationTime\tStopTime"} {print $2 "\t\t" $11 + $16}' revoke_bias_stoptimes.txt >> revoke_bias_stoptimes_data.txt
# 按秒统计停顿时间
awk 'NR>1 {split($1, a, "."); res[a[1]] += $2} END{slen = asorti(res, k); for (i = 1; i <= slen; i++) {print k[i] "\t" res[k[i]]}}' revoke_bias_stoptimes_data.txt | sort -n