Concurrent -- 03 -- AQS解析
相关文章:
AQS (AbstractQueuedSynchronizer) 是用于构建锁或其他同步组件的基础框架类,在 Java 中许多并发工具类的内部类实现都依赖于 AQS,如:ReentrantLock、Semaphore、CountDownLatch 等,是 JUC 包的核心组件
一、实现原理
- AQS 的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,则会阻塞当前请求资源的线程,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,当持有锁的线程释放锁后,会唤醒队列中的后继线程
二、核心概念
-
Node
- 是 AQS 的核心内部类,用于包裹线程以及相关状态信息,是其他一切操作的基础
-
Condition
-
是 JUC 包提供的一个与 Object 类中 wait()、notify()、notifyAll() 方法类似功能的一个接口
-
其定义了 await()、awaitNanos(long)、signal()、signalAll() 等方法,配合对象锁 (如:ReentrantLock) 可以用于实现线程的选择性通知
-
在 AQS 中的具体实现为 内部类 ConditionObject
-
-
CLH
-
CLH (是算法提出者 Craig、Landin、Hagersten 的名字简称) 是由 AQS 内部维护的一个 FIFO (先进先出) 的虚拟双向队列 (即不存在队列实例,仅存在节点之间的关联关系),AQS 依赖其来管理等待中的线程

