Java Synchronized 重量级锁原理深入剖析上(互斥篇) 您所在的位置:网站首页 重量级锁有哪些 Java Synchronized 重量级锁原理深入剖析上(互斥篇)

Java Synchronized 重量级锁原理深入剖析上(互斥篇)

2023-10-30 02:10| 来源: 网络整理| 查看: 265

前言

线程并发系列文章:

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类。

image.png

如上图,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。

以上就是膨胀的流程,用图表示如下:

image.png

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()函数。

用图表示如下:

image.png

再次尝试加锁

初次获取锁失败后,会走到下面的流程,也就是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、成功则退出流程,失败继续走上面的流程。

用图表示如下:

image.png

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,并取出队头节点唤醒。

用图表示如下:

image.png

5、重量级锁小结

从加锁、解锁的流程可以明显地看出:

1、加锁过程是不断地尝试加锁,实在不行了才放入队列里,而且还是插入队列头的位置,最后才挂起自己。 2、想象一种场景:现在A线程持有锁,B线程在队列里等待,在A释放锁的时候,C线程刚好插进来获取锁,还未等B被A唤醒,C就获取了锁,B苦苦等待那么久还是没有获取锁。B线程不排队的行为造成了不公平竞争锁。 3、再想象另一种场景:还是A线程持有锁,B线程在队列里等待,此时C线程也要获取锁,因此要进入队列里排队,此处进入的是队列头,也就是在B的前面排着。当A释放锁后,唤醒队列里的头节点,也就是C线程。C线程插队的行为造成了不公平竞争锁。 4、综合1、2、3点可知,因为有走后门(不排队)\、插队(插到队头)、重量级锁是不公平锁。

综合加锁、解锁流程,用图表示如下:

image.png

图上流程对应的场景如下:

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(); } }

每次输出结果都很固定:

image.png

可以看出与我们预期的一致:B虽然先去抢占锁,但总是被后来者的C先抢到锁,不公平之处尽显。

6、与偏向锁、轻量级锁的比对

至此,偏向锁、轻量级锁、重量级锁都已经分析完毕。 锁的核心在于谁是锁?

对于偏向锁和轻量级锁,"锁"是Mark Word。 对于重量级锁,"锁"是ObjectMonitor。

更多关于三者的异同以及适用场景请移步上篇文章:Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程

本篇文章分析了重量级锁的互斥过程,下篇文章将会分析与重量级锁紧密相关的同步过程(wait/notify/notifyAll)。

本文源码基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力 持续更新中,和我一起步步为营系统、深入学习Java/Android


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有