AbstractQueuedSynchronizer(以下简称AQS)的内容确实有点多,博主考虑再三,还是决定把它拆成三期。原因有三,一是放入同一篇博客势必影响阅读体验,而是为了表达对这个伟大基础并发组件的崇敬之情。第三点其实是为了偷懒。

又扯这么多没用的,还是直接步入正题吧~

AQS介绍

AQS是一个抽象类,它是实现多种并发同步工具的核心组件。比如大名鼎鼎的可重入锁(ReentrantLock),它的底层实现就是借助内部类Sync,而Sync类就是继承了AQS并实现了AQS定义的若干钩子方法。这些并发同步工具包括:

从设计模式上来看,AQS主要使用的是模板方法模式(Template Method Pattern)。它提供了若干钩子方法供子类实现(如tryAcquire、tryRelease等),AQS的模板方法(如acquire、release等)会调用这些钩子方法。子类使用AQS的方式就是直接调用AQS的模板方法,并重写这些模板方法涉及到的特定钩子方法即可。不需要调用的钩子方法可以不用重写,AQS为它们均提供了默认实现:抛出UnsupportedOperationException异常

此外,AQS也提供了其他一些方法供子类调用,如getState、hasQueuedPredecessors等方法,方便子类获取、判断同步器的状态

什么是钩子方法?

钩子方法的概念源于模板方法模式,这种模式是在一个方法中定义了算法的骨架,某些关键步骤会交给子类去实现。模板方法在不改变算法本身结构的情况下,允许子类自定义其中一些关键步骤

这些关键步骤可以由父类定义成方法,这些方法可以是抽象方法,或钩子方法

  • 抽象方法:父类定义但不实现,由abstract关键字标识
  • 钩子方法:父类定义且实现,但这种实现一般都是空实现,并没有任何意义,这么做只是为了方便子类根据需要重写特定的钩子方法,而不用实现所有的钩子方法

AQS的核心思想:

  • 使用一个volatile int变量state(也被称为资源),进行同步控制,但是state在不同的同步工具实现中具有不同的语义。另外配合Unsafe类提供的CAS操作,原子性地修改state值,保证其线程安全性
  • AQS内部维护了一个同步队列,用来管理排队的线程。另外需要借助LockSupport类提供的线程阻塞、唤醒方法
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15673957.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

AQS的基本结构

状态state

AQS使用volatile int变量state来作为核心状态,所有的同步控制都是围绕这个state来进行的,volatile保证其内存可见性,并使用CAS确保state的修改是原子性的。volatile和CAS同时存在,就保证了state的线程安全性

对于不同的同步工具实现来说,语义是不同的,如下:

  • ReentratntLock:表示当前线程获取锁的重入次数,0表示锁空闲
  • ReentrantReadWriteLock:state的高16位表示读锁数量,低16位表示写锁数量
  • CountDownLatch:表示当前的计数值
  • Semaphore:表示当前可用信号量的个数

针对state这个核心状态,AQS提供了getState、setState等多个获取、修改方法,源码如下:

private volatile int state;

protected final int getState() {
return state;
} protected final void setState(int newState) {
state = newState;
} protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15673957.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

同步队列

Node类

AQS内部维护了一个同步队列(网上有些文章会叫它为CLH队列,至于为啥叫这个我也不知道-_-||,但不重要~)。队列中的每个节点都是Node类型。其源码如下:

static final class Node {

    static final Node SHARED = new Node();
static final Node EXCLUSIVE = null; static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev;
volatile Node next; volatile Thread thread; Node nextWaiter; final boolean isShared() {
return nextWaiter == SHARED;
} final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
} Node() { // Used to establish initial head or SHARED marker
} Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
} Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}

prev、next用于保存该节点的前驱、后继节点,表明这个同步队列是一个双向队列

Node的thread域保存了对应的线程,只有在创建时赋值,使用完要null掉,以方便GC

Node使用SHAREDEXCLUSIVE两个常量来标记该线程是由于获取共享资源、互斥资源失败,而被阻塞并放入到同步队列中进行等待

Node使用waitStatus来记录当前线程的等待状态,通过CAS进行修改。它的取值可以是:

  • CANCELLED:表示该节点由于超时中断而被取消。该状态不会再转变为其他状态,而且该节点的线程再也不会被阻塞
  • SIGNAL:表示其后继节点(后面相邻的那个节点)需要被唤醒,即该线程被释放或被取消时,必须唤醒其后继节点
  • CONDITION:表示该节点的线程在条件队列中等待,而非在同步队列中。如果该条件变量signal该节点后,该节点会被转移到同步队列中参与资源竞争
  • PROPAGATE:只有在共享模式下才会被用到,表示无条件传播状态。引入这个状态是为了解决共享模式下并发释放而引起的线程挂起的bug,这里不多解释,网上有文章给出了更加详细的解释,见下方

