【Java 并发】AbstractQueuedSynchronizer

1 AQS 简介

在同步组件的实现中, AQS 是核心部分, 同步组件的实现者通过使用 AQS 提供的模板方法实现同步组件语义。
AQS 则实现了对同步状态的管理, 以及对阻塞线程进行排队, 等待通知等一些底层的实现处理。
AQS 的核心也包括了这些方面: 同步队列, 独占式锁的获取和释放, 共享锁的获取和释放以及可中断锁, 超时等待锁获取这些特性的实现,
而这些实际上则是 AQS 提供出来的模板方法, 归纳整理如下:

在 Java 并发编程领域中, AbstractQueuedSynchronizer (AQS) 是一项功能强大且设计精巧的工具。
它为开发人员提供了一种高效的同步机制, 用于安全地控制多线程环境下的资源访问和状态管理。

其本身的设计很简单, 内部维护 1 个 int 的状态和 1 个链表

  1. 一个线程过来获取锁 (本质就是通过 cas 修改 int 的状态), 获取锁成功 (int 状态修改成功), 线程继续执行
  2. 一个线程过来获取锁, 获取锁失败, 则将线程封装为链表的一个节点, 放入链表中, 然后挂起
  3. 获取锁的线程执行完逻辑, 释放锁, 就唤醒链表的头节点, 重新尝试获取锁, 获取成功, 从链表移除, 执行逻辑 (这个过程可能有从外部来的线程进行竞争)

上面是 AQS 非公平锁的大体过程, AQS 本身还提供了公平锁的实现, 为了实现这些锁的逻辑,
AQS 本身还需要支持 同步队列, 独占式锁的获取和释放, 共享锁的获取和释放以及可中断锁, 超时等待锁获取等功能
而这些功能本身复杂度高同时还是高频的逻辑, 所以 AQS 本身借助了模板方法的设计模式, 将常用的逻辑封装起来, 然后让子类去实现自己锁获取释放的逻辑。
大体的逻辑如下:

