线程并发系列文章:
Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
上篇文章已经分析了Java对象头构成、源码及其对象头的调试,本篇将分析偏向锁、轻量级锁、重量级锁的实现及其演变过程。由于涉及到c++源码,估计不少同学没兴趣看,因此重点多以图+源码辅助分析。
通过本篇文章,你将了解到:
1、什么是重量级锁
2、轻量级锁/偏向锁的由来
3、偏向锁的加锁、撤销锁、释放锁
4、轻量级锁的加锁、释放锁
5、偏向锁、轻量级锁、重量级锁的异同点
private synchronized void testLock() {
//doSomething();
}
现有两个线程(t1、t2)同时访问testLock()方法,假若t1先拿到锁并执行同步块里的代码。此时t2也要访问testLock()方法,但是因为锁被t1持有,因此t2只能阻塞等待t1释放锁。
此时,锁的形态称为重量锁。
由上面的例子可以看出,t2因为没有获取到锁然后挂起自己,等待t1释放锁后唤醒自己。线程的挂起/唤醒需要CPU切换上下文,此过程代价比较大,因此称此种锁为重量级锁。
线程挂起/唤醒请移步:Java Unsafe/CAS/LockSupport 应用与原理
还是上面的例子,假设现在t1、t2是交替执行testLock()方法,此时t1、t2没必要阻塞,因为它们之间没有竞争,也就是不需要重量级锁。
线程之间交替执行临界区的情形下使用的锁称为轻量级锁。
轻量级锁相比重量级锁的优势:
1、每次加锁只需要一次CAS
2、不需要分配ObjectMonitor对象
3、线程无需挂起与唤醒
依旧是上面的例子,假设testLock()始终只有一个线程t1在执行呢?这个时候若是使用轻量级锁,每次t1获取锁都需要进行一次CAS,有点浪费性能。
因此就出现了偏向锁:
当锁偏向某个线程时,该线程再次获取锁时无需CAS,只需要一个简单的比较就可以获取锁,这个过程效率很高。
偏向锁相比轻量级锁的优势:
同一个线程多次获取锁时,无需再次进行CAS,只需要简单比较。
上面阐述了这三种锁的由来,这些锁是如何实现的呢?接下来从源码的角度进行分析。这三种锁的基础是对象头,关于对象头的详细分析请查看:Java 对象头分析与使用(Synchronized相关)
**锁的本质是共享变量,因此问题的关键是如何访问这个共享变量。**理解这个对于理解三种锁的演变事半功倍,接下来将重点突出这一信息。
既然涉及到了锁,那么自然而然有加锁/释放锁操作,偏向锁比较特殊还多了个撤销锁的操作。
先来复习对象头:
可以看到偏向锁里存储了偏向线程的id,epoch,偏向锁标记(biased_lock),锁标记(lock)等信息。这些信息统称为Mark Word。
在看源码之前,先来朴素(脑补)地猜测线程t1获取偏向锁的过程:
1、先判断Mark Word里的线程id是否有值。
1.1、如果没有,说明还没有线程占用锁,则直接将t1的线程id记录到Mark Word里。可能会存在多个线程同时修改Mark Word,因此需要进行CAS修改Mark Word。
1.2、如果已有id值,那么判断分两种情况:
1.2.1、该id是t1的id,则此次获取锁是个重入的过程,直接就获取了。
1.2.2、如果该id不是t1的id,说明已经有其它线程获取了锁,t1想要获取锁就需要走撤销流程。
来看看源码:bytecodeInterpreter.cpp
CASE(_monitorenter): {
//获取对象头,用oop表示对象头 -------->(1)
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
//遍历线程栈,找到对应的空闲的BasicObjectLock (2)
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
if (entry != NULL) {
//BasicObjectLock _obj字段指向oop ------>(3)
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
//取出对象头里的Mark Word
markOop mark = lockee->mark();
intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 支持偏向锁
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
//当前的线程id
thread_ident = (uintptr_t)istate->thread();
//异或运算结果-------->(4)
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
if (anticipated_bias_locking_value == 0) {
//完全相等,则认为是重入了该锁------>(5)
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
//不支持偏向锁了-------->(6)
//构造无锁的Mark Word
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
//CAS 修改Mark Word为无锁状态
if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
//epoch 过期了------->(7)
//使用当前线程id构造偏向锁
markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
//CAS修改
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
//成功则获取了锁
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
}
else {
//否则进行下一步
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
//构造匿名偏向锁---------(8)
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);)
//CAS 修改为偏向当前线程的锁
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
//不成功则进行下一步
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
}
if (!success) {
//上面尝试使用偏向锁,可惜没有成功,则尝试升级为轻量级锁
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
// Is it simple recursive case?
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
entry->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
代码看起来很多,重点说明标注的(1)~(9)个点:
(1)
oop 表示对象头,里边包括含了Mark Word、Klass Word。
(2)
在basicLock.hpp里,BasicObjectLock 结构如下:
#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
//BasicLock
BasicLock _lock;
//对象头
oop _obj;
...
};
继续看BasicLock:
#basicLock.hpp
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
//存储Mark Word
volatile markOop _displaced_header;
...
};
BasicObjectLock 即是熟知的Lock Record的实现,其包含了两个内容:
1、存储Mark Word的_displaced_header
2、指向对象头的指针:_obj
(3)
将Lock Record里的_obj赋值为lockee,也就是_obj表示的是对象头。
(4)
从对象头(lockee)里取出Klass Word,该Word是指向Klass类型的指针,Klass类里有个名为_prototype_header字段, 也是表示Mark Word,里面存储着epoch、偏向锁标记等信息(后面为方便描述,使用Klass替代说明)。此处是取出这些信息并拼接上当前线程id,进而和对象头里的Mark Word进行异或运算,找出不相等的位,接下来就是判断具体Mark Word的哪个部分不相等,从而有不同的处理逻辑。
(5)
如果上面的异或相等,那说明Mark Word里存储有当前线程id,epoch、偏向锁标记都一致,也就是锁被当前线程持有了,此次是个重入的过程。因为已经拥有锁了,所以啥也不干了。
(6)
发现Mark Word里的偏向锁标志位和Klass 里的不同,而Mark Word之前已经判断是偏向锁了,因此可以推断Klass 已经不支持偏向锁了。既然不支持偏向锁了,就修改Mark Word为无锁状态,等待后面升级为轻量级锁/重量级锁。
(7)
发现Mark Word里的epoch与Klass里的不同,则认为发生了批量重偏向,因此可以直接修改Mark Word偏向当前线程。
(8)
如果上述条件都不满足,则认为当前是匿名偏向锁(是偏向锁,但是没有偏向任何线程)。尝试直接修改Mark Word偏向当前线程。
通过上述步骤的分析,结果比较明显了:
1、线程每次尝试获取锁都需要关联Lock Record,并将_obj指向对象头,此时Lock Record与对象头就建立了联系。
2、线程成功将线程id写入Mark Word后即表示该线程获取了该偏向锁
偏向锁状态时Lock Record与对象头关系:
此时_displaced_header字段并没有使用。
好了,再来回顾一下线程t1、t2获取偏向锁的过程:
1、t1获取锁,一开始锁是匿名偏向锁,所以走的是上图步骤4,成功获取锁。
2、t1再次获取锁,因为之前已经获取到锁了,所以走的是上图步骤1,重入获取锁。
3、此时t2尝试获取锁,因为t1正在持有锁,因此走的是上图步骤5。
1、4、5 步骤情景已经涉及到了,剩下2、3步骤下面分析。
偏向锁获取不成功,那么在升级为轻量级锁之前先将锁变为无锁状态,此为偏向锁的撤销过程。
来看看源码入口:
#InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
...
if (UseBiasedLocking) {
//使用偏向锁则进入快速处理流程
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
//升级为轻量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
...
#synchronizer.cpp
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) ;
}
可以看出撤销分为在安全点撤销和非安全点撤销。
重点说一下非安全点撤销:revoke_and_rebias
里面代码比较多,就不一一贴出了。
用图表示如下:
上面的撤销是在不安全点执行的,因此都会有CAS操作。
上图进行了初步的撤销/重偏向,若是成功则后续会升级为轻量级锁;若是失败,则需要进一步地撤销。
批量重偏向/批量撤销逻辑最终也会调用直接撤销函数,继续来看看直接撤销的流程,实际上就是biasedLocking.cpp#revoke_bias 函数:
可以看出,上图修改Mark Word时并没有使用CAS,因为执行这段代码是在安全点执行的,也就是说只要执行了就能成功。
经过上面的分析,我们知道:
1、当某个线程持有偏向锁,另一个线程想要获取锁时需要撤销锁。
2、撤销先尝试在不安全点使用CAS修改Mark Word为无锁状态,若还是无法撤销则考虑在安全点撤销,等安全点是比较低效的操作。
因此偏向锁引入了批量重偏向与批量撤销。
当某个类的对象锁撤销次数达到一定阈值,比如达到了20次,那么就会触发批量重偏向的逻辑,修改Klass里的epoch值,并修改当前正在使用该类型锁Mark Word里的epoch值。当线程想要获取偏向锁时,对比当前对象的epoch值与Klass里的epoch值,发现不相等,则认为过期。此时该线程被允许直接CAS修改Mark Word偏向当前线程,就不用再走撤销逻辑了。这部分对应最初分析偏向锁入口的标记(7)。
同样的当撤销次数达到40次时,认为该对象已经不适合应用偏向锁了,因此会修改Klass里的偏向锁标记,更改为不支持偏向锁。当线程想要获取偏向锁时,检查Klass里的偏向锁标记值,若是不允许偏向,说明之前发生了批量撤销,因此该线程被允许直接CAS修改Mark Word为无锁状态,就不用再走撤销逻辑了。这部分对应最初分析偏向锁入口的标记(6)。
批量重偏向与批量撤销是对偏向锁性能的优化。
正常的想法是:当线程退出临界区,也就是释放了锁。
#bytecodeInterpreter.cpp
CASE(_monitorexit): {
//对象头
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
//遍历线程栈
while (most_recent != limit ) {
//找到对应的Lock Record
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
//设置Lock Record 里的_obj字段 为null
most_recent->set_obj(NULL);
//此处是轻量级锁的释放,先省略
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
most_recent++;
}
...
}
偏向锁对象头和Lock Record关系前面已经分析:每次尝试获取偏向锁时,先找到空闲的Lock Record,并将Lock Record里的_obj指向对象头,表示这俩建立起了联系。释放锁的时候将这联系切断,_obj=null。
你也许已经发现了Mark Word并没有发生改变,依然是偏向了之前的线程,那还是没释放锁的嘛。的确是,线程退出临界区时候,并没有释放偏向锁,这么做的目的是:
当再次需要获取锁的时候,只需要简单按位运算判断是否是重入,即可快速获取锁,而不用每次都CAS,这也是偏向锁在只有一个线程访问锁的情景下高效的核心所在。
前边花了很大篇幅阐述偏向锁,看起来很复杂,实际上就是撤销部分比较复杂。
1、偏向锁的"锁"即是Mark Word,想要获取锁就需要对Mark Word进行修改,可能会有多线程竞争修改,因此需要借助CAS。
2、因为撤销操作可能需要在安全点执行,效率比较低,多次撤销更会影响效率,因此引入了批量重偏向与批量撤销。
3、偏向锁的重入计数依靠线程栈里Lock Record个数。
4、偏向锁撤销失败,最终会升级为轻量级锁。
5、偏向锁退出时并没有修改Mark Word,也就是没有释放锁。
偏向锁的撤销操作比较复杂,而轻量级锁的加锁、释放锁则简单得多。
#synchronizer.cpp
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
//取出Mark Word
markOop mark = obj->mark();
//走到此说明已经不是偏向锁了
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
if (mark->is_neutral()) {
//如果是无锁状态
//将Mark Word拷贝到Lock Record的_displaced_header 字段里
lock->set_displaced_header(mark);
//CAS修改Mark Word使之指向Lock Record
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())) {
//mark->has_locker() -->表示已经是轻量级锁
//THREAD->is_lock_owned((address)mark->locker()) 并且是当前线程获取了轻量级锁
//这两点说明当前线程重入该锁
//直接设置header==null
lock->set_displaced_header(NULL);
return;
}
//走到这说明不能使用轻量级锁,则需要升级为重量级锁
此时,我们发现Lock Record与轻量级锁的关系更加紧密。
偏向锁了没用的_displaced_header用上了,用以存储无锁状态的Mark Word,待释放锁时恢复(保留了hash值等)。
而Mark Word里的锁记录指针指向了Lock Record,表示该Lock Record所在的线程获取了轻量级锁。
#bytecodeInterpreter.cpp
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
while (most_recent != limit ) {
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
most_recent->set_obj(NULL);
//以上部分和偏向锁释放一致的
if (!lockee->mark()->has_bias_pattern()) {
//不是偏向模式
bool call_vm = UseHeavyMonitors;
//header 不为空,说明是线程第一次获取轻量级锁时占用的Lock Record
if (header != NULL || call_vm) {
//而header存储的是无锁状态的Mark Word
//因此需要将Mark Word修改恢复为之前的无锁状态
if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
//失败的话,再将obj设置上,为了重量级锁使用
most_recent->set_obj(lockee);
CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
most_recent++;
}
...
}
与偏向锁不同的是,轻量级锁是真的释放了锁,因为修改Mark Word为无锁状态了。
你可能会有疑惑:只有拿到锁的线程才会有释放锁的操作,为什么此处还需要CAS呢?
考虑一种情况:线程A获取了轻量级锁,此时线程B也想要获取锁,由于锁被A占用,因此B将锁膨胀为重量级锁(修改了Mark Word)。而此时A还在执行临界区代码,它并不知道Mark Word已经被更改了。所以当A退出临界区释放锁的时候,它不能直接修改Mark Word,于是使用CAS尝试更新Mark Word。若是Mark Word已经改变了,也就是说之前Mark Word是指向线程A的Lock Reocrd指针,现在是指向ObjectMonitor了,当然A的CAS会失败,接着进行下一步判断,最终可能膨胀为重量级锁。若是没有改变,A释放轻量级锁就直接成功了。
1、轻量级锁的"锁"即是Mark Word,想要获取锁就需要对Mark Word进行修改,可能会有多线程竞争修改,因此需要借助CAS。
2、如果初始锁为无锁状态,则每次进入都需要一次CAS尝试修改为轻量级锁,否则判断是否重入。
3、如果不满足2的条件,则膨胀为重量级锁。
4、轻量级锁退出时即释放锁,变为无锁状态。
5、可以看出轻量级锁比较敏感,一旦有线程竞争就会膨胀为重量级锁。
由上面的分析可知,想要在不安全点获取锁,就得依靠CAS操作,因此理解CAS的原理是深入锁的基础。有关CAS原理与使用请移步:Java Unsafe/CAS/LockSupport 应用与原理
1、都需要和Lock Record关联;偏向锁和重量级锁只用到了_obj字段,而轻量级锁用到了_displaced_header。
2、释放锁时都需要修改Lock Record 里的_obj字段。
1、偏向锁和轻量级锁的"锁"即是Mark Word,而重量级锁的"锁"是ObjectMonitor,此时Mark Word保留了指针指向ObjectMonitor。
2、偏向锁和轻量级锁依靠Lock Record个数来记录重入的次数,而重量级锁通过
ObjectMonitor里的_recursions 整形变量记录。
3、偏向锁和轻量级锁的重入只需要做简单的判断即可,而重量级锁需要通过CAS判断是否是重入。
1、偏向锁适合在只有一个线程访问锁的场景,在此种场景下,线程只需要执行一次CAS获取偏向锁,后续该线程再次访问该锁时仅仅只需要简单的判断即可获取锁。
2、轻量级锁适合在有多个线程交替访问锁,并且不会发生竞争的场景。此种场景下,线程每次获取锁只需要执行一次CAS即可。
3、重量级锁适合在多线程竞争环境下访问锁,执行临界区的时间比较长,未获取锁的线程将会被挂起,等待拥有锁的线程释放锁而后唤醒它。此种场景下,线程每次都需要进行多次CAS操作,操作失败将会被放入队列里等待唤醒。
偏向锁、轻量级锁是在Java1.6(Java 6)提出的用以对重量级锁的改进。
从上面分析我们也知道为了实现偏向锁的撤销,引入了复杂的同步代码,包括在安全点执行等操作,且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构。**因此,在Java 15废弃了偏向锁。**https://openjdk.java.net/jeps/374。
至此,偏向锁、轻量级锁的原理已经阐述完毕,由于篇幅所限,重量级锁的原理下篇分析。
虽然尽量避免贴过多的代码,但还是无法避免贴了一些,不关注源码的同学请直接看每段的小结。若是对更多的源码细节感兴趣,可查看下面的链接,本篇也参考了以下链接:
https://github.com/farmerjohngit/myblog/issues/13
https://github.com/HenryChenV/my-notes/issues/3
本文源码基于jdk1.8。