-
-
waitStatus (节点状态)
-
volatile int waitStatus; -
waitStatus 用于表示 Node 节点的状态,有以下 5 个值
-
CANCELLED (1)
-
表示当前节点中的线程已被取消
-
当线程等待超时或被中断时,会触发变更为此状态,进入该状态的节点不会再发生变化,且节点中的线程永远不会再被阻塞
-
-
SIGNAL (-1)
-
表示后继节点正在等待当前节点的唤醒
-
当节点释放锁会被取消时,会唤醒其后继节点,使后继节点中的线程得以运行
-
-
CONDITION (-2)
-
表示当前节点等待在 Condition 上
-
当其他线程调用了 Condition 的 signal() 方法后,该节点会从等待队列转移到同步队列中,等待获取同步锁
-
-
PROPAGATE (-3)
-
表示下一次的共享状态会被无条件的传播下去
-
在共享模式下,当前节点不仅会唤醒其后继节点,同时也可能会唤醒后继的后继节点
-
-
0
- 新节点加入队列时的默认状态
-
-
-
state (同步状态)
-
private volatile int state; -
state 用于表示同步状态,子类可以根据实际需要,灵活地定义该变量的值来达到想要的效果
-
在 ReentrantLock 中,使用 state 来表示当前线程获取锁的次数
-
在 CountDownLatch 中,使用 state 来表示当前线程执行 await() 方法前必须要执行的次数
-
-
-
资源共享方式
-
Share (共享)
-
static final Node SHARED = new Node(); -
只有一个线程能执行,如:ReentrantLock,可分为公平锁和非公平锁
-
-
Exclusive (独占)
-
static final Node EXCLUSIVE = null; -
多个线程可同时执行,如:Semaphore 或 CountDownLatch
-
-
三、设计模式
-
AQS 底层使用了模板方法模式,如果需要自定义同步器,一般方式如下
-
使用者继承 AQS,并重写特定的方法,主要重写以下 5 个方法
-
tryAcquire(int)
- 尝试获取独占锁,成功返回 true,失败返回 false
-
tryRelease(int)
- 尝试释放独占锁,成功返回 true,失败返回 false
-
tryAcquireShared(int)
- 尝试获取共享锁,负数表示失败;0 表示获取成功,但没有剩余可用资源;整数表示成功,且有剩余资源
-
tryReleaseShared(int)
- 尝试释放共享锁,如果释放后允许唤醒后继节点则返回 ture,否则返回 false
-
isHeldExclusively()
- 判断当前线程是否正在独占资源 (只有用到 Condition 时才需要去实现它)
-
-
将 AQS 组合在自定义同步组件的视线中,并调用其模板方法,这些模板方法则会调用使用者重写的方法
-
-
模板方法模式
-
模板方法模式,定义了一个操作中的算法的骨架,并将一些步骤延迟到子类中去实现,它使得子类可以在不改变一个算法结构的情况下,就可以重新定义该算法中的某些特定步骤
-
模板方法模式可以把子类中不变的行为抽象到父类,从而去除子类中重复的代码,帮助子类摆脱重复的不变行为的纠缠
-
四、源码方法详解
-
获取独占锁的流程
-
acquire(int arg)
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }-
获取独占锁,忽略中断
-
若获取成功,线程直接返回;若获取失败,线程会被阻塞,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止
-
可以用于实现
Lock.lock()方法
-
-
tryAcquire(int arg)
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }-
尝试获取独占锁,成功返回 true,失败返回 false
-
如果获取失败,则会阻塞当前线程,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止
-
可以用于实现
Lock.tryLock()方法
-
-
addWaiter(Node)
private Node addWaiter(Node mode) { // model: SHARED(共享)、EXCLUSIVE(独占) Node node = new Node(Thread.currentThread(), mode); Node pred = tail; // 判断队列尾部节点是否为null if (pred != null) { // 如果不为null,则将pred设置为node的前驱节点 node.prev = pred; // 使用CAS操作将尾部节点由pred更新为node if (compareAndSetTail(pred, node)) { // 将node设置为pred的后继节点 pred.next = node; return node; } } // 如果为null,则调用enq(Node)方法将其加入到队列尾部 enq(node); return node; }- 将当前线程和给定模式包裹成一个 Node 节点,并将其加入到 CLH 队列尾部
-
enq(Node)
private Node enq(final Node node) { for (;;) { Node t = tail; // 判断队列尾部节点是否为null if (t == null) { /* * 如果为null,则说明队列为空,创建一个新的Node * 节点作为队列头部节点,并将尾部节点也指向它 */ if (compareAndSetHead(new Node())) tail = head; } else { // 如果不为 null,则说明队列不为空,将t设置为node的前驱节点 node.prev = t; // 使用CAS将尾部节点由t更新为node if (compareAndSetTail(t, node)) { // 将node设置为t的后继节点 t.next = node; return t; } } } }- 将 Node 节点加入到 CLH 队列尾部
-
acquireQueued(final Node node, int arg)
final boolean acquireQueued(final Node node, int arg) { // 标记是否成功获取资源 boolean failed = true; try { // 标记等待过程中是否被中断过 boolean interrupted = false; for (;;) { // 获取当前节点的前驱节点p final Node p = node.predecessor(); // 如果p为head节点,则当前节点会尝试去获取资源 if (p == head && tryAcquire(arg)) { // 设置当前节点为head节点 setHead(node); /* * setHead中的node.prev已经置为null,此处再将head.next置为null * 从而方便GC回收之前的head节点,也就意味着之前拿完资源的节点出队了 */ p.next = null; // help GC failed = false; return interrupted; } /* * 如果未成功获取锁,则根据前驱节点状态来判断是否要阻塞当前节点 * shouldParkAfterFailedAcquire(p, node)方法在前驱节点状态不为 * SIGNAL时,会循环重试获取锁 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 如果未成功获取资源,则取消当前节点获取锁的尝试 if (failed) cancelAcquire(node); } }- 当前节点位于 CLH 队列中等待,直到其他节点释放资源,由当前节点获取并返回
-
shouldParkAfterFailedAcquire(Node pred, Node node)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 获取前驱节点状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * 如果前驱节点状态为SIGNAL(-1),则表明其在释放锁的时候会去 * 唤醒后继节点(即当前节点),所有后继节点房前可以阻塞自己 */ return true; if (ws > 0) { /* * 如果前驱节点状态为CANCELLED(1),则向前遍历 * 直到找到一个非取消状态的节点,并将该节点设置为当前节点的前驱节点 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 如果前驱节点状态为0或PROPAGATE(-3) * 则通过CAS操作将前驱节点状态设置为SIGNAL */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }- 根据前驱节点状态来判断是否要阻塞当前节点
-
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() { // 阻塞当前线程 LockSupport.park(this); return Thread.interrupted(); }- 调用 LockSupport 的 park() 方法来阻塞当前线程
-
cancelAcquire(Node node)
private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; /* * 如果前驱节点状态为CANCELLED(1),则向前遍历 * 直到找到一个非取消状态的节点,并将该节点设置为当前节点的前驱节点 */ Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // 记录pred的后继节点predNext,后续CAS会用到 Node predNext = pred.next; //将当前节点状态设置为CANCELLED(1) node.waitStatus = Node.CANCELLED; /* * 如果当前节点为tail节点,并且通过CAS操作将tail节点从node更新为pred * 则再通过CAS操作将pred的后继节点predNext更新为null,这样就断开了pred * 与其所有后继节点的联系,此时这些后继节点变得不可达,最终会被GC回收掉 */ if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; /* * 如果当前节点的后续节点next状态不是CANCELLED(1)的话 * 则通过CAS操作将next设置为当前节点的后继节点 */ if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { /* * 如果pred为head节点或pred为CANCELLED(1)状态或pred中的线程为null * 在这种情况下,为了保证队列的活跃性,需要去唤醒一次后继节点 * 举例说明: * 如果pred为head节点,则完全有可能当前已经没有线程持有锁了 * 也就不会有释放锁唤醒后继节点的动作,如果不唤醒后继节点 * 队列就挂掉了 */ unparkSuccessor(node); } /* * 将当前节点的后继节点设置为其本身,之所以不设置为null * 是因为为了方便AQS中Condition部分的isOnSyncQueue方法 * isOnSyncQueue: * 用于判断一个原先属于条件队列的节点是否转移到了同步队列 * 因为同步队列中会用到节点的next域,如果节点状态为CANCELLED(1) * 与此同时其next域也有值的话,则可以说明该节点一定位于同步队列上 * * 在GC层面上,和设置为null具有相同的效果 */ node.next = node; // help GC } }- 取消当前节点获取锁的尝试
-
unparkSuccessor(Node node)
private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 如果节点状态<0,则将其置为0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 后继节点 Node s = node.next; // 如果后继节点为空或已被取消(即CANCELLED状态) if (s == null || s.waitStatus > 0) { s = null; // 从后往前遍历Node节点 for (Node t = tail; t != null && t != node; t = t.prev) // 当发现第一个状态不为0的节点时,将其赋值给变量s if (t.waitStatus <= 0) s = t; } // 如果后继节点不为空,则将其唤醒 if (s != null) LockSupport.unpark(s.thread); }- 如果当前节点存在后继节点,则对其进行唤醒
-
-
释放独占锁的流程
-
release(int arg)
public final boolean release(int arg) { // 判断资源是否已被释放 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 唤醒后继节点 unparkSuccessor(h); return true; } return false; }-
释放独占锁,成功返回 true,失败返回 false
-
可以用于实现
Lock.unlock()方法
-
-
tryRelease(int arg)
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }- 尝试释放独占锁,成功返回 true,失败返回 false
-
-
获取共享锁的流程
-
acquireShared(int arg)
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }-
获取共享锁,忽略中断
-
若获取成功,线程直接返回;若获取失败,线程会被阻塞,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止
-
-
tryAcquireShared(int arg)
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }-
尝试获取共享锁,返回一个数值,负数表示失败;0 表示获取成功,但没有剩余可用资源;正数表示成功,且有剩余资源
-
如果获取失败,则会阻塞当前线程,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止
-
-
doAcquireShared(int arg)
private void doAcquireShared(int arg) { // 将Node节点加入到队列尾部 final Node node = addWaiter(Node.SHARED); // 标记是否成功获取资源 boolean failed = true; try { // 标记等待过程中是否被中断过 boolean interrupted = false; for (;;) { // 获取当前节点的前驱节点p final Node p = node.predecessor(); // 如果p为head节点,则当前节点会尝试去获取资源 if (p == head) { /* * 负数表示失败 * 0表示获取成功,但没有剩余可用资源 * 正数表示成功,且有剩余资源 */ int r = tryAcquireShared(arg); if (r >= 0) { // 将当前节点设置为head节点,并唤醒后继节点 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } /* * 如果未成功获取锁,则根据前驱节点状态来判断是否要阻塞当前节点 * shouldParkAfterFailedAcquire(p, node)方法在前驱节点状态不为 * SIGNAL时,会循环重试获取锁 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 如果未成功获取资源,则取消当前节点获取锁的尝试 if (failed) cancelAcquire(node); } }- 将当前线程和给定模式包裹成一个 Node 节点,并将其加入到 CLH 队列尾部等待,直到其他节点释放资源,由当前节点获取并返回
-
setHeadAndPropagate(Node node, int propagate)
private void setHeadAndPropagate(Node node, int propagate) { // 将旧的head节点封闭在方法栈上,用于下面的条件检查 Node h = head; // 将当前节点设置为head节点 setHead(node); // propagate为tryAcquireShared方法的返回值,是决定是否传播的依据之一,>0表示获取资源成功,且有剩余资源 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 根据当前节点的后继节点是否共享来决定是否传播唤醒 Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }- 设置当前节点为head节点,并根据tryAcquireShared方法返回的状态以及节点状态来判断是否需要唤醒后继线程
-
doReleaseShared()
private void doReleaseShared() { for (;;) { Node h = head; // 如果队列中存在后继线程 if (h != null && h != tail) { int ws = h.waitStatus; // 如果节点状态为SIGNAL if (ws == Node.SIGNAL) { // 使用CAS操作将节点状态由SIGNAL改为0,若操作失败则跳过本次循环 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒后继节点 unparkSuccessor(h); } // 如果节点状态为0,需要使用CAS操作来设置节点状态为PROPAGATE,用以保证唤醒的传播 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 检查h是否仍然为head,如果不是则需要重新循环 if (h == head) // loop if head changed break; } }-
唤醒后继节点并设置传播状态,或者由于多个线程同时释放共享锁,导致处在中间过程时读取到head节点状态为0,此时为了保证唤醒能过正确稳定地传递下去,设置 head 节点状态为 PROPAGATE(-3)
-
这样在获取共享锁,执行 setHeadAndPropagate 方法时,则可以读取到 PROPAGATE(-3) 状态,从而由获取锁的线程去释放锁并唤醒后继线程
-
-
-
释放共享锁的流程
-
releaseShared(int arg)
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }- 释放共享锁,成功返回 true,失败返回 false
-
tryReleaseShared(int arg)
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }- 尝试释放共享锁,成功返回 true,失败返回 false
-
五、获取独占锁流程图


浙公网安备 33010602011771号