public abstract class AbstractQueuedSynchronizer {public void lock() {// 1. 尝试获取锁// 由子类决定当前线程是否获取锁成功if (tryAcquire()) {// 获取成功, 直接返回return;}// 2. 获取锁失败, 将线程封装为节点, 放入队列, 然后挂起// 这些逻辑由 AQS 内部进行实现addNodeToQueueAndPark();}// 由子类进行实现protected abstract boolean tryAcquire();
}

而这些实际上则是 AQS 提供出来的模板方法, 归纳整理如下:

独占式锁相关的方法

// 独占式获取同步状态, 如果获取失败则插入同步队列进行等待
void acquire(int arg);// 与 acquire 方法相同, 但在同步队列中进行等待的时候可以检测中断
void acquireInterruptibly(int arg);// 在 acquireInterruptibly 基础上增加了超时等待功能, 在超时时间内没有获得同步状态返回 false
boolean tryAcquireNanos(int arg, long nanosTimeout);// 释放同步状态, 该方法会唤醒在同步队列中的下一个节点
boolean release(int arg);

共享式锁相关的方法

// 共享式获取同步状态, 与独占式的区别在于同一时刻有多个线程获取同步状态
void acquireShared(int arg);// 在 acquireShared 方法基础上增加了能响应中断的功能
void acquireSharedInterruptibly(int arg);// 在 acquireSharedInterruptibly 基础上增加了超时等待的功能
boolean tryAcquireSharedNanos(int arg, long nanosTimeout);// 共享式释放同步状态
boolean releaseShared(int arg);

本身了解这些模板方法的逻辑, 就能够很好的理解 AQS 的设计思想, 以及后续的同步组件的实现。

2 AQS 同步队列

AQS 内部核心的 2 个变量, 1 个 int 的状态值, 1 个同步队列。
int 的状态值本身没有多大的问题, 但是链表本身有一点设计, 所以这里对 AQS 的链表做个简单的介绍, 便于后面 AQS 的理解。

在 AQS 有一个静态内部类 Node (只列举了部分重要的属性)

static final class Node {/******************** 属性  **************************/// 节点状态volatile int waitStatus;// 当前节点的前驱节点volatile Node prev;// 当前节点的后驱节点volatile Node next;// 加入同步队列的线程引用volatile Thread thread;// 等待队列中的下一个节点Node nextWaiter;/******************** 节点模式  **************************/// 标识节点为独占模式static final Node SHARED = new Node();// 标识节点为独占模式static final Node EXCLUSIVE = null;/******************** 节点状态  **************************/// 节点从同步队列中取消int CANCELLED = 1; // 等待唤醒的状态int SIGNAL = -1;// 当前节点进入等待队列中int CONDITION = -2;// 在共享锁的释放中, 会从头节点向后逐个唤醒状态为 signal 的节点的线程, 直到遇到第一个状态为 0 的, 停下来, 会将其从 0 设置为 -3// 表示下一次共享式同步状态获取将会无条件传播下去int PROPAGATE = -3;// 初始状态int INITIAL = 0;
}

从上面的节点的属性可以知道每个节点有前驱节点 prev 和后驱节点 next, 所以可以知道同步队列的真实实现是一个双向链表。

另外 AQS 自身的属性中有两个重要的成员变量:

public abstract class AbstractQueuedSynchronizer {// 同步队列的头节点private transient volatile Node head;// 同步队列的尾节点private transient volatile Node tail;
}

结合 2 个属性, 可以得出 AQS 中维护的同步队列的结构如下:

Alt 'AQS 双向链表的结构'

同时, 我们也可以大概分析出节点加入同步队列的过程:

// 1. 将线程封装为节点
// 2. 将节点设置到双写链表的尾部
// 3. 修改 AQS 的 tail 指向新的节点

退出链表的逆推就行了, 这里就不再赘述了。

3 AQS 中的独占锁实现

3.1 独占锁的获取 - acquire 方法

public final void acquire(int arg) {// 调用需要子类实现的 tryAcquire() 方法, 尝试获取锁// 1. 获取锁成功了, 方法结束// 2. 获取锁失败, 将当前线程封装为 Node 节点, 放到等待队列中, 等待唤醒// 3. acquireQueued 方法返回 true 表示当前线程需要中断了, 设置线程的中断标识为 trueif (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 设置当前的线程的中断标识为 true selfInterrupt();
}
3.1.1 acquire 中的入队操作 - addWaiter 方法
// 当前使用的为 OpenJdk 11 版本, 可能会有出入
// 入参的 mode 为 Node.EXCLUSIVE 或者 Node.SHARED, 表示当前节点的模式为独占模式或者共享模式
private Node addWaiter(Node mode){// 1 将当前线程封装成一个 Node 节点, 这个节点的下一个等待的节点的模式, 既 Node.EXCLUSIVE 或 Node.SHARED// 通过这个下一个节点的模式可以间接等待当前节点模式Node node = new Node(Thread.currentThread(), mode);// 死循环for (;;){// 取到当前链表的尾节点Node oldTail = tail;// 2 当前尾节点是否为 nullif (oldTail != null){// 2.2 设置新的节点的前驱节点为当前链表的尾节点node.setPrevRelaxed(oldTail);// 通过 CAS 把当前节点设置为尾节点if (compareAndSetTail(oldTail, node)){// 旧的尾节点的下一个节点为当前的新节点oldTail.next = node;return node;}} else{// 2.1 当前同步队列尾节点为 null, 说明当前线程是第一个加入同步队列进行等待的线程, 初始化同步队列// 同步队列这时候不为空了, 又执行一次循环initializeSyncQueue();}}
}private final void initializeSyncQueue() {Node h;// 创建出一个空的 Node 节点, 通过 CAS 操作尝试将其变为头节点, 再将尾节点的指针指向新创建的节点if (HEAD.compareAndSet(this, null, (h = new Node())))tail = h;
}

分析可以看上面的注释。
程序的逻辑主要分为两个部分:

