前言
线程并发系列文章:
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各种锁(终极篇)
线程池必懂系列
上篇文章分析了偏向锁、轻量级锁的演变过程,本篇将分析重头戏:重量级锁的原理。
通过本篇文章,你将了解到: 1、ObjectMonitor 的运用
2、锁的膨胀过程
3、重量级锁的加锁流程
4、重量级锁的解锁流程
5、重量级锁小结
6、与偏向锁、轻量级锁的比对
1、ObjectMonitor 的运用
我们知道当锁处在轻量级锁的状态时,Mark Word 存放着指向Lock Record指针,Lock Record是线程私有的。
而处在重量级锁状态时说明有线程没拿到锁需要阻塞等待锁,当拥有锁的线程释放锁后唤醒它继续竞争锁。此处就引入了一个问题:其它线程如何找到被阻塞的线程?我们很容易想到:把阻塞的线程放到多线程共享的(能访问)的列表里。
而Lock Record是线程私有的,显然不能满足需求。
因此,重量级锁引入了ObjectMonitor类。
来看看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线程插队的行为造成了不公平竞争锁。
4、综合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。