JVM锁简介:偏向锁、轻量级锁和重量级锁
转自:https://www.aimoon.site/blog/2018/05/21/biased-locking/
比较复杂,简略见另一篇:https://www.cnblogs.com/twoheads/p/10148598.html
JVM中的java对象头
注意:在没有特殊说明的情况下,都是32 bits为例。
上一小节主要介绍了java中synchronized
关键字的使用方法,而在这一小节中将介绍一下synchronized
在JVM中的实现基础——java对象头中的Mark Word
。
表1 Java对象头的长度
内容 | 说明 | 备注 |
---|---|---|
Mark Word | 存储对象的Mark Word信息 | - |
Class Metadata Address | 存储指向对象存储类型的指针 | - |
Array Length | 数组的长度 | 只有数组对象有该属性 |
synchronized
使用的锁是存放在Java对象头中的Mark Word中,OpenJDK中的markOop.hpp 头文件详细介绍了Mark Word的内容,下面将分析32 bits的JVM中的Mark Word的构成。
表2 32位JVM的Mark Word存储结构
锁状态 | 23 bits | 2 bits | 4 bits | 1 bit | 2 bits |
---|---|---|---|---|---|
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
无锁状态 | hash code | 分代年龄 | 0 | 01 | |
偏向锁 | Thread ID | epoch | 分代年龄 | 1 | 01 |
重量级锁 | 指向监视器(monitor)的指针 | 10 | |||
GC标记 | 0 | 11 |
注:最后两位为锁标记位,倒数第三位是偏向标记,如果是1表示是偏向锁;合并单元格的位数就是 该字段的位数,例如hash code共25(23+2)位。
另外,对于偏向锁,如果Thread ID = 0,表示未加锁
JVM锁的类型及对比
Java 1.6对synchronized
进行了大幅度的优化,其性能也有了大幅度的提升。Java 1.6引入 “偏向锁”和“轻量级锁”的概念,减少了获得锁和释放锁的消耗。在Java 1.6之后,加上原有的重量 级锁,锁一共有4种状态,分别是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。锁只能 按照上述的顺序进行升级操作,锁只要升级之后,就不能降级。
下面将分别介绍一下偏向锁、轻量级锁和重量级锁,并探索一下偏向锁升级为轻量级锁(revoke bias
) 的流程和轻量级锁升级为重量级锁(inflate
)的流程。偏向锁、轻量级锁的状态转化及对象 Mark Word的关系如下图所示。图片来源:Synchronization and Object Locking文章中的配图
图1 偏向锁、轻量级锁的状态转化及对象Mark Word的关系
1. 偏向锁
偏向锁是Java 1.6新添加的内容,并且是jdk默认启动的选项,可以通过-XX:-UseBiasedLocking
来关闭偏向锁。另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0
来关闭延迟。
如果JVM支持偏向锁,那么将按照下图所示的流程分配对象,加偏向锁。图片来源:Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing第3页的配图
图2 偏向锁中的Mark Word的状态转化图
注意:这是简化版的流程图,因为偏向锁的图中缺少了epoch
字段。
1.1 偏向锁的加锁
如果JVM支持偏向锁,那么在分配对象时,分配一个可偏向而未偏向的对象(Mark Word的最后3位 为101,并且Thread ID
字段的值为0)。
然后,当一个线程访问同步块并获取锁时,将通过CAS(Compare And Swap)来尝试将对象头中的 Thread ID
字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出 同步块时,就不需要使用CAS来获取锁,只是简单的测试一个对象头中的Mark Word字段中是否存储 着指向当前线程的偏向锁;如果使用CAS设置失败时,说明存在锁的竞争,那么将执行偏向锁的撤销操作 (revoke bias
),将偏向锁升级为轻量级锁。
注:代码请查看biasedLocking.cpp中的revoke_and_rebias方法。
1.2 偏向锁的升级
下面结合代码(有缩减)分析一下偏向锁升级为轻量级锁的过程,这里暂时不考虑批量撤销偏向 (bulk revocation
)的情况。详细代码请查看biasedLocking.cpp中的revoke_bias方法。 偏向锁的撤销操作需要在全局检查点(global safepoint
)执行,在全局检查点上没有 线程执行字节码。
注:偏向锁的撤销的入口函数是biasedLocking.cpp中的revoke方法, 之后会通过VMThread
调用revoke_bias方法。
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias,
bool is_bulk, JavaThread* requesting_thread) {
markOop mark = obj->mark();
// 检查是否可偏向
if (!mark->has_bias_pattern()) {
return BiasedLocking::NOT_BIASED;
}
uint age = mark->age();
markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age);
markOop unbiased_prototype = markOopDesc::prototype()->set_age(age);
JavaThread* biased_thread = mark->biased_locker();
if (biased_thread == NULL) {
// 可偏向但是未偏向的情况
// 可能的使用场景为:因计算hash code而撤销偏向
if (!allow_rebias) {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
// 判断对象现在偏向的线程是否还存在
// 即对象头中Mark Word中Thread ID字段指向的线程是否存在
bool thread_is_alive = false;
if (requesting_thread == biased_thread) {
// 请求的线程拥有偏向锁
thread_is_alive = true;
} else {
// 请求的线程不拥有偏向锁,递归查询
for (JavaThread* cur_thread = Threads::first();
cur_thread != NULL; cur_thread = cur_thread->next()) {
if (cur_thread == biased_thread) {
thread_is_alive = true;
break;
}
}
}
if (!thread_is_alive) {
if (allow_rebias) {
obj->set_mark(biased_prototype);
} else {
obj->set_mark(unbiased_prototype);
}
return BiasedLocking::BIAS_REVOKED;
}
// 拥有偏向锁的线程仍然存活
// 检查该线程是否拥有锁:
// 如果拥有锁,那么需要升级为轻量级锁,然后将displaced mark word复制到线程栈中;
// 如果不再拥有锁,如果允许重偏向,那么将mark word中的Thread ID 重新置0;
// 如果不允许重偏向,那么将mark work设置为无锁状态,即最后两位为01
// cached_monitor_info 是该线程拥有的锁对象的信息,按照从加锁顺序的逆序排列
GrowableArray<MonitorInfo*>* cached_monitor_info =
get_or_compute_monitor_info(biased_thread);
BasicLock* highest_lock = NULL;
for (int i = 0; i < cached_monitor_info->length(); i++) {
MonitorInfo