AQS:为什么需要PROPAGATE?

AQS源码深入分析之共享模式-你知道为什么AQS中要有PROPAGATE这个状态吗?

同步队列的结构

AQS中维护了一个同步队列,它通过两个指针标记队头队尾,分别是headtail,源码如下:

private transient volatile Node head;

private transient volatile Node tail;

该队列的出入规则遵循FIFO(First In, First Out)

注意:如果该同步队列非空,那么head其实并不是指向第一个线程对应的Node,而是指向一个空的Node

接下来让我们剖析一下AQS针对这个同步队列设计的入队、出队算法

入队算法

入队事件主要在线程尝试获取资源失败时触发。当线程尝试获取资源失败之后,会将该线程加入到同步队列的队尾

入队算法的源码见AQS的addWaiter方法,如下:

// mode可以是Node.EXCLUSIVE或Node.SHARED
private Node addWaiter(Node mode) {
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.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

首先为该线程创建一个Node节点,mode可以是Node.EXCLUSIVE或Node.SHARED,表示两种不同的模式。

之后直接CAS试图将其入队。这里注意,如果队列本身为空,或CAS竞争失败,才会进入enq方法。这里addWaiter方法出于性能考虑,先尝试快捷的入队方式,不成功才执行eng方法

eng方法是完整的入队逻辑,源码如下:

private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果队列为空,则将head和tail初始化为同一个空Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // 不断CAS直到成功为止
t.next = node;
return t;
}
}
}
}

enq中的代码都包含在for循环中,如果CAS失败,就会不断循环CAS直到成功为止

注意,这段代码也体现出同步队列的三个特点

  • 入队都是从队尾
  • 进入队列的操作都是CAS操作,保证了线程安全性
  • 如果队列为空,则head和tail都为null;如果不为空,head指向的节点并不是第一个线程对应的节点,而是一个哑节点

出队算法

出队事件主要发生在:位于同步队列中的线程再次获取资源,并成功获取时

出队算法在AQS中并没有直接对应的方法,而是零散分布在某些方法中。因为获取资源失败而被阻塞的线程被唤醒后,会重新尝试获取资源。如果获取成功,则会执行出队逻辑

例如,在acquireQueued中,就包含了出队事件:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
} private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}

出队的逻辑体现在第6-9行,此时p指向head指向的空节点,而node是队首元素(不是第一个空节点)

首先调用setHead方法,将head指向node、将node的thread域、prev域置空,然后将head的next域置空,以方便该节点的GC

节点的取消

线程会因为超时或中断而被取消,之后不会再参与锁的竞争,会等待GC

取消的过程见cancelAcquire方法,该方法的调用时机都是在获取资源失败之后,而失败就是由于超时或中断。其源码如下:

private void cancelAcquire(Node node) {
if (node == null)
return; node.thread = null; // 将thread域置空以方便GC // 向前遍历并跳过被取消的Node
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev; Node predNext = pred.next;
node.waitStatus = Node.CANCELLED; // 如果是tail,那么将tail修改为pred
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需要signal,那么就将pred的next设为node的next
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}

总之,cancelAcquire方法就是将目标节点node的thread域置空,并将waitStatus置为CANCELLED

这里有一个问题:node的后继节点next的prev指针仍然指向node,没有更新为pred,这不仅语义上是错误的,而且会阻碍node被GC。那么何时进行更新?

答:任何其他线程尝试获取锁失败之后,都会被放入同步队列,然后调用shouldParkAfterFailedAcquire方法判断是否应该被阻塞。如果发现当前节点的前驱节点被置为CANCELLED,就会执行:

do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

此外,cancelAcquire方法也会做类似的操作,如下:

Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;

这两处都会更新被取消节点的后继节点的prev指针,所以前面说到的的问题根本不存在

注意:cancelAcquire的调用时机一般都是在获取锁逻辑后面的finally块中,如果获取失败就会调用cancelAcquire方法。获取失败的原因主要有两个,中断或超时

总结:

  • 节点被取消的原因:获取锁超时或在获取的过程中被中断
  • 取消节点的主要逻辑:将其waitStatus修改为CANCELLED。再将节点thread域置空,将指向它的next指针指向其后继节点,以方便GC

好了,到这里为止,我们就完成了对AQS基本结构的分析。这里如果有不懂的地方,可以暂时跳过,等看完后续博客再回头看这篇,应该就能明白了

下一篇我们会逐步剖析AQS如何实现对资源的获取和释放,go go go!

