AbstractQueuedSynchronizer 队列同步器(AQS)

队列同步器 (AQS), 是用来构建锁或其他同步组件的基础框架,它通过使用 int 变量表示同步状态,通过内置的 FIFO 的队列完成资源获取的排队工作。(摘自《Java并发编程的艺术》)

我们知道获取同步状态有独占和共享两种模式,本文先针对独占模式进行分析。

变量定义

private transient volatile Node head;

head 同步队列头节点

private transient volatile Node tail;

tail 同步队列尾节点

private volatile int state;

state 同步状态值

Node - 同步队列节点定义

volatile int waitStatus;

waitStatus 节点的等待状态,可取值如下 :

  • 0 : 初始状态
  • -1 : SIGNAL 处于该状态的节点,说明其后置节点处于等待状态; 若当前节点释放了锁可唤醒后置节点
  • -2 : CONDITION 该状态与 Condition 操作有关后续在说明
  • -3 : PROPAGATE 该状态与共享式获取同步状态操作有关后续在说明
  • 1 : CANCELLED 处于该状态的节点会取消等待,从队列中移除
volatile Node prev;

prev 指向当前节点的前置节点

volatile Node next;

next 指向当前节点的后置节点

volatile Thread thread;

thread 节点对应的线程也是指当前获取锁失败的线程

Node nextWaiter;

acquire()

独占模式下获取同步状态, 既是当前只允许一个线程获取到同步状态

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

从 acquire 方法中我们可以大概猜测下,获取锁的过程如下:

  • tryAcquire 尝试获取同步状态, 具体如何判定获取到同步状态由子类实现
  • 当获取同步状态失败时,执行 addWaiter 创建独占模式下的 Node 并将其添加到同步队列尾部
  • 加入同步队列之后,再次尝试获取同步状态,当达到某种条件的时候将当前线程挂起等待唤醒

下面具体看下各个阶段如何实现:

private Node addWaiter(Node mode) {
// 绑定当前线程 创建 Node 节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 判断同步队列尾节点是否为空
if (pred != null) {
// node 的前置节点指向队列尾部
node.prev = pred;
// 将同步队列的 tail 移动指向 node
if (compareAndSetTail(pred, node)) {
// 将原同步队列的尾部后置节点指向 node
pred.next = node;
return node;
}
}
// tail 为空说明同步队列还未初始化
// 此时调用 enq 完成队列的初始化及 node 入队
enq(node);
return node;
}
private Node enq(final Node node) {
// 轮询的方式执行
// 成功入队后退出
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 创建 Node, 并将 head 指向该节点
// 同时将 tail 指向该节点
// 完成队列的初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
// node 的前置节点指向队列尾部
node.prev = t;
// 将同步队列的 tail 移动指向 node
if (compareAndSetTail(t, node)) {
// 将原同步队列的尾部后置节点指向 node
t.next = node;
return t;
}
}
}
}