  1. 当前同步队列的尾节点为 null, 调用方法 initializeSyncQueue(), 初始出一个头部没有任何信息的链表, 然后回来, 重写回到循环, 再次尝试把当前节点放到链表的尾部
  2. 当前队列的尾节点不为 null, 则采用尾插入 (compareAndSetTail() 方法) 的方式入队
3.1.2 acquire 中的在等待队列唤醒 - acquireQueued 方法

获取独占式锁失败的线程会包装成 Node, 然后插入等待同步队列。
在同步队列中的节点 (线程) 会做什么事情来保证自己能够有机会获得独占式锁了?
带着这样的问题我们就来看看 acquireQueued() 方法, 从方法名就可以很清楚, 这个方法的作用就是排队获取锁的过程, 源码如下:

final boolean acquireQueued(final Node node, int arg) {// 是否需要通知当前线程中断boolean interrupted = false;try {for (;;) {// 获取当前节点的前驱节点final Node p = node.predecessor();// 2 前驱节点是头节点并且成功获取同步状态, 即可以获得独占式锁// 在上面创建 addWaiter 方法可以知道, 同步队列为空, 会创建一个默认值的头节点 head, 再把新节点放到这个头节点前面// 如果一个节点的前驱节点为头节点, 就可以判断出这个节点为链表中真正数据的第一个节点if (p == head && tryAcquire(arg)) {// 当前节点设置为 头节点// 设置头节点 = node// 设置 node.thread = null// 设置 node.prev = null// 这时候头节点的状态为 signal (-1)setHead(node);p.next = null;return interrupted;}// 3 获取锁失败, 线程进入等待状态等待获取独占式锁// shouldParkAfterFailedAcquire 主要是判断当前的节点里面的线程是否可以挂起, // 返回 true 的条件: node 的前驱节点的状态为 signal (等待唤醒的状态), 前驱在等待唤醒, 那么这个节点先挂起// parkAndCheckInterrupt 这时会挂起线程, 阻塞住, 直到被唤醒获取中断if (shouldParkAfterFailedAcquire(p, node))// | 或运算, 只要有一个真, 就是真// interrupted 默认为 false, parkAndCheckInterrupt() 返回了 true, 那么 interrupted 就会为 trueinterrupted |= parkAndCheckInterrupt();}} catch (Throwable t) {// 上面的逻辑出现了异常了, 正常的情况就是线程的中断标识为 true, 但是挂起了, 或者挂起中, 被中断了// 取消获取锁cancelAcquire(node);// 需要设置中断标识, if (interrupted)selfInterrupt();throw t;}
}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;// 前驱节点的状态为 signal // signal 表示等待唤醒的状态, 安全的, 当前线程可以挂起if (ws == Node.SIGNAL)return true;// > 0, 状态为取消状态if (ws > 0) {    // 从当前节点一直往前找到第一个状态不为 CANCELLED (1) 的节点,// 也就是找到链表中前面中最接近当前节点, 同时状态不为 CANCELLED (1), 将当前节点放到这个节点的后面, 中间的节点舍弃掉// 效果: 从当前节点到第一个不为 CANCELLED 状态的节点之间所有的 CANCELLED 状态的节点都被删除do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将前驱节点设置为 SIGNAL 状态, 表示节点里面的线程等待唤醒pred.compareAndSetWaitStatus(ws, Node.SIGNAL);}// 返回 false, 表示当前的线程还不能挂起, 再走一遍循环return false;
}private final boolean parkAndCheckInterrupt() {// 使当前线程挂起, 直到被唤醒LockSupport.park(this);// 返回当前线程的中断标识return Thread.interrupted();
}

到这里就应该清楚了, acquireQueued() 在自旋过程中主要完成了两件事情:

1 如果当前节点的前驱节点是头节点, 并且再次尝试, 能够获取到同步状态的话 (即获取到锁), 直接返回, 让线程能哥继续执行, 否则进入下一步
2 获取锁失败的话, 会根据前驱节点的状态进行处理 (如下)

2.1 前驱节点的状态为 CANCELLED, 从当前节点一直往前找到第一个不是取消状态的节点, 将当前节点放到其后面, 重新执行 acquireQueued 方法的逻辑
2.2 前驱节点不是 SIGNAL 和 CANCELLED, 将前驱节点设置为 SIGNAL 状态, 重新执行 acquireQueued 方法的逻辑
2.3 前驱节点为 SIGNAL 状态, 把当前线程挂起来。等待被唤醒

到这里可以看出独占锁的特点

  1. 线程进来, 就直接尝试获取同步状态, 获取成功, 直接返回
  2. 获取失败, 就将线程封装为节点, 放入等待链表, 然后挂起
3.1.3 acquire 中等待队列唤醒异常 - cancelAcquire 方法

在上面的 acquireQueued 方法中, 线程的中断标识为 true, 尝试挂起会失败, 这时候会让这个线程取消获取锁的逻辑