全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础的更多相关文章

  1. 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放

    上期的<全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础>中介绍了什么是AQS,以及AQS的基本结构.有了这些概念做铺垫之后,我们就可以正 ...

  2. JDK源码之AQS源码剖析

    除特别注明外,本站所有文章均为原创,转载请注明地址 AbstractQueuedSynchronizer(AQS)是JDK中实现并发编程的核心,平时我们工作中经常用到的ReentrantLock,Co ...

  3. 并发编程之 AQS 源码剖析

    前言 JDK 1.5 的 java.util.concurrent.locks 包中都是锁,其中有一个抽象类 AbstractQueuedSynchronizer (抽象队列同步器),也就是 AQS, ...

  4. 硬核剖析Java锁底层AQS源码,深入理解底层架构设计

    我们常见的并发锁ReentrantLock.CountDownLatch.Semaphore.CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁. 上篇 ...

  5. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

  6. AbstractQueuedSynchronizer AQS框架源码剖析

    一.引子 Java.util.concurrent包都是Doug Lea写的,来混个眼熟 是的,就是他,提出了JSR166(Java Specification RequestsJava 规范提案), ...

  7. AQS源码详细解读

    AQS源码详细解读 目录 AQS源码详细解读 基础 CAS相关知识 通过标识位进行线程挂起的并发编程范式 MPSC队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...

  8. ReentrantLock 与 AQS 源码分析

    ReentrantLock 与 AQS 源码分析 1. 基本结构    重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...

  9. 深度分析ReentrantLock源码及AQS源码,从入门到入坟,建议先收藏!

    一.ReentrantLock与AQS简介 在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile.Java5.0增加了一种新的机制:Reentra ...

随机推荐

  1. K8S核心概念之SVC(易混淆难理解知识点总结)

    本文将结合实际工作当中遇到的一些问题和情况来解析SVC的作用以及一些比较易混淆和难理解的概念,方便日后工作用到或者遗忘时可以直接在自己曾经学习总结的博客当中直接查找到. 首先应该清楚SVC的作用是什么 ...

  2. python实现拉普拉斯图像金字塔

    一,定义 二,代码: 要求:拉普拉斯金字塔时,图像大小必须是2的n次方*2的n次方,不然会报错 1 # -*- coding=GBK -*- 2 import cv2 as cv 3 4 5 #高斯金 ...

  3. [loj6278]数列分块入门2

    做法1 以$K$为块大小分块,并对每一个块再维护一个排序后的结果,预处理复杂度为$o(n\log K )$ 区间修改时将整块打上标记,散块暴力修改并归并排序,单次复杂度为$o(\frac{n}{K}+ ...

  4. [hdu7034]Array

    令$f(a)_{i}=\min_{i<j\le n,a_{i}=a_{j}}j$​​(特别的,若不存在$j$​​则令$f(a)_{i}=n+1$​​),则有以下性质: 1.对于$b_{i}$​​ ...

  5. [atARC071F]Infinite Sequence

    注意到当$a_{i}\ne 1$且$a_{i+1}\ne 1$,那么$\forall i<j,a_{j}=a_{i+1}$(证明的话简单归纳就可以了) 令$f_{i}$表示在题中条件下,还满足$ ...

  6. 【Tool】IDEA功能--SVN和Git

    IDEA功能--SVN和Git 2019-11-08  21:12:22  by冲冲 1.IDEA的SVN (1)提交项目代码到SVN服务器 ① 指定不用上传的目录 ② 设置项目上传的路径 SVN服务 ...

  7. Redis分布式缓存剖析及大厂面试精髓v6.2.6

    概述 官方说明 Redis官网 https://redis.io/ 最新版本6.2.6 Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对 ...

  8. Codeforces 1491H - Yuezheng Ling and Dynamic Tree(分块)

    Codeforces 题目传送门 & 洛谷题目传送门 *3400 的毒瘤 H 题,特意写个题解纪念一下( 首先对于这种数据结构不太好直接维护的东东可以考虑分块.然鹅我除了分块其他啥也没想到 我 ...

  9. 【GS应用】基因组选择在杂交玉米上的应用示例

    目录 GS两步走 示例 缩短周期和成本 分类 杂交类型 试验研究 选择响应 选择的强度 选择的周期 预测能力 数据分析的注意事项 GS实施 优缺点 GS的成功 展望 GS两步走 示例 缩短周期和成本 ...

  10. mysql proxy 数据库读写分离字符集乱码

    mysql proxy 数据库读写分离字符集乱码 解决办法 在对应配置后端数据库服务器的配置.cnf中加入如下代码 init-connect='SET NAME UTF8' skip-characte ...