从代码中可以看出通过 CAS 操作保证节点入队的有序安全,其入队过程中如下图所示:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//
for (;;) {
// 获取当前节点的前置节点
final Node p = node.predecessor();
// 判断前置节点是否为 head 头节点
// 若前置节点为 head 节点,则再次尝试获取同步状态
if (p == head && tryAcquire(arg)) {
// 若获取同步状态成功
// 则将队列的 head 移动指向当前节点
setHead(node);
// 将原头部节点的 next 指向为空,便于对象回收
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.
*/
// 若前置节点状态为 -1 ,则说明后置节点 node 可以安全挂起了
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// ws > 0 说明前置节点状态为 CANCELLED , 也就是说前置节点为无效节点
// 此时从前置节点开始向队列头节点方向寻找有效的前置节点
// 此操作也即是将 CANCELLED 节点从队列中移除
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.
*/
// 若前置节点状态为初始状态 则将其状态设为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 将当前线程挂起
LockSupport.park(this);
// 被唤醒后检查当前线程是否被挂起
return Thread.interrupted();
}

从 acquireQueued 的实现可以看出,节点在入队后会采用轮询的方式(自旋)重复执行以下过程:

  • 判断前置节点是否为 head, 若为 head 节点则尝试获取同步状态; 若获取同步状态成功则移动 head 指向当前节点并退出循环
  • 若前置节点非 head 节点或者获取同步状态失败,则将前置节点状态修改为 -1, 并挂起当前线程,等待被唤醒重复执行以上过程

如下图所示:

接下来我们看看同步状态释放的实现。

release

释放同步状态

public final boolean release(int arg) {
// 尝试释放同步状态
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒后置节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 将 head 节点状态改为 0
compareAndSetWaitStatus(node, ws, 0); /*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 获取后置节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒后置节点上所阻塞的线程
LockSupport.unpark(s.thread);
}

从上述代码,我们可以明白释放同步状态的过程如下:

  • 调用 tryRelease 尝试释放同步状态,同样其具体的实现由子类控制
  • 成功释放同步状态后,将 head 节点状态改为 0
  • 唤醒后置节点上阻塞的线程

如下图所示(红色曲线表示节点自旋过程) :

acquireInterruptibly()

独占模式下获取同步状态,不同于 acquire 方法,该方法对中断操作敏感; 也就是说当前线程在获取同步状态的过程中,若被中断则会抛出中断异常

public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
// 检查线程是否被中断
// 中断则抛出中断异常由调用方处理
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 不同于 acquire 的操作,此处在唤醒后检查是否中断,若被中断直接抛出中断异常
throw new InterruptedException();
}
} finally {
if (failed)
// 抛出中断异常后最终执行 cancelAcquire
cancelAcquire(node);
}
}
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return; node.thread = null; // Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next; // Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves.
// 若当前节点为 tail 节点,则将 tail 移动指向 node 的前置节点
if (node == tail && compareAndSetTail(node, pred)) {
// 同时将node 前置节点的 next 指向 null
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 当前节点位于队列中部
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 将前置节点的 next 指向 node 的后置节点
compareAndSetNext(pred, predNext, next);
} else {
// 若 node 的前置节点为 head 节点则唤醒 node 节点的后置节点
unparkSuccessor(node);
} node.next = node; // help GC
}
}

从 acquireInterruptibly 的实现可以看出,若线程在获取同步状态的过程中出现中断操作,则会将当前线程对应的同步队列等待节点从队列中移除并唤醒可获取同步状态的线程。

tryAcquireNanos()

独占模式超时获取同步状态,该操作与acquireInterruptibly一样对中断操作敏感,不同在于超过等待时间若未获取到同步状态将会返回

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 计算等待到期时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
// 超时时间到期直接返回
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 按指定时间挂起s
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

节点的状态

同步队列中的节点在自旋获取同步状态的过程中,会将前置节点的状态由 0 初始状态改为 -1 SIGNAL, 若是中断敏感的操作则会将状态由 0 改为 1

同步队列中的节点在释放同步状态的过程中会将同步队列的 head 节点的状态改为 0, 也即是由 -1 变为 0;

小结

本文主要分析了独占模式获取同步状态的操作,其大概流程如下:

  • 在获取同步状态时,AQS 内部维护了一个同步队列,获取状态失败的线程会被构造一个节点加入到队列中并进行一系列自旋操作
  • 在释放同步状态时,唤醒 head 的后置节点去获取同步状态

AbstractQueuedSynchronizer 队列同步器源码分析的更多相关文章

  1. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

  2. 并发编程(六)——AbstractQueuedSynchronizer 之 Condition 源码分析

    我们接着上一篇文章继续,本文讲讲解ReentrantLock 公平锁和非公平锁的区别,深入分析 AbstractQueuedSynchronizer 中的 ConditionObject 公平锁和非公 ...

  3. [Java并发] AQS抽象队列同步器源码解析--独占锁释放过程

    [Java并发] AQS抽象队列同步器源码解析--独占锁获取过程 上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文... 锁释放入 ...

  4. 并发编程(五)——AbstractQueuedSynchronizer 之 ReentrantLock源码分析

    本文将从 ReentrantLock 的公平锁源码出发,分析下 AbstractQueuedSynchronizer 这个类是怎么工作的,希望能给大家提供一些简单的帮助. AQS 结构 先来看看 AQ ...

  5. 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

  6. 多线程高并发编程(11) -- 非阻塞队列ConcurrentLinkedQueue源码分析

    一.背景 要实现对队列的安全访问,有两种方式:阻塞算法和非阻塞算法.阻塞算法的实现是使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出队和入队各一把锁LinkedBloc ...

  7. 【JDK】JDK源码分析-AbstractQueuedSynchronizer(2)

    概述 前文「JDK源码分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 节点的「独占模式」和「共享模式」,其实 AQS 也主要是围绕对这两种模 ...

  8. 【JDK】JDK源码分析-AbstractQueuedSynchronizer(3)

    概述 前文「JDK源码分析-AbstractQueuedSynchronizer(2)」分析了 AQS 在独占模式下获取资源的流程,本文分析共享模式下的相关操作. 其实二者的操作大部分是类似的,理解了 ...

  9. ReentrantReadWriteLock源码分析及理解

    本文结构 读写锁简介:介绍读写锁.读写锁的特性以及类定义信息 公平策略及Sync同步器:介绍读写锁提供的公平策略以及同步器源码分析 读锁:介绍读锁的一些常用操作和读锁的加锁.解锁的源码分析 写锁:介绍 ...

随机推荐

  1. iOS 面试集锦2

    4.写一个setter方法用于完成@property (nonatomic,retain)NSString *name,写一个setter方法用于完成@property(nonatomic,copy) ...

  2. MAC的睡眠模式介绍

    因为之前用的是网上流传的土法来禁止生成 sleepimage,尝到了苦头,而且2次! 大家知道 OSX 有几种睡眠模式,其中 hibernatemode 可以是 0 (传统睡眠方式,不生成 sleep ...

  3. 【最短路径树】51nod1443 路径和树

    并不是什么高端操作并且一些模型会用到 Description 给定一幅无向带权连通图G = (V, E) (这里V是点集,E是边集).从点u开始的最短路径树是这样一幅图G1 = (V, E1),其中E ...

  4. 如何用纯 CSS 创作在文本前后穿梭的边框

    效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/qYepNv 可交互视频教 ...

  5. element使用心得

    Table Table 常用属性解释 数据过滤,filter过滤器 <el-table-column width="200" show-overflow-tooltip la ...

  6. linux定时任务执行php任务

    首先用命令检查服务是否在运行 systemctl status crond.service 如果服务器上没有装有crontab ,则可以执行 yum install vixie-cron yum in ...

  7. Python常见文件操作的函数示例

    # -*-coding:utf8 -*- ''''' Python常见文件操作示例 os.path 模块中的路径名访问函数 分隔 basename() 去掉目录路径, 返回文件名 dirname() ...

  8. Knockout v3.4.0 中文版教程-3-监控-通过监控创建视图模型(下)

    6. 显式订阅监控 你通常不需要手动设置订阅,所以初学者应该跳过这一节. 对于高级用户,如果你想注册自己的订阅来监控通知变化,你可以使用 subscribe函数,比如: myViewModel.per ...

  9. python模拟浏览器webdriver登陆网站后抓取页面并输出

    关键在于以下两行代码 特别是find_element_by_xpath写法 很多写成 findElementsByXpath不知道是写错了 还是高级版本是这么写的... #webElement = s ...

  10. Python算法-二叉树深度优先遍历

    二叉树 组成: 1.根节点  BinaryTree:root 2.每一个节点,都有左子节点和右子节点(可以为空)  TreeNode:value.left.right 二叉树的遍历: 遍历二叉树:深度 ...