前言
线程并发系列文章:
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各种锁(终极篇)
上篇文章分析了偏向锁、轻量级锁的演变过程,本篇将分析重头戏:重量级锁的原理。
通过本篇文章,你将了解到:
1、ObjectMonitor 的运用
2、锁的膨胀过程
3、重量级锁的加锁流程
4、重量级锁的解锁流程
5、重量级锁小结
6、与偏向锁、轻量级锁的比对
1、ObjectMonitor 的运用
我们知道当锁处在轻量级锁的状态时,Mark Word 存放着指向Lock Record指针,Lock Record是线程私有的。
而处在重量级锁状态时说明有线程没拿到锁需要阻塞等待锁,当拥有锁的线程释放锁后唤醒它继续竞争锁。此处就引入了一个问题:其它线程如何找到被阻塞的线程?我们很容易想到:把阻塞的线程放到多线程共享的(能访问)的列表里。
而Lock Record是线程私有的,显然不能满足需求。
因此,重量级锁引入了ObjectMonitor类。
如上图,Mark Word 存放着指向ObjectMonitor的指针,ObjectMonitor是线程间共享的并且拥有比Lock Record更多的信息。
来看看ObjectMonitor 记录的信息:
#ObjectMonitor.hpp
ObjectMonitor() {
//记录无锁状态的Mark Word
_header = NULL;
_count = 0;
//等待锁的线程个数
_waiters = 0,
//线程重入次数
_recursions = 0;
//指向的对象头
_object = NULL;
//锁的本身,指向线程或者Lock Record
_owner = NULL;
//调用wait()方法后等待锁的队列
_WaitSet = NULL;
//等待队列的锁
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
//ObjectWaiter 队列
_cxq = NULL ;
FreeNext = NULL ;
//ObjectWaiter 队列
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
可以看出,Lock Record里拥有的信息ObjectMonitor里也有,如存储Mark Word的_header字段,存储指向对象头的指针_object字段。当然,ObjectMonitor还有更丰富的信息,如获取锁失败存放阻塞线程的队列_cxq,调用wait()方法后等待的线程队列_WaitSet等。
2、锁的膨胀过程
知道有ObjectMonitor这个东西了,接下来看看如何使用它。
回顾之前的分析,偏向锁升级为轻量级锁时要修改Mark Word,使之指向Lock Record,轻量级锁升级为重量级锁时也需要修改Mark Word,使之指向ObjectMonitor。
而创建/获取ObjectMonitor 对象的过程即是锁的膨胀过程。
源码里的膨胀过程就是个inflate(xx)函数:
#synchronizer.cpp
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
...
//死循环,直到获取到ObjectMonitor为止
for (;;) {
//取出Mark Word
const markOop mark = object->mark() ;
//如果是重量级锁
if (mark->has_monitor()) {
//是重量级锁,说明肯定已经有现成的ObjectMonitor,直接用就好了
ObjectMonitor * inf = mark->monitor() ;
return inf ;
}
//正在膨胀的时候
if (mark == markOopDesc::INFLATING()) {
//继续循环,需要等待膨胀完成
continue ;
}
//如果当前是轻量级锁
if (mark->has_locker()) {
//分配ObjectMonitor对象
ObjectMonitor * m = omAlloc (Self) ;
//初始化一些参数
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class
//尝试将Mark Word更改为膨胀状态,此时Mark Word 全是0 --------->(1)
//可能会有多线程走到这,因此用CAS
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {
//修改失败,继续循环
omRelease (Self, m, true) ;
continue ; // Interference -- just retry
}
//若是修改成功,则取出之前轻量级锁存储的Mark Word
markOop dmw = mark->displaced_mark_helper() ;
//将Mark Word 搬到ObjectMonitor的_header字段里
m->set_header(dmw) ;
//_owner指向Lock Record,也就是设置锁的持有者是Lock Record------->(2)
m->set_owner(mark->locker());
//指向对象头
m->set_object(object);
//将Mark Word 指向ObjectMonitor------->(3)
object->release_set_mark(markOopDesc::encode(m));
...
//成功,则返回ObjectMonitor 对象
return m ;
}
//无锁状态
ObjectMonitor * m = omAlloc (Self) ;
//初始化一些参数
m->Recycle();
//直接记录mark
m->set_header(mark);
//_owner为空-------------------->(4)
m->set_owner(NULL);
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class
//将Mark Word修改为指向ObjectMonitor的指针-------------------->(5)
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
...
//失败,则重新尝试
continue ;
}
...
//成功,则返回ObjectMonitor 对象
return m ;
}
}
上述代码即为简化过的膨胀流程,标注了5个重点:
(1)
如果当前锁是轻量级锁,说明有线程正在持有该锁,尝试CAS修改锁为膨胀状态。
(2)
_owner不指向任何线程,指向的是Lock Reocrd,后续会有相应的判断。
(3)
轻量级锁时Mark Word存储着指向Lock Record的指针,而此时变为指向重量级锁的指针,也就是指向ObjectMonitor的指针。此处是单线程操作,因此可以直接设置。
markOopDesc::encode(m) 定义如下:
#markOop.hpp
static markOop encode(ObjectMonitor* monitor) {
intptr_t tmp = (intptr_t) monitor;
//Mark Word指向ObjectMonitor
return (markOop) (tmp | monitor_value);
}
(4)
如果当前锁是无锁状态,将_owner置空。
(5)
CAS尝试将Mark Word 指向ObjectMonitor。
以上就是膨胀的流程,用图表示如下:
3、重量级锁的加锁流程
初次尝试加锁
回顾偏向锁、轻量级锁加锁流程核心:修改Mark Word。
而在膨胀为重量级锁时也是修改了Mark Word,不同的是此过程并没有线程占用重量级锁。来看看重量级锁的抢占过程:
#ObjectMonitor.cpp
void ATTR ObjectMonitor::enter(TRAPS) {
//当前线程
Thread * const Self = THREAD ;
void * cur ;
//尝试修改_owner字段为当前线程,也就是尝试获取锁
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
//修改成功,则获取了重量级锁
return ;
}
//以下都是CAS失败后的处理
//如果当前_owner值为当前线程,则认为是重入了该锁
if (cur == Self) {
//重入次数+1,成功获取了锁
_recursions ++ ;
return ;
}
//_owner值为Lock Record,说明当前线程是之前轻量级锁的持有者
if (Self->is_lock_owned ((address)cur)) {
//重入次数为1次
_recursions = 1 ;
//改为当前线程
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
...
{
...
for (;;) {
//没有获取到锁,则执行该函数
EnterI (THREAD) ;
...
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
}
由上可知,enter(xx)函数主要做了如下事情:
先CAS尝试修改ObjectMonitor的_owner字段,会有几种结果:
1、锁没被其它线程占用,当前线程成功获取锁。
2、锁被当前线程占用,当前线程重入该锁,获取锁成功。
3、锁被LockRecord占用,而LockRecord又属于当前线程,属于重入,重入次数为1。
4、以上条件都不满足,调用EnterI()函数。
用图表示如下:
再次尝试加锁
初次获取锁失败后,会走到下面的流程,也就是EnterI()函数的实现:
#ObjectMonitor.cpp
void ATTR ObjectMonitor::EnterI (TRAPS) {
//当前线程
Thread * Self = THREAD ;
//尝试加锁----------->(1)
if (TryLock (Self) > 0) {
return ;
}
//尝试自旋加锁----------->(2)
if (TrySpin (Self) > 0) {
return ;
}
//构造ObjectWaiter 节点
ObjectWaiter node(Self) ;
//挂起/唤醒线程重置参数
Self->_ParkEvent->reset() ;
//前驱节点为无效节点
node._prev = (ObjectWaiter *) 0xBAD ;
//当前节点状态为CXQ,也就是说节点在_cxq队列里
node.TState = ObjectWaiter::TS_CXQ ;
ObjectWaiter * nxt ;
for (;;) {
node._next = nxt = _cxq ;
//将节点插入_cxq队列的头----------->(3)
if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
//尝试获取锁----------->(4)
if (TryLock (Self) > 0) {
return ;
}
}
...
for (;;) {
//再次尝试获取锁----------->(5)
if (TryLock (Self) > 0) break ;
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
}
//挂起线程----------->(6)
if (_Responsible == Self || (SyncFlags & 1)) {
//挂起有超时时间
Self->_ParkEvent->park ((jlong) RecheckInterval) ;
} else {
//挂起没有超时时间
Self->_ParkEvent->park() ;
}
//唤醒后再次获取锁,成功则退出循环----------->(7)
if (TryLock(Self) > 0) break ;
//...还是一些自旋策略
}
//将节点从_cxq或_EntryList里移除----------->(8)
UnlinkAfterAcquire (Self, &node) ;
...
return ;
}
上述代码标注了8点重点,来看看更详细的解释:
(1)
TryLock 顾名思义尝试获取锁:
#ObjectMonitor.cpp
int ObjectMonitor::TryLock (Thread * Self) {
for (;;) {
//for 循环名存实亡
void * own = _owner ;
//中途判断_owner是否已经被更改,若是则退出
if (own != NULL) return 0 ;
//还是尝试更新_owner
if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
return 1 ;
}
if (true) return -1 ;
}
}
(2)
TryLock 只执行一次CAS,而TrySpin顾名思义:自旋获取锁。
#ObjectMonitor.cpp
int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
...
for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
if (TryLock(Self) > 0) {
...
return 1 ;
}
//休息一下继续
SpinPause () ;
}
...
}
可以看出TrySpin里多次调用TryLock,次数是10次。源码里指出经验值20-100可能最佳。
(3)
此处是死循环,直到插入队列成功或者获取了锁。
此处是往_cxq写数据,并且它的_next指针指向_cxq,因此每次新节点都放在队列头。又因为可能存在多线程修改_cxq,因此需要CAS。
(4)
插入队列失败后,再尝试获取锁。
(5)
又是个死循环,先尝试获取锁。
(6)
至此,线程放弃获取锁的动作,将自己挂起了,线程阻塞于此处,等待别的线程唤醒它。
(7)
当某个线程唤醒在(6)被挂起的线程后,被唤醒的线程立即再尝试获取锁,如果还是失败了,则继续回到(5)的循环。
(8)
获取锁成功后,因为前边已经加入到队列了,因此需要将节点从队列(_cxq/_EntryList)移除。
通过上述(1)~(8)的分析可知,enterI()函数主要做了如下事情:
1、多次尝试加锁。
2、实在不行将线程包装后加入到阻塞队列里。
3、再尝试获取锁。
4、失败后将自己挂起。
5、被唤醒后继续尝试获取锁。
6、成功则退出流程,失败继续走上面的流程。
用图表示如下:
4、重量级锁的解锁流程
上面分析了加锁的过程,它有两种结果:
1、成功获取锁,那么可以执行临界区代码。
2、获取锁失败,挂起等待别人唤醒。
关于2思考一个问题:是谁唤醒了它,如何唤醒的?
先来看看1,线程执行完临界区代码后需要释放锁,偏向锁和轻量级锁的释放上篇文章已经分析:若是释放失败,则会走到重量级锁的释放流程。
重量级锁的释放流程,也就是exit()函数的实现:
#ObjectMonitor.cpp
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
//释放锁的线程不一定是重量级锁的获得者-------->(1)
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
//释放锁的线程是轻量级锁的获得者,先占用锁
_owner = THREAD ;
} else {
//异常情况
return;
}
}
if (_recursions != 0) {
//是重入锁,简单标记后退出
_recursions--;
return ;
}
...
for (;;) {
if (Knob_ExitPolicy == 0) {
//默认走这里
//释放锁,别的线程可以抢占了
OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock
OrderAccess::storeload() ; // See if we need to wake a successor
//如果没有线程在_cxq/_EntryList等待,则直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT (Inflated exit - simple egress) ;
return ;
}
//有线程在等待,再把之前释放的锁拿回来
if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
//若是失败,说明被人抢占了,直接退出
return ;
}
} else {
...
}
ObjectWaiter * w = NULL ;
int QMode = Knob_QMode ;
//此处省略代码
//根据QMode不同,选不同的策略,主要是操作_cxq和_EntryList的方式不同
//默认QMode=0
w = _EntryList ;
if (w != NULL) {
//_EntryList不为空,则释放锁---------(2)
ExitEpilog (Self, w) ;
return ;
}
//_EntryList 为空,则看_cxq有没有数据
w = _cxq ;
if (w == NULL) continue ;//没有继续循环
for (;;) {
//将_cxq头节点置空
ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ;
if (u == w) break ;
w = u ;
}
if (QMode == 1) {
...
} else {
// QMode == 0 or QMode == 2
//_EntryList指向_cxq
_EntryList = w ;
ObjectWaiter * q = NULL ;
ObjectWaiter * p ;
//该循环的目的是为了将_EntryList里的节点前驱连接起来---------(3)
for (p = w ; p != NULL ; p = p->_next) {
guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
//改为ENTER状态
p->TState = ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
}
w = _EntryList ;
if (w != NULL) {
//释放锁---------(4)
ExitEpilog (Self, w) ;
return ;
}
}
}
依旧是列出了4个点,exit()函数主要做了如下事情:
(1)
若膨胀的时候锁是轻量级锁,此时_owner指向Lock Record。当轻量级锁的占有者线程释放锁后会走到此,因此释放锁的线程不一定是重量级锁的获得者。
(2)
ExitEpilog (Self, w) 释放锁:
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
//从队列节点里取出ParkEvent
ParkEvent * Trigger = Wakee->_event ;
Wakee = NULL ;
//释放锁,将_owner置空
OrderAccess::release_store_ptr (&_owner, NULL) ;
OrderAccess::fence() ; // ST _owner vs LD in unpark()
//唤醒节点里封装的线程
Trigger->unpark() ;
}
release_store_ptr内部是汇编语句实现的原子操作。
(3)
之前_EntryList只用了后驱节点,也就是单向链表实现的队列,此处将前驱节点使用上了,也就是_EntryList变为双向链表了。
(4)
和(2)一样的作用,释放锁并唤醒对应的线程。
再来看看上面提出的2问题,从释放锁的流程已经得知:
当前占有锁的线程释放锁后会唤醒阻塞等待锁的线程
具体唤醒哪个线程,要看QMode值,以默认值QMode=0为例:
1、若是_EntryList队列不为空,则取出_EntryList队头节点并唤醒。
2、若是_EntryList为空,将_EntryList指向_cxq,并取出队头节点唤醒。
用图表示如下:
5、重量级锁小结
从加锁、解锁的流程可以明显地看出:
1、加锁过程是不断地尝试加锁,实在不行了才放入队列里,而且还是插入队列头的位置,最后才挂起自己。
2、想象一种场景:现在A线程持有锁,B线程在队列里等待,在A释放锁的时候,C线程刚好插进来获取锁,还未等B被A唤醒,C就获取了锁,B苦苦等待那么久还是没有获取锁。B线程不排队的行为造成了不公平竞争锁。
3、再想象另一种场景:还是A线程持有锁,B线程在队列里等待,此时C线程也要获取锁,因此要进入队列里排队,此处进入的是队列头,也就是在B的前面排着。当A释放锁后,唤醒队列里的头节点,也就是C线程。C线程插队的行为造成了不公平竞争锁。
3、综合1、2、3点可知,因为有走后门(不排队)\、插队(插到队头)、重量级锁是不公平锁。
综合加锁、解锁流程,用图表示如下:
图上流程对应的场景如下:
1、线程A先抢占锁,A在进入阻塞队列前已经成功获取锁。
2、而后线程B抢占锁,发现锁已被占有,于是加入阻塞队列队头。
3、最后线程C也来抢占锁,发现锁已经被占有,于是加入阻塞队列队头,此时B已经被C抢了队头位置。
4、当A释放锁后,唤醒阻塞队列里的队头线程C,C开始去抢占锁。
5、C拿到锁后,将自己从阻塞队列里移出。
6、后面的流程和之前一样。
上面的流程可能比较枯燥,用代码来演示以上场景:
public class TestThread {
static Object object = new Object();
static Thread a, b, c;
public static void main(String args[]) {
a = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A before get lock");
synchronized (object) {
System.out.println("A get lock");
try {
Thread.sleep(1000);
b.start();
//等待b已经启动并去抢占锁
Thread.sleep(1000);
c.start();
//等待b/c都已经启动,并且去抢占锁
Thread.sleep(2000);
} catch (Exception e) {
}
}
System.out.println("A after get lock");
}
});
a.start();
b = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("B before get lock");
synchronized (object) {
System.out.println("B get lock");
}
System.out.println("B after get lock");
}
});
b.start();
c = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("C before get lock");
synchronized (object) {
System.out.println("C get lock");
}
System.out.println("C after get lock");
}
});
c.start();
}
}
每次输出结果都很固定:
可以看出与我们预期的一致:B虽然先去抢占锁,但总是被后来者的C先抢到锁,不公平之处尽显。
6、与偏向锁、轻量级锁的比对
至此,偏向锁、轻量级锁、重量级锁都已经分析完毕。
锁的核心在于谁是锁?
对于偏向锁和轻量级锁,"锁"是Mark Word。
对于重量级锁,"锁"是ObjectMonitor。
更多关于三者的异同以及适用场景请移步上篇文章:Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
本篇文章分析了重量级锁的互斥过程,下篇文章将会分析与重量级锁紧密相关的同步过程(wait/notify/notifyAll)。
本文源码基于jdk1.8。
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android
1、Android各种Context的前世今生
2、Android DecorView 一窥全貌(上)
3、Android DecorView 一窥全貌(下)
4、Window/WindowManager 不可不知之事
5、View Measure/Layout/Draw 真明白了
6、Android事件分发全套服务
7、Android invalidate/postInvalidate/requestLayout 彻底厘清
8、Android Window 如何确定大小/onMeasure()多次执行原因
9、Android事件驱动Handler-Message-Looper解析
10、Android 键盘一招搞定
11、Android 各种坐标彻底明了
12、Android Activity/Window/View 的background
13、Android IPC 之Service 还可以这么理解
14、Android IPC 之Binder基础
15、Android IPC 之Binder应用
16、Android IPC 之AIDL应用(上)
17、Android IPC 之AIDL应用(下)
18、Android IPC 之Messenger 原理及应用
19、Android IPC 之获取服务(IBinder)
20、Android 存储基础
21、Android 10、11 存储完全适配(上)
22、Android 10、11 存储完全适配(下)
23、Java 并发系列不再疑惑