ReentrantLock的原理学习
转载:https://my.oschina.net/andylucc/blog/651982
摘要
提到JAVA加锁,我们通常会想到synchronized关键字或者是Java Concurrent Util(后面简称JCU)包下面的Lock,今天就来扒一扒Lock是如何实现的,比如我们可以先提出一些问题:当我们通实例化一个ReentrantLock并且调用它的lock或unlock的时候,这其中发生了什么?如果多个线程同时对同一个锁实例进行lock或unlcok操作,这其中又发生了什么?
什么是可重入锁?
ReentrantLock是可重入锁,什么是可重入锁呢?可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待。可重入锁是如何实现的呢?这要从ReentrantLock的一个内部类Sync的父类说起,Sync的父类是AbstractQueuedSynchronizer(后面简称AQS)。
什么是AQS?
AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性可以这么说,JCU包里面几乎所有的有关锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS这个框架。AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。当state的值为0的时候,标识改Lock不被任何线程所占有。
ReentrantLock锁的架构
ReentrantLoc的架构相对简单,主要包括一个Sync的内部抽象类以及Sync抽象类的两个实现类。上面已经说过了Sync继承自AQS,他们的结构示意图如下:
上图除了AQS之外,我把AQS的父类AbstractOwnableSynchronizer(后面简称AOS)也画了进来,可以稍微提一下,AOS主要提供一个exclusiveOwnerThread属性,用于关联当前持有该所的线程。另外、Sync的两个实现类分别是NonfairSync和FairSync,由名字大概可以猜到,一个是用于实现公平锁、一个是用于实现非公平锁。那么Sync为什么要被设计成内部类呢?我们可以看看AQS主要提供了哪些protect的方法用于修改state的状态,我们发现Sync被设计成为安全的外部不可访问的内部类。ReentrantLock中所有涉及对AQS的访问都要经过Sync,其实,Sync被设计成为内部类主要是为了安全性考虑,这也是作者在AQS的comments上强调的一点。
AQS的等待队列
作为AQS的核心实现的一部分,举个例子来描述一下这个队列长什么样子,我们假设目前有三个线程Thread1、Thread2、Thread3同时去竞争锁,如果结果是Thread1获取了锁,Thread2和Thread3进入了等待队列,那么他们的样子如下:
AQS的等待队列基于一个双向链表实现的,HEAD节点不关联线程,后面两个节点分别关联Thread2和Thread3,他们将会按照先后顺序被串联在这个队列上。这个时候如果后面再有线程进来的话将会被当做队列的TAIL。
1)入队列
我们来看看,当这三个线程同时去竞争锁的时候发生了什么?
代码:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
解读:
三个线程同时进来,他们会首先会通过CAS去修改state的状态,如果修改成功,那么竞争成功,因此这个时候三个线程只有一个CAS成功,其他两个线程失败,也就是tryAcquire返回false。
接下来,addWaiter会把将当前线程关联的EXCLUSIVE类型的节点入队列:
代码:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
解读:
如果队尾节点不为null,则说明队列中已经有线程在等待了,那么直接入队尾。对于我们举的例子,这边的逻辑应该是走enq,也就是开始队尾是null,其实这个时候整个队列都是null的。
代码:
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
解读:
如果Thread2和Thread3同时进入了enq,同时t==null,则进行CAS操作对队列进行初始化,这个时候只有一个线程能够成功,然后他们继续进入循环,第二次都进入了else代码块,这个时候又要进行CAS操作,将自己放在队尾,因此这个时候又是只有一个线程成功,我们假设是Thread2成功,哈哈,Thread2开心的返回了,Thread3失落的再进行下一次的循环,最终入队列成功,返回自己。
2)并发问题
基于上面两段代码,他们是如何实现不进行加锁,当有多个线程,或者说很多很多的线程同时执行的时候,怎么能保证最终他们都能够乖乖的入队列而不会出现并发问题的呢?这也是这部分代码的经典之处,多线程竞争,热点、单点在队列尾部,多个线程都通过【CAS+死循环】这个free-lock黄金搭档来对队列进行修改,每次能够保证只有一个成功,如果失败下次重试,如果是N个线程,那么每个线程最多loop N次,最终都能够成功。
3)挂起等待线程
上面只是addWaiter的实现部分,那么节点入队列之后会继续发生什么呢?那就要看看acquireQueued是怎么实现的了,
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
我们还是以上面的例子来看看,Thread2和Thread3已经被放入队列了,进入acquireQueued之后:
对于Thread2来说,它的prev指向HEAD,因此会首先再尝试获取锁一次,如果失败,则会将HEAD的waitStatus值为SIGNAL,下次循环的时候再去尝试获取锁,如果还是失败,且这个时候prev节点的waitStatus已经是SIGNAL,则这个时候线程会被通过LockSupport挂起。
对于Thread3来说,它的prev指向Thread2,因此直接看看Thread2对应的节点的waitStatus是否为SIGNAL,如果不是则将它设置为SIGNAL,再给自己一次去看看自己有没有资格获取锁,如果Thread2还是挡在前面,且它的waitStatus是SIGNAL,则将自己挂起。
如果Thread1死死的握住锁不放,那么Thread2和Thread3现在的状态就是挂起状态啦,而且HEAD,以及Thread的waitStatus都是SIGNAL,尽管他们在整个过程中曾经数次去尝试获取锁,但是都失败了,失败了不能死循环呀,所以就被挂起了。当前状态如下:
锁释放-等待线程唤起
我们来看看当Thread1这个时候终于做完了事情,调用了unlock准备释放锁,这个时候发生了什么。
代码:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
解读:
首先,Thread1会修改AQS的state状态,加入之前是1,则变为0,注意这个时候对于非公平锁来说是个很好的插入机会,举个例子,如果锁是非公平锁,这个时候来了Thread4,那么这个锁将会被Thread4抢去。。。
我们继续走常规路线来分析,当Thread1修改完状态了,判断队列是否为null,以及队头的waitStatus是否为0,如果waitStatus为0,说明队列无等待线程,按照我们的例子来说,队头的waitStatus为SIGNAL=-1,因此这个时候要通知队列的等待线程,可以来拿锁啦,这也是unparkSuccessor做的事情,unparkSuccessor主要做三件事情:
将队头的waitStatus设置为0.
通过从队列尾部向队列头部移动,找到最后一个waitStatus<=0的那个节点,也就是离队头最近的没有被cancelled的那个节点,队头这个时候指向这个节点。
将这个节点唤醒,其实这个时候Thread1已经出队列了。
还记得线程在哪里挂起的么,上面说过了,在acquireQueued里面,我没有贴代码,自己去看哦。这里我们也大概能理解AQS的这个队列为什么叫FIFO队列了,因此每次唤醒仅仅唤醒队头等待线程,让队头等待线程先出。
羊群效应
这里说一下羊群效应,当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么如果有成千上万个线程在等待锁,有一种做法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然造成资源的剧增和浪费,因此终究只能有一个线程竞争成功,其他线程还是要老老实实的回去等待。AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。其实这个思路已经被应用到了分布式锁的实践中,见:Zookeeper分布式锁的改进实现方案。
ReentrantLock的原理学习的更多相关文章
- ReentrantLock实现原理深入探究
前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写ReentrantLock的使用.ReentrantLock和synchronized的区别的文章很多,研究ReentrantLoc ...
- IIS原理学习
IIS 原理学习 首先声明以下内容是我在网上搜索后整理的,在此只是进行记录,以备往后查阅只用. IIS 5.x介绍 IIS 5.x一个显著的特征就是Web Server和真正的ASP.NET Appl ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- 【Java并发编程】15、ReentrantLock实现原理深入探究
原文已经写得非常详细了,直接把大神的文章转发过来了 https://www.cnblogs.com/xrq730/p/4979021.html 前言 这篇文章被归到Java基础分类中,其实真的一点都 ...
- zookkeper原理学习
zookkeper原理学习 https://segmentfault.com/a/1190000014479433 https://www.cnblogs.com/felixzh/p/58692 ...
- GIS原理学习目录
GIS原理学习目录 内容提要 本网络教程是教育部“新世纪网络课程建设工程”的实施课程.系统扼要地阐述地理信息系统的技术体系,重点突出地理信息系统的基本技术及方法. 本网络教程共分八章:第一章绪论,重点 ...
- 转:SVM与SVR支持向量机原理学习与思考(一)
SVM与SVR支持向量机原理学习与思考(一) 转:http://tonysh-thu.blogspot.com/2009/07/svmsvr.html 弱弱的看了看老掉牙的支持向量机(Support ...
- Android自复制传播APP原理学习(翻译)
Android自复制传播APP原理学习(翻译) 1 背景介绍 论文链接:http://arxiv.org/abs/1511.00444 项目地址:https://github.com/Tribler ...
- 计算机原理学习(1)-- 冯诺依曼体系和CPU工作原理
前言 对于我们80后来说,最早接触计算机应该是在95年左右,那个时候最流行的一个词语是多媒体. 依旧记得当时在同学家看同学输入几个DOS命令就成功的打开了一个游戏,当时实在是佩服的五体投地.因为对我来 ...
随机推荐
- Swift游戏实战-跑酷熊猫 01 创建工程导入素材
在这节里,我们将建立一个游戏工程,并导入一些必要的素材,例如序列帧动画文件,声音素材文件.动画文件我们使用atlas形式.在打包发布或者模拟器测试的时候,它会将整个.atlas文件夹下的图片打包成一张 ...
- 查看真机的系统版本sdk
1.adb devices 确保连接上了真机 2.adb shell 进入android系统 3.进入system目录下 4.查看build.prop文件 cat build.prop
- yii2封装一个类控制div宽度,高度
1.首先,封装一个类,放在文件夹vendor下,命名为articls.php. <?phpclass Articles{ //测试 function add() { r ...
- springmvc下上传文件
使用ajax+表单+jQuery: function sendFile() { var action = "c/goFile.do"; $("#form").a ...
- MyEclipse启动失败
日志的一部分: !SESSION 2014-09-24 11:47:03.156 -----------------------------------------------eclipse.buil ...
- java 网络编程(三)---TCP的基础级示例
下面是TCP java网络编程的基础示例: tcp传输:客户端建立过程的思路:1.创建TCP客户端的Socket服务,使用的是socket对象,建议在创建的过程中,就明确了目的地和要连接的主机2.如果 ...
- hadoop之输入输出格式
<STRONG>jobConf.setInputFormat(MyInputFormat. class ); InputFormat:</STRONG> TextInputFo ...
- 【GDI+】继续图形的问题
现在有一个需求: 一个或多个四个点组成的矩形,一个或多个值指定 下一点->当前点方向的垂直方向是不闭合的. 目前大概有三种情况: 1.只有一个矩形且只有一个指定不闭合方向的时候,此时只用按照指定 ...
- 【GDI+】一些规则多边形分离的问题
在近期的工作中,需要做一样工作:将一些有规则的图形,进行适当的分离,以达到不重叠的问题. 首先组成图形的点都可以是按照逆时针排好序的. 规则的图形可以大致分为三类: A :两个点组成的线 或者 四个点 ...
- MySQL OnlineDDL
参考资料: http://dev.mysql.com/doc/refman/5.6/en/innodb-create-index-overview.html http://www.mysqlperfo ...