java aqs源码_Java-AQS源码详解(细节很多!)
ReentrantLock调用lock()时时序图:
addWaiter方法:
enq方法:自旋
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
getState()
setState()
compareAndSetState()
aqs有两种资源访问模式:独占(ReentrantLock)和共享(CountDownLatch和Semaphore、CyclicBarrier)
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了!接下来开始撸吧。。至于这里双向链表是怎么样的一个结构,这里就不做多于的描述了,大家可以自行去补充。
首先我们由一张图开头,我们要知道AQS其实主要实现的是一个FIFO的双向链表的维护,每个Node其实就是一个等待被释放的线程,在竞争锁失败后,会封装成Node的形式进入到链表尾部。。在了解了最基本的概念后,我们先来看看AQS最经典的应用ReentrantLock的lock方法:
public voidlock() {
sync.lock();// sync主要两种实现类
}
// 第一种非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**非公平锁实现的lock方法
*/
final void lock() {
if (compareAndSetState(0, 1))// CAS操作去尝试将state变为1,也就是独占状态
setExclusiveOwnerThread(Thread.currentThread());// 非公平锁并不会老老实实去排队,而是一上来就插队,插不了就只能去排队了。。
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 第二种公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);// 相比于非公平锁,就比较守规矩了
}
」
因为非公平和公平就只有这么一个差别,那我就以非公平锁为切入点了,可以看到在尝试抢占失败后,调用acquire方法,ok进入到该方法:
// 此方法是AQS的,但是注意里面的tryAcquire是需要我们的自定义AQS实现的,直接调用AQS的会直接抛出异常UnsupportedOperationException
public final void acquire(intarg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire是NonfairSync实现的,而他内部又直接调用Sync父类的nonfairTryAcquire方法:
final boolean nonfairTryAcquire(intacquires) {final Thread current =Thread.currentThread();int c =getState();if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);// 直接CAS独占return true;
}
}else if (current ==getExclusiveOwnerThread()) {int nextc = c +acquires;// 这里很确切的说明了ReentrantLock是一个可重入的锁if (nextc < 0) //overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);return true;
}return false;
}
上面的方法相信大家应该很快就理解了,在尝试独占失败后,tryAcquire操作返回false,然后这个时候要做的操作相信大家也可以猜到,就是插入到双向链表中,看上面的代码,第一个操作是addWaiter,于是我们贴出这个方法涉及的源码:
privateNode addWaiter(Node mode) {
Node node= newNode(Thread.currentThread(), mode);// 在这里首先根据当前线程创建出一个节点
Node pred =tail;// 既然要插入节点,肯定是要插入到最尾部的,先获取到tail节点if (pred != null) {
node.prev=pred;// 将当前节点的prev和尾部节点关联 --第五行if(compareAndSetTail(pred, node)) {
pred.next=node;// node和老tail关联完成returnnode;
}
}
enq(node);returnnode;
}
如果某个线程在插入队列没有其他线程干扰的话,enq都不会进去的,直接在CAS设置成tail之后直接返回了,但是实际上,总是会有那么几个“不长眼”的线程来和你对着干。。。来假设这么一个场景:A线程是tail节点,此时B和C进来,他们都同时进入到第五行那里,也就是你会发现A会有B和C两个节点的prev指向它,但是下一行的CAS操作是一个原子性操作,所以B和C只能一个成为tail,那么又假设B成功CAS了,也就是B可以直接返回,但是C就比较“悲催”了,它得进入到下一个方法enq,因为此时的链表结构很是奇怪,C的prev指向了old tail:A,所以得做一个“修复”结构操作,将C的prev指向B,接下来看enq代码:
private Node enq(finalNode node) {// 此时没有成功CAS的C节点“失魂落魄”的走了进来for(;;) {
Node t=tail;if (t == null) { //
if (compareAndSetHead(newNode()))// 如果此时队列完全为空(第一个线程进来),需要弄一个冗余head节点,之后你会看到作用的。。别急
tail=head;
}else{
node.prev=t;// 此时的C节点要和B节点绑上关系if(compareAndSetTail(t, node)) {
t.next=node;// 关联完成returnt;
}
}
}
}
此时的C应该是可以回到正轨的,就算此时又一个线程打扰了C的关联操作而导致CAS失败,但是因为代码在for循环里,可以重试,基本上很快就可以回到队列正轨!!于是我们又可以愉快的进行下一个步骤了,再回到我们熟悉的acquire(有点绕,忘记的往上翻),可以看到addWaiter之后,会将当前节点返回给一个“新面孔”-acquireQueued方法作为参数,我们再看看这个方法是怎么做的:
final boolean acquireQueued(final Node node, intarg) {boolean failed = true;try{boolean interrupted = false;for(;;) {final Node p =node.predecessor();// 当前节点的前置节点if (p == head &&tryAcquire(arg)) {
// 当前置节点为head,那么可以去尝试获取锁,成功的话就调用setHead方法将自己设置为head节点
setHead(node);
p.next= null; //help GC
failed = false;returninterrupted;
}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
// 判断当前节点是否可以被阻塞,shouldParkAfterFailedAcquire方法为核心
interrupted= true;
}
}finally{if(failed)
cancelAcquire(node);
}
}
可以看到,如果当前节点的上一个节点就是head的话,说明当前可以竞争到锁的概率会很大,一旦head节点的线程执行完unlock后,当前的state变为0,当前节点就可以进入到setHead方法,但是如果头节点还在执行中,那么当前节点只能老老实实的进入到shouldParkAfterFailedAcquire方法内部,来决定当前节点是否应该能被阻塞:
private static booleanshouldParkAfterFailedAcquire(Node pred, Node node) {
//static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;static final int PROPAGATE = -3;
int ws =pred.waitStatus;// 在这里终于有用上这个变量了if (ws ==Node.SIGNAL)/*在这里我打算用一个很易于理解的方式来讲述这个SIGNAL值有什么用:
相信大家都有过排队的经历,在这里服务窗口相当于锁,每个人过来时,发现窗口有其他人在,所以此时只能去队尾排队,也就是addWaiter操作,在队尾后,waitStatus的值默认是0,但是此时刚排进队的小伙伴,因为队伍太长,
而且比较累,需要低头打个盹,但是怕如果瞌睡打过头了,就不知道什么时候窗口没人了可以被服务,所以此时小伙伴为了保险,他需要一个可靠的“前置队友”,也就是他前面的人如果业务办完了,可以顺便回头来叫醒他,在这里可
以把“委托前面的人,如果结束了麻烦叫醒我,谢谢!”这个操作理解为将prev节点的waitStatus设置为SIGNAL,如果前置节点的waitStatus不是0,需要尝试设置为SIGNAL,但如果前面的小伙伴已经是SIGNAL了,直接返回,
说明当前小伙伴可以安心的打盹了(被阻塞)!!*/
return true;if (ws > 0) {/** 如果是CANCELLED,代表当前节点已经不需要处理业务了,可以在队列里直接清除出去,然后队列重新规整*/
do{
node.prev= pred =pred.prev;
}while (pred.waitStatus > 0);
pred.next=node;
}else{/* 到这一步,就会尝试去将前置节点设置为SIGNAL,但是有可能会设置失败或者设置成功,但是不论成功还是失败,都会返回false,也就是在上面的acquireQueued中,返回false后会继续for循环里去尝试获取锁,因为小伙伴必须要确定前面的伙伴要靠谱,也就是必须要是SIGNAL
*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}return false;
}
我们再来看看unlock方法,他有直接调用AQS的release方法,而tryRelease方法由自定义的AQS类实现:
public final boolean release(intarg) {if(tryRelease(arg)) {
Node h=head;if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 关键操作,如何去唤醒后面的小伙伴return true;
}return false;
}
protected final boolean tryRelease(intreleases) {int c = getState() -releases;if (Thread.currentThread() !=getExclusiveOwnerThread())throw newIllegalMonitorStateException();boolean free = false;if (c == 0) {
free= true;
setExclusiveOwnerThread(null);// 彻底释放锁后,将ownerThread设置为null,重置state
}
setState(c);returnfree;
}
tryRelease操作其实很好理解,主要是unparkSuccessor方法:
private voidunparkSuccessor(Node node) {/** 在释放完锁后,此时的节点他已经不需要SIGNAL这个状态了,因为他觉得自己办完业务了,就可以尝试去给自己“放个假”,当变成0的时候,后面的小伙伴在shouldPark里就会返回false,代表当前前置节点很有可能不是刚刚初始化
导致的waitStatus == 0,而是前置节点刚释放完锁,所以就是head:“我此时已经释放完锁了,后面的,你现在就别打盹了,赶紧再去尝试抢锁吧!”,于是此时心急的小伙伴就赶紧再进入for循环里尝试tryAcquire*/
int ws =node.waitStatus;if (ws < 0)
compareAndSetWaitStatus(node, ws,0);/***/Node s=node.next;if (s == null || s.waitStatus > 0) {
s= null;for (Node t = tail; t != null && t != node; t =t.prev)
// 从后往前,找到第一个需要被唤醒的小伙伴,状态也是必须>=0,至于为什么会==0,因为最后一个节点的status一定是0if (t.waitStatus <= 0)
s=t;
}if (s != null)
LockSupport.unpark(s.thread);// 此时的s就是下一个需要被唤醒的,于是unpark
}
不知道你们有没有发现,为什么上面的代码里要从后往前扫描呢,双向链表不是两边都可以扫吗,这个就很有趣了,不知道你们有没有看到在addWaiter和enq方法里,在将当前节点CAS成tail的前一步,有一个先将node的prev设置为前一个节点,也就是双向表的建立关系是先后节点连接前节点开始的,但是因为设置两个节点的关系时不是原子操作,那么就会导致可能prev关系存在,但是next关系不存在的时候,unpark操作就开始需要去遍历链表了,而这个时候,用next操作就很可能会遗漏掉哪个“小伙伴”而导致出现误“唤醒”!!
java aqs源码_Java-AQS源码详解(细节很多!)相关推荐
- java单例设计模式_Java设计模式之单例模式详解
在Java开发过程中,很多场景下都会碰到或要用到单例模式,在设计模式里也是经常作为指导学习的热门模式之一,相信每位开发同事都用到过.我们总是沿着前辈的足迹去做设定好的思路,往往没去探究为何这么做,所以 ...
- java 打印异常内容_java自定义异常打印内容详解
本文实例为大家分享了java自定义异常打印内容的具体代码,供大家参考,具体内容如下 背景:在开发中,我们可能会使用到自定义异常,但是,这个自定义异常在打印日志时,往往打印的内容比较多. 1.自定义异常 ...
- java虚拟机工作原理_Java虚拟机工作原理详解
一.类加载器 首先来看一下java程序的执行过程. 从这个框图很容易大体上了解java程序工作原理.首先,你写好java代码,保存到硬盘当中.然后你在命令行中输入: javac YourClassNa ...
- java代码轻量级锁_Java轻量级锁原理详解(Lightweight Locking)
转自http://www.cnblogs.com/redcreen/archive/2011/03/29/1998801.html 大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的 ...
- java的static类_java中staticclass静态类详解
一般情况下是不可以用static修饰类的.如果一定要用static修饰类的话,通常static修饰的是匿名内部类. 在一个类中创建另外一个类,叫做成员内部类.这个成员内部类可以静态的(利用static ...
- Java 初始化 代码块_Java中初始化块详解及实例代码
Java中初始化块详解 在Java中,有两种初始化块:静态初始化块和非静态初始化块. 静态初始化块:使用static定义,当类装载到系统时执行一次.若在静态初始化块中想初始化变量,那仅能初始化类变量, ...
- java connection 单例_Java设计模式之单例模式详解
Java设计模式之单例模式详解 什么是设计模式 设计模式是在大量的实践中总结和理论之后优选的代码结构,编程风格,以及解决问题的思考方式.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可 ...
- java指数表示法_Java指数计数法详解
Java指数计数法详解 时间:2017-10-16 来源:华清远见Java培训中心 Java指数计数法并不是一个很难的运算,关键是你要理解应用,很多朋友不理解Java指数计数法,所以也无从运用 ...
- java方法怎么写_java方法定义格式详解,java方法怎么写?
对于java方法你了解多少呢?你知道java方法应该如何写吗?下面要给大家介绍的就是和java方法相关的内容,一起来了解一下这个概念吧. 在学习运算符的时候,都为每个运算符单独的创建一个新的类和mai ...
- java解析json数据_java解析JSON数据详解
JSON是目前最流行的轻量级数据交换语言(没有之一).尽管他是javaScript的一个子集.但由于其是独立与语言的文本格式,它几乎可以被所有编程语言所支持. 以下是对java语言中解析json数据的 ...
最新文章
- vscode 无法跳转到函数定义_玩转VS Code
- mysql python 接口_Python中的MySQL接口:PyMySQL MySQLdb
- 安装安卓SDK和JDK的简便方法
- INLINE HOOK过驱动保护的理论知识和大概思路
- Linux环境PHP7安装
- 【HDU - 4345 】Permutation(DP)
- asp.net页面调用cs中的方法
- 适配器模式的极简概述
- windows库的创建和使用:静态库+动态库
- 带蓝色的紫罗兰色——三色配色篇
- D. Pythagorean Triples (math、暴力)
- 影音视频领域开源项目专区
- 如何在Nginx上 安装SSL证书
- nodejs 安装模块失败 解决方法
- VS2019 .NetCore智能提示从英文变成中文设置
- mongoose http 源码解析(1)
- python中对象的多态、封装、继承介绍
- Rtools is required to build R packages but is not currently installed
- 美洽消息推送 php,GitHub - Meiqia/MeiqiaSDK-Push-Signature-Example: 美洽 SDK 3.0 推送的数据结构签名算法,多语言示例。...
- 【MySQL】——mysql exporter源码分析
热门文章
- CUDA运行时 Runtime(二)
- 人脸照片自动生成游戏角色_ICCV2019论文解析
- 2021年大数据Spark(十七):Spark Core的RDD持久化
- java gui 案例_JavaGui入门—布局的嵌套使用附实例
- HarmonyOS Text超出部分末尾显示...
- ValueError: max() arg is an empty sequence
- Windows 系统下使用grep 命令
- github READme 的使用教程
- JS中的7种设计模式
- 随心测试_软测基础_005 测试人员工作内容