private void cancelAcquire(Node node) {// 节点为 null, 直接结束if (node == null)return;// 设置节点的线程为 null node.thread = null;Node pred = node.prev;// 从当前的节点往前找到第一个状态为取消状态 (1) 的节点, 也就是当前链表中最后一个状态为取消状态的节点while (pred.waitStatus > 0)// 设置当前节点的前缀节点为这个取消状态节点的前驱节点node.prev = pred = pred.prev;// 这里的 predNext 就是当前链表中最后一个状态为取消状态的节点, 为下面的 cas 使用Node predNext = pred.next;   // 当前节点的状态设置为取消状态(1)node.waitStatus = Node.CANCELLED; // 当前节点就是为节点, 通过 cas 将当前链表的尾节点从当前节点设置为找到的节点if (node == tail && compareAndSetTail(node, pred)) {// 设置找到的节点的下一个节点从 predNext 设置为 nullpred.compareAndSetNext(predNext, null);} else {int ws;// 找到的节点不是头节点, 同时节点的线程不为空// 加上 节点的状态为 signal 或者 不是取消状态下, 能设置为 signal 状态// 后面的判断最少为了确保找到的节点为 signal 状态if (pred != head && pred.thread != null && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL)))) {// 当前节点的下一个节点Node next = node.next;// 下一个节点不为空, 同时状态不是取消状态, 将找到的节点的下一个节点设置为当前节点的下一个节点if (next != null && next.waitStatus <= 0)pred.compareAndSetNext(predNext, next);} else {// 找到的节点为头节点// 找到的节点的线程为空// 找到的节点的状态为取消状态// 都会执行到这个方法, 唤醒这个节点后面的第一个状态小于等于 0 的线程unparkSuccessor(node);}// 协助 gcnode.next = node; }
}private void unparkSuccessor(Node node) {int ws = node.waitStatus;// 当前的节点状态为不是初始状态或者取消状态, 设置为默认值 0, 初始状态if (ws < 0)node.compareAndSetWaitStatus(ws, 0);// 下一个节点Node s = node.next;if (s == null || s.waitStatus > 0) {  s = null;// 从后往前找到, 找到第一个状态不为取消的节点和初始状态的节点for (Node p = tail; p != node && p != null; p = p.prev)if (p.waitStatus <= 0)s = p;}  // 找到了进行唤醒if (s != null)LockSupport.unpark(s.thread);    
}

取消获取锁的过程看起来很绕, 实际整理起来很简单

  1. 清除当前节点和它前面的到第一个非取消状态的节点之间所有取消状态的节点
  2. 如果找到的节点为头节点 (注意了头节点为没有任何信息的节点), 尝试从当前节点往后找到第一个不为取消状态的节点, 唤醒它

3.2 独占锁的释放 - release 方法

独占锁的释放就相对来说比较容易理解了, 废话不多说先来看下源码:

public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;// 头节点存在, 同时状态不为 0 (初始状态)// 判断 != 0 的作用下面分析if (h != null && h.waitStatus != 0)// 唤醒头节点的下一个节点unparkSuccessor(h);return true;}return false;
}

首先获取头节点的后驱节点, 后驱节点存在并且状态不为取消状态, 唤醒这个线程。

如果不存在后驱节点或者后驱节点为取消状态, 会尝试从尾节点往前找到第一个状态不为取消状态和初始状态的节点, 同时这个节点不是当前的节点, 找到了会唤醒这个节点对应的线程。

  1. 假设现在有一个锁, 线程 A 通过 acquire 获取到了锁, 经过上面的上面的代码, 可以知道, 这时没有同步队列还没创建
  2. 线程 B 这时候通过 acquire 尝试获取锁失败了, 会创建出一个链表, 把自己封装为节点 B 放到链表的后面
  3. acquireQueued 方法中的死循环会一直判断到当前的节点的前驱节点为头节点, 会不断重试获取锁, 而不会挂起
  4. 这时候线程 A 要释放锁了, 不需要唤醒头节点的下一个节点, 在第三步中会自己唤醒
  5. 在线程 A 释放锁之前, 又要线程 C 尝试获取锁, 失败了, 拼接到节点 B 的后面, 节点 C, 这时候会被挂起
  6. 第三步中, 线程 B 获取锁成立, 会将 B 节点设置为头节点, 清空里面的前驱节点, 线程信息等, 保留下了状态 signal (-1)
  7. 后面线程 B 释放锁, 状态不为 0 了, 就能进入唤醒 C 的过程
  8. C 唤醒后, 重新执行 acquireQueued 的方法, 这是 C 的前置节点为原本的节点 B, 将自己的节点 C 设置为头节点, 这时候的链表只有一个原本节点 C 的节点了

所以最终的独占锁的处理如下:

  1. 线程获取锁失败, 线程被封装成 Node 进行入队操作, 核心方法在于 addWaiter(), 同时 addWaiter() 会在队列为 null 的时候进行初始化。同时通过不断的 CAS 操作将节点存到当前队列的尾部
  2. 线程获取锁是一个自旋的过程, 当且仅当当前节点的前驱节点是头节点并且成功获得同步状态时, 节点出队即该节点引用的线程获得锁, 否则, 当不满足条件时就会调用 LookSupport.park() 方法使得线程阻塞
  3. 释放锁的时候会唤醒后继节点

总体来说:
在获取同步状态时, AQS 维护一个同步队列, 获取同步状态失败的线程会加入到链表中进行挂起, 从链表移除 (或唤醒) 的条件是前驱节点是头节点并且成功获得了同步状态。在释放同步状态时, 同步器会调用 unparkSuccessor() 方法唤醒后驱节点

3.3 可中断式独占锁的获取 - acquireInterruptibly 方法

我们知道 lock 相较于 synchronized 有一些更方便的特性, 比如能响应中断以及超时等待等特性, 现在我们依旧采用通过学习源码的方式来看看能够响应中断是怎么实现的。
可响应中断式锁可调用方法 lock.lockInterruptibly()。

而该方法其底层会调用 AQS 的 acquireInterruptibly 方法, 源码为:

public final void acquireInterruptibly(int arg) throws InterruptedException {// 线程的中断标识为 true, 直接抛出异常if (Thread.interrupted())throw new InterruptedException();// 尝试获取锁失败   if (!tryAcquire(arg))doAcquireInterruptibly(arg);
}private void doAcquireInterruptibly(int arg) throws InterruptedException {// 将节点存入到 同步等待链表final Node node = addWaiter(Node.EXCLUSIVE);try {for (;;) {// 获取前驱节点final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {// help GCsetHead(node);p.next = null; return;}// shouldParkAfterFailedAcquire 判断当前线程是否可以挂起// parkAndCheckInterrupt 挂起当前线程, 唤醒后, 判断线程的中断标识是否为 true, 这里为 true, 就会直接抛出异常, 结束死循环, 进入 catch 里面的逻辑if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())throw new InterruptedException();}} catch (Throwable t) {// 取消获取锁cancelAcquire(node);throw t;}}

与 acquire 方法逻辑几乎一致, 唯一的区别是当 parkAndCheckInterrupt 返回 true, 即线程阻塞时该线程被中断, 代码抛出被中断异常。

3.4 带超时等待时间的独占锁的获取 - tryAcquireNanos 方法

通过调用 lock.tryLock(timeout,TimeUnit) 方式达到超时等待获取锁的效果, 该方法会在三种情况下才会返回:

  1. 在超时时间内, 当前线程成功获取了锁
  2. 当前线程在超时时间内被中断
  3. 超时时间结束, 仍未获得锁返回 false

该方法会调用 AQS 的方法 tryAcquireNanos(), 源码为


public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {// 线程的中断标识为 trueif (Thread.interrupted())throw new InterruptedException();// 先尝试获取锁, 获取锁成功, 直接返回// 获取锁失败, 调用实现超时等待的方法return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {// 等待的时间小于 0, 直接返回if (nanosTimeout <= 0L)return false;// 得到最终结束等待的时间点    final long deadline = System.nanoTime() + nanosTimeout;   // 把当前节点加入到等待链表final Node node = addWaiter(Node.EXCLUSIVE);try {for (;;) {// 前驱节点为头结点, 同时获取锁成功, 将当前节点置为头结点final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null;return true;}// 1 计算超时时间nanosTimeout = deadline - System.nanoTime();// 2 判断是否到了结束的时间点if (nanosTimeout <= 0L) {// 将当前节点从队列里面删除cancelAcquire(node);return false;}// 3// 判断可以挂起线程, 同时设置的超时时间 > SPIN_FOR_TIMEOUT_THRESHOLD = 1000L, 即超时时间大于 1 秒// 带超时时间的挂起线程if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)LockSupport.parkNanos(this, nanosTimeout);// 线程的中断标识为 trueif (Thread.interrupted())throw new InterruptedException();    }} catch (Throwable t) {// 取消获取锁cancelAcquire(node);throw t;}}

程序逻辑同独占锁可响应中断式获取基本一致, 唯一的不同在于获取锁失败后, 对超时时间的处理上。
先计算出按照现在时间和超时时间计算出理论上的截止时间 deadline, 然后 deadline - System.nanoTime() 计算出来就是一个负数, 自然而然会在第 2 步中的 if 判断之间返回 false。
如果还没有超时即第 2 步中的 if 判断为 true 时就会继续执行第 3 步。

4 AQS 中的共享锁实现

4.1 共享锁的获取 - acquireShared 方法

public final void acquireShared(int arg) {// 调用子类重写的获取共享锁方法// 返回了大于 0 的值, 表示获取锁// 共享锁的 tryAcquireShared 的返回值, 代表了锁当前有多少个持有者// 0 表示无锁状态, 返回 1 表示有 1 个持有者, 返回 2 表示锁已经有 2 个持有者if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
}private void doAcquireShared(int arg) {// 把节点加入等待链表中final Node node = addWaiter(Node.SHARED);boolean interrupted = false;try {for (;;) {// 获取前驱节点final Node p = node.predecessor();// 前驱节点为头节点if (p == head) {// 获取锁int r = tryAcquireShared(arg);// 获取锁成功if (r >= 0) {setHeadAndPropagate(node, r);p.next = null;return;}}// 判断是否可以挂起线程if (shouldParkAfterFailedAcquire(p, node))interrupted |= parkAndCheckInterrupt();}} catch (Throwable t) {cancelAcquire(node);throw t;} finally {if (interrupted)selfInterrupt();}
}

共享锁的获取逻辑和独占式锁的获取差不多, 这里的线程退出死循环的条件: 当前节点的前驱节点是头节点并且 tryAcquireShared(arg) 返回值大于等于 0 即能成功获得同步状态

和独占锁的获取不同的点在于

  1. 独占锁的获取成功, 只会把自己的节点移除
  2. 共享锁的获取成功, 则复杂了很多, 除了唤醒自己, 还需要把其他共享的节点也唤醒

4.1.1 acquireShard 中在等待代理中唤醒后的行为 - setHeadAndPropagate 方法

private void setHeadAndPropagate(Node node, int propagate) {Node h = head;// 将当前节点设置为头节点, 清空线程信息setHead(node);// 持有共享锁的线程数大于 0 // 头节点为 null// 头节点的状态为不是取消状态// 新的头节点为 null// 新的头节点的状态不是取消状态if (propagate > 0 || h == null || h.waitStatus < 0 ||  (h = head) == null || h.waitStatus < 0) {Node s = node.next;// 下一个节点为 null 或者为共享节点if (s == null || s.isShared())// 尝试是否共享锁doReleaseShared();}
}private void doReleaseShared() {// 从当前的头节点开始, 向后处理, 把后面所有的状态为 signal 的节点唤醒,// 直到遇到第一个节点状态不为 SIGNAL 的, 停止, 同时把这个节点的状态设置为 PROPAGATEfor (;;) {// 获取头节点Node h = head;// 头节点不为 null 同时 头节点不等于尾节点if (h != null && h != tail) {// 获取头节点的状态int ws = h.waitStatus;// 头节点的状态等于 signal if (ws == Node.SIGNAL) {// 通过 cas 将头节点从 signal 设置为 0if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))// 设置失败了, 重新开始循环continue;  // 获取后驱节点    unparkSuccessor(h);// 状态为 0, 则通过 cas 将其从 0 设置为 -3, 设置失败了, 则继续回到头部,} else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE))continue;}if (h == head)break;}
}

大体的逻辑如下:

  1. 把当前的节点设置为头节点
  2. 如果头节点的下一个节点为共享节点, 向后处理, 把后面所有的状态为 signal 的节点唤醒, 直到遇到第一个节点状态为 0 的, 停止, 同时把这个节点的状态设置为 PROPAGATE

4.2 共享锁的释放 - releaseShared 方法

public final boolean releaseShared(int arg) {// 尝试释放锁if (tryReleaseShared(arg)) {// 从当前的头节点开始, 向后处理, 把后面所有的状态为 signal 的节点唤醒, 直到遇到第一个节点状态为 0 的, 停止, 同时把这个节点的状态设置为 PROPAGATEdoReleaseShared();return true;}return false;
}

4.3 共享锁的其他方法

  1. 可中断式的共享锁获取 acquireSharedInterruptibly
  2. 带超时等待时间的共享锁获取 tryAcquireSharedNanos

其实现和独占式锁可中断获取锁以及超时等待的实现几乎一致, 具体的就不再说了

5 参考

深入理解AbstractQueuedSynchronizer(AQS)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/746889.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

做一个微信小程序需要什么条件?

在移动互联网时代&#xff0c;微信小程序已成为连接用户与服务的重要桥梁。它们无需下载安装&#xff0c;使用方便&#xff0c;功能强大&#xff0c;成为商家和开发者的新宠。如果你想要开发一个微信小程序&#xff0c;需要满足哪些条件&#xff1f; 一、开发小程序的基本条件…

Redis 持久化-RDB

RDB&#xff08;Redis DataBase&#xff09;&#xff0c;在指定的时间间隔内将内存中的数据集快照写入磁盘&#xff0c;也就是行话讲的Snapshot快照&#xff0c;它恢复时是将快照文件直接读到内存里。 Redis会单独创建&#xff08;fork&#xff09;一个子进程来进行持久化&…

用网格大师网络重划分时,坐标原点按照XML文件原点填写,导出的瓦块文件命名不对,坐标原点该怎么填写

瓦块名称和原来不一致的原因是瓦块原点这里设置的不对&#xff0c;要和重建设置的瓦块原点保持一致。 网格大师是一款能够解决实景三维模型空间参考、原点、瓦块大小不统一&#xff0c;重叠区域处理问题的工具“百宝箱”&#xff0c;集格式转换、坐标转换、轻量化、瓦片重划分…

ModStartBlog v9.1.0 桌面快捷应用,图片上传前端压缩

ModStart 是一个基于 Laravel 模块化极速开发框架。模块市场拥有丰富的功能应用&#xff0c;支持后台一键快速安装&#xff0c;让开发者能快的实现业务功能开发。 系统完全开源&#xff0c;基于 Apache 2.0 开源协议。 功能特性 丰富的模块市场&#xff0c;后台一键快速安装 …

淘宝商品销量数据接口,淘宝API接口

淘宝商品销量数据接口是淘宝开放平台提供的一种API接口&#xff0c;通过该接口&#xff0c;商家可以获取到淘宝平台上的商品销量数据。 淘宝商品销量数据接口可以用于获取特定商品的销量数据、特定店铺的销量数据、特定类目的销量数据等。商家可以根据自己的需求来调用该接口&…

控制学习_正弦波无刷直流力矩电机建模、控制带宽讨论与选择

无刷电机通过电子换向器实现定子的磁场旋转&#xff0c;去电刷后使用寿命大幅提升&#xff0c;是现在更流行的选择。三相无刷电机则是无刷电机中比较流行的一款。三相无刷电机的驱动方式有多种&#xff0c;最简单的被称为梯形波驱动、方波驱动或正弦波驱动。而正弦波驱动技术可…

前端路由跳转bug

路由后面拼接了id的千万不能取相近的名字&#xff0c;浏览器分辩不出&#xff0c;只会匹配前面的路径 浏览器自动跳转到上面的路径页面&#xff0c;即使在菜单管理里面配置了正确的路由 跳转了无数次&#xff0c;页面始终不对&#xff0c;检查了路由配置&#xff0c;没有任何问…

java枚举与模拟方法

枚举 枚举的定义 枚举算法&#xff08;穷举算法&#xff09;&#xff0c;这种算法就是在解决实际问题的时候去使用所有的方式去解决这个问题&#xff0c;会通过推理去考虑事件发生的每一种可能性&#xff0c;最后推导出结果 优点 简单粗暴&#xff0c;他暴力的枚举所有可能&…

3.14号arm

1. 计算机基础理论 1.1 计算机的组成 输入设备&#xff1a;将数据转换成计算机可以识别&#xff0c;存储&#xff0c;处理的形式&#xff0c;发送到计算机中 输出设备&#xff1a;将计算机对程序和数据的运算结果输送到外部的设备 存储器&#xff1a;用于将数据保存的模块。 …

【火猫TV】欧联:8强无弱队,意甲英超球队扎堆,利物浦和药厂优势很大!

2023-2024欧联杯八强正式出炉&#xff0c;马赛、本菲卡、勒沃库森、利物浦、西汉姆联、罗马、亚特兰大和AC米兰成功拿到了晋级名额&#xff0c;于是球迷们即将看到近20年竞争最激烈的欧联杯八强对决。虽然这些队伍实力有强有弱&#xff0c;但是能够拼到现在的球队没有弱旅&…

1335:【例2-4】连通块

【算法分析】 设数组vis&#xff0c;vis[i][j]表示(i,j)位置已经访问过。遍历地图中的每个位置&#xff0c;尝试从每个位置开始进行搜索。如果该位置不是0且没有访问过&#xff0c;那么访问该位置&#xff0c;并尝试从其上下左右四个位置开始搜索。在看一个新的位置时&#xff…

医疗设备控费系统防止私收、漏收、人情费

加19339904493&#xff08;康&#xff09; 医院完成信息化建设&#xff0c;不仅是一次技术性人深过信息化技术&#xff0c;医院能够更好地管理病患信息&#xff0c;提高诊断的准确性和效率&#xff0c;同时优化医疗资源的配置&#xff0c;降低医疗成本。在信息化的推动下&#…

Git概述及安装步骤

一、Git简介 Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。Git 易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。其性能优于Subversion、CV…

Linux下的多线程编程:原理、工具及应用(1)

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;Flower of Life—陽花 0:34━━━━━━️&#x1f49f;──────── 4:46 &#x1f504; ◀️ ⏸ ▶️ ☰ …

文献速递:深度学习乳腺癌诊断---使用深度学习改善乳腺癌组织学分级

Title 题目 Improved breast cancer histological grading using deep learning 使用深度学习改善乳腺癌组织学分级 01 文献速递介绍 乳腺癌组织学分级是乳腺癌中一个确立的临床变量&#xff0c;它包括来自三个方面的信息&#xff0c;即小管形成程度、核多态性和有丝分裂计…

IP证书有什么作用?怎么申请?

关于IP地址证书&#xff0c;它的主要作用有这么几个点&#xff1a; 1.验明正身&#xff1a;就像身份证一样&#xff0c;它可以证明某个服务器的IP地址是真的、合法的&#xff0c;让咱知道咱们连接的就是正确的服务器&#xff0c;而不是冒牌货。这样一来&#xff0c;就可以降低像…

【C++设计模式】策略模式

文章目录 前言一、策略模式是什么&#xff1f;二、策略模式的实现原理三、UML图四、代码实现总结 前言 策略模式是一种行为设计模式&#xff0c;它允许在运行时选择算法的行为。通过将每个算法封装到具有共同接口的独立类中&#xff0c;客户端可以在不改变自身代码的情况下选择…

Bugku MISC做题笔记

简单套娃DX 这一题需要对png图片的结构有所了解。详细可参考https://www.w3.org/TR/png/ 幸好每一张图片只有一个错误&#xff0c;逐步调试&#xff0c;就可以发现所有错误&#xff0c;修正即可。具体错误参看python程序中的注释&#xff1a; import ossrc_dir .\\XD\\ de…

微信每天通过好友上限是多少个呢?

微信每天通过好友上限是多少个呢&#xff1f; 1、新号和不活跃的号 微信新号是指注册不满15十五天&#xff0c;或者注册超过15天&#xff0c;但是没有好好养号的的账号。&#xff08;包括很多长期不活跃的账号&#xff0c;突然使用的情况&#xff09; 2、正常帐号 &#xf…

案例分析篇06:数据库设计相关28个考点(17~22)(2024年软考高级系统架构设计师冲刺知识点总结系列文章)

专栏系列文章推荐: 2024高级系统架构设计师备考资料(高频考点&真题&经验)https://blog.csdn.net/seeker1994/category_12593400.html 【历年案例分析真题考点汇总】与【专栏文章案例分析高频考点目录】(2024年软考高级系统架构设计师冲刺知识点总结-案例分析篇-…