图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)
前言
AbstractQueuedSynchronizer 抽象队列同步器,简称 AQS 。是在 JUC 包下面一个非常重要的基础组件,JUC 包下面的并发锁
ReentrantLock
CountDownLatch
等都是基于 AQS 实现的。所以想进一步研究锁的底层原理,非常有必要先了解 AQS 的原理。
公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!
介绍
先看下 AQS 的类图结构,以及源码注释,有一定的大概了解之后再从源码入手,一步一步研究它的底层原理。
" 源码注释
提供了实现阻塞锁和相关同步器依靠先入先出(FIFO)等待队列(信号量,事件等)的框架。 此类中设计了一个对大多数基于 AQS 的同步器有用的原子变量来表示状态(state)。 子类必须定义 protected 方法来修改这个 state,并且定义 state 值在对象中的具体含义是 acquired 或 released。 考虑到这些,在这个类中的其他方法可以实现所有排队和阻塞机制。 子类可以保持其他状态字段,但只能使用方法 getState 、setState 和 compareAndSetState 以原子方式更新 state 。
子类应被定义为用于实现其封闭类的同步性能的非公共内部辅助类。 类AbstractQueuedSynchronizer没有实现任何同步接口。 相反,它定义了一些方法,如 acquireInterruptibly 可以通过具体的锁和相关同步器来调用适当履行其公共方法。
此类支持独占模式和共享模式。 在独占模式下,其他线程不能获取成功,共享模式下可以(但不一定)获取成功。 此类不“理解”,在机械意义上这些不同的是,当共享模式获取成功,则下一个等待的线程(如果存在)也必须确定它是否能够获取。 线程在不同模式下的等待共享相同的FIFO队列。 通常情况下,实现子类只支持其中一种模式,但同时使用两种模式也可以,例如ReadWriteLock 。 仅共享模式不需要定义支持未使用的模式的方法的子类。
这个类中定义了嵌套类 AbstractQueuedSynchronizer.ConditionObject ,可用于作为一个 Condition 由子类实现,并使用 isHeldExclusively 方法说明当前线程是否以独占方式进行,release()、 getState() acquire() 方法用于操作 state 原子变量。
此类提供检查和监视内部队列的方法,以及类似方法的条件对象。 根据需要进使用以用于它们的同步机制。
要使用这个类用作同步的基础上,需要重新定义以下方法,如使用,通过检查和或修改 getState 、setState 或 compareAndSetState 方法:
tryAcquire
tryRelease
tryAcquireShared
tryReleaseShared
isHeldExclusively
"
通过上面的注释可以得出大概的印象:
内部依靠先入先出(FIFO) 等待队列。
存在 state 表示状态信息。state 值只能用 getState 、setState 和 compareAndSetState 方法以原子方式更新。
支持独占模式和共享模式,但具体需要子类实现具体支持哪种模式。
嵌套 AbstractQueuedSynchronizer.ConditionObject 可以作为 Condition 由子类实现。
子类需要重新定义 tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively 方法。
队列节点 Node
Node节点,包含以下元素:
prev :上一个节点
next :下一个节点
thread :持有线程
waitStatus :节点状态
nextWaiter : 下一个处于 CONDITION 状态的节点
组合成等待队列则如下:
下面是等待队列节点的 Node 属性:
在 Node 节点中需要重点关注 waitStatus
默认状态为 0;
waitStatus > 0 (CANCELLED 1) 说明该节点超时或者中断了,需要从队列中移除;
waitStatus = -1 SIGNAL 当前线程的前一个节点的状态为 SIGNAL,则当前线程需要阻塞(unpark);
waitStatus = -2 CONDITION -2 :该节点目前在条件队列;
waitStatus = -3 PROPAGATE -3 :releaseShared 应该被传播到其他节点,在共享锁模式下使用。
了解完 Node 的结构之后,再了解下 AQS 结构,并从常用方法入手,逐步了解具体实现逻辑。
AbstractQueuedSynchronizer
在 AQS 中主要参数为:
head :等待队列头
tail : 等待队列尾
state : 同步状态
通过注释了解到,在 AQS 里主要分为两种操作模式,分别是:独占模式、共享模式,下面分别从两个不同的角度去分析源码。
acquire : 以独占模式获取,忽略中断。 通过调用至少一次实施tryAcquire ,在成功时返回。 否则,线程排队,可能重复查封和解封,调用tryAcquire直到成功为止。 这种方法可以用来实现方法Lock.lock 。
release : 以独占模式释放。 通过疏通一个或多个线程,如果实现tryRelease返回true。 这种方法可以用来实现方法Lock.unlock 。
acquireShared : 获取在共享模式下,忽略中断。 通过至少一次第一调用实现tryAcquireShared ,在成功时返回。 否则,线程排队,可能重复查封和解封,调用tryAcquireShared直到成功为止。
releaseShared : 以共享模式释放。 通过疏通一个或多个线程,如果实现tryReleaseShared返回true。
无论是共享模式还是独占模式在这里面都会用到 addWaiter 方法,将当前线程及模式创建排队节点。
独占模式
获取独占资源 acquire
在独占模式下会尝试获取 state,当获取失败时会调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。
tryAcquire(arg),尝试获取 state 这块由子类自己实现,不同的子类逻辑不同,这块在介绍子类代码时会说明。
获取 state 失败后,会进行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这块代码可以拆分为两块:addWaiter(Node.EXCLUSIVE),acquireQueued(node, arg)。
addWaiter(Node.EXCLUSIVE) 返回的是当前新创建的节点。
acquireQueued(node, arg) 线程获取锁失败,使用 addWaiter(Node.EXCLUSIVE) 放入等待队列,而 acquireQueued(node, arg) 使用循环,不断的为队列中的节点去尝试获取资源,直到获取成功或者被中断。
总结获取资源主要分为三步:
尝试获取资源
入队列
出队列
尝试获取资源 tryAcquire(arg)
,由子类实现,那下面则着手分别分析 入队列
、出队列
。
入队列:addWaiter(Node.EXCLUSIVE)
使用 addWaiter(Node.EXCLUSIVE)
方法将节点插入到队列中,步骤如下:
根据传入的模式创建节点
判断尾节点是否存在,不存在则需要使用
enq(node)
方法初始化节点,存在则直接尝试
插入尾部。尝试
插入尾部时使用 CAS 插入,防止并发情况,如果插入失败,会调用enq(node)
自旋直到插入。
看完代码和注释肯定还是有点模糊,现在用图一步一步进行说明。
因为根据初始尾节点是否为空
分为两种情况,这里使用两幅图:
第一幅为第一次添加节点,此时 head 会延迟初始化;
第二幅图为已经存在队列,进行插入节点;
注意看代码,enq 方法返回的是
之前的尾节点
;addWaiter 方法 返回的是
当前插入的新创建的节点
。
节点添加到队列之后,返回当前节点,而下一步则需要调用方法 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
不断的去获取资源。
出队列:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法会通过循环不断尝试获取拿到资源,直到成功。代码如下:
不断获取本节点的上一个节点是否为 head,因为 head 是虚拟节点,如果当前节点的上一个节点是 head 节点,则当前节点为
第一个数据节点
;第一个数据节点不断的去获取资源,获取成功,则将 head 指向当前节点;
当前节点不是头节点,或者
tryAcquire(arg)
失败(失败可能是非公平锁)。这时候需要判断前一个节点状态决定当前节点是否要被阻塞
(前一个节点状态是否为 SIGNAL)。
在 shouldParkAfterFailedAcquire
方法中,会判断前一个节点的状态,同时取消在队列中当前节点前面无效的节点。
再继续阅读 出队列 acquireQueued 方法,发现有一个 finally 会判断状态后执行 cancelAcquire(node);
,也就是上面流程图中下面的红色方块。
cancelAcquire(Node node)
流程分析:
找到当前节点的前一个非无效节点 pred;
当前节点如果是尾节点,则将最后一个有效节点设置为尾节点,并将 predNext 设置为空;
pred 不是头节点 && ( pred的状态是 SIGNAL || pred 的状态设置为 SIGNAL 成功 ) && pred 的绑定线程不为空;
其他情况。
下面分别画图:
Q: 通过图可以看出来,只操作了 next 指针,但是没有操作 prev 指针,这是为什么呢?
A: 在 出队列:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法中,shouldParkAfterFailedAcquire
方法会判断前一个节点的状态,同时取消在队列中当前节点前面无效的节点。这时候会移除之前的无效节点,此处也是为了防止指向一个已经被移除的节点。同时保证 prev 的稳定,有利于从 tail 开始遍历列表,这块在 unparkSuccessor(node);
中也可以看到是从后往前表里列表。
Q: unparkSuccessor(Node node) 为什么从后往前遍历?
A:
在 addWaiter(Node.EXCLUSIVE)
插入新节点时,使用的是 尾插法
,看红框部分,此时有可能还未指向next。
Q: node.next = node; 这块导致 head不是指向最新节点,链表不就断了么?
A: acquireQueued 方法介绍中,里面有个循环,会不断尝试获取资源,成功之后会设置为 head。并且在 shouldParkAfterFailedAcquire 中也会清除当前节点前的无效节点。
释放独占资源 release
以独占模式释放。 通过释放一个或多个线程,如果实现tryRelease返回true。 这种方法可以用来实现方法Lock.unlock 。
tryRelease(arg) 操作释放资源,同样是由子类实现,后面介绍子类时会进行说明。返回 true 说明资源现在已经没有线程持有了,其他节点可以尝试获取;
释放成功,且 head != null && h.waitStatus != 0, 会继续执行 unparkSuccessor(h);
这块会看到 只要 tryRelease(arg) 操作释放资源成功, 后面无论执行是否成功,都会返回 true,unparkSuccessor(h) 相当于只是附加操作。
共享模式
获取共享资源 acquireShared
tryAcquireShared(arg),尝试获取资源,这块由子类实现;
返回值分为 3 种:
1. 小于 0: 表示失败;
2. 等于 0: 表示共享模式获取资源成功,但后续的节点不能以共享模式获取成功;
3. 大于 0: 表示共享模式获取资源成功,后续节点在共享模式获取也可能会成功,在这种情况下,后续等待线程必须检查可用性。
在失败后会使用
doAcquireShared(arg);
不断获取资源;final Node node = addWaiter(Node.SHARED);
同样会创建节点;在循环中不断判断前一个节点如果是 head,则尝试获取资源;
在共享模式下获取到资源后会使用
setHeadAndPropagate(node, r);
设置头节点,同时唤醒后续节点。
设置头节点,并传播唤醒后续节点
doReleaseShared() 释放共享资源
从头节点开始进行,如果 h != null && h != tail 说明队列不是空或者刚初始化;
节点状态为 SIGNAL( -1 )说明后续线程需要释放;
会更改当前节点状态,成功后唤醒后续节点,失败则继续循环;
节点状态如果是 0 则更新为 PROPAGATE,会将状态传播。
释放共享资源 releaseShared
以共享模式释放。 通过释放一个或多个线程,如果实现tryReleaseShared返回true。
总结
Q: AQS 到底是什么?
A: AQS 内部提供了一个先入先出(FIFO)双向等待队列,内部依靠 Node 实现,并提供了在独占模式
和共享模式
下的出入队列的公共方法。而关于状态信息 state 的定义是由子类实现。tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared等尝试获取资源操作都是由子类进行定义和实现的。而 AQS 中提供了子类获取资源之后的相关操作,包括节点 Node 的出入队列,自旋获取资源等等。
Q: AQS 获取资源失败后会如何操作?
A: 线程获取资源失败后,会放到等待队列中,在队列中会不断尝试获取资源(自旋),说明线程只是进入等待状态,后面还是可以再次获取资源的。
Q: AQS 等待队列的数据结构是什么?
A: CLH变体的先入先出(FIFO)双向等待队列。(CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。)
Q: AQS 等待队列中的节点如何获取获取和释放资源的?
A: 可以看下独占模式
中的讲述过程,通过代码梳理。
本文分别从 独占模式
和 共享模式
介绍的 AQS 基本逻辑,并通过源码和作图理解基本思路。但是并没有对需要子类实现的业务逻辑做介绍。这块会在后面介绍 ReentrantLock
、CountDownLatch
等子类的时候做介绍。
版权声明: 本文为 InfoQ 作者【程序员小航】的原创文章。
原文链接:【http://xie.infoq.cn/article/5a3cc0b709012d40cb9f41986】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论