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等多个获取、修改方法,源码如下:

  1. private volatile int state;
  2. protected final int getState() {
  3. return state;
  4. }
  5. protected final void setState(int newState) {
  6. state = newState;
  7. }
  8. protected final boolean compareAndSetState(int expect, int update) {
  9. // See below for intrinsics setup to support this
  10. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  11. }
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15673957.html

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

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

同步队列

Node类

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

  1. static final class Node {
  2. static final Node SHARED = new Node();
  3. static final Node EXCLUSIVE = null;
  4. static final int CANCELLED = 1;
  5. static final int SIGNAL = -1;
  6. static final int CONDITION = -2;
  7. static final int PROPAGATE = -3;
  8. volatile int waitStatus;
  9. volatile Node prev;
  10. volatile Node next;
  11. volatile Thread thread;
  12. Node nextWaiter;
  13. final boolean isShared() {
  14. return nextWaiter == SHARED;
  15. }
  16. final Node predecessor() throws NullPointerException {
  17. Node p = prev;
  18. if (p == null)
  19. throw new NullPointerException();
  20. else
  21. return p;
  22. }
  23. Node() { // Used to establish initial head or SHARED marker
  24. }
  25. Node(Thread thread, Node mode) { // Used by addWaiter
  26. this.nextWaiter = mode;
  27. this.thread = thread;
  28. }
  29. Node(Thread thread, int waitStatus) { // Used by Condition
  30. this.waitStatus = waitStatus;
  31. this.thread = thread;
  32. }
  33. }

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

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

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

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

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

AQS:为什么需要PROPAGATE?

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

同步队列的结构

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

  1. private transient volatile Node head;
  2. private transient volatile Node tail;

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

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

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

入队算法

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

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

  1. // mode可以是Node.EXCLUSIVE或Node.SHARED
  2. private Node addWaiter(Node mode) {
  3. Node node = new Node(Thread.currentThread(), mode);
  4. // Try the fast path of enq; backup to full enq on failure
  5. Node pred = tail;
  6. if (pred != null) {
  7. node.prev = pred;
  8. if (compareAndSetTail(pred, node)) {
  9. pred.next = node;
  10. return node;
  11. }
  12. }
  13. enq(node);
  14. return node;
  15. }

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

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

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

  1. private Node enq(final Node node) {
  2. for (;;) {
  3. Node t = tail;
  4. if (t == null) { // 如果队列为空,则将head和tail初始化为同一个空Node
  5. if (compareAndSetHead(new Node()))
  6. tail = head;
  7. } else {
  8. node.prev = t;
  9. if (compareAndSetTail(t, node)) { // 不断CAS直到成功为止
  10. t.next = node;
  11. return t;
  12. }
  13. }
  14. }
  15. }

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

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

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

出队算法

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

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

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

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. if (p == head && tryAcquire(arg)) {
  8. setHead(node);
  9. p.next = null; // help GC
  10. failed = false;
  11. return interrupted;
  12. }
  13. if (shouldParkAfterFailedAcquire(p, node) &&
  14. parkAndCheckInterrupt())
  15. interrupted = true;
  16. }
  17. } finally {
  18. if (failed)
  19. cancelAcquire(node);
  20. }
  21. }
  22. private void setHead(Node node) {
  23. head = node;
  24. node.thread = null;
  25. node.prev = null;
  26. }

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

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

节点的取消

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

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

  1. private void cancelAcquire(Node node) {
  2. if (node == null)
  3. return;
  4. node.thread = null; // 将thread域置空以方便GC
  5. // 向前遍历并跳过被取消的Node
  6. Node pred = node.prev;
  7. while (pred.waitStatus > 0)
  8. node.prev = pred = pred.prev;
  9. Node predNext = pred.next;
  10. node.waitStatus = Node.CANCELLED;
  11. // 如果是tail,那么将tail修改为pred
  12. if (node == tail && compareAndSetTail(node, pred)) {
  13. compareAndSetNext(pred, predNext, null);
  14. } else {
  15. int ws;
  16. if (pred != head &&
  17. ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
  18. pred.thread != null) {
  19. // 如果node的next需要signal,那么就将pred的next设为node的next
  20. Node next = node.next;
  21. if (next != null && next.waitStatus <= 0)
  22. compareAndSetNext(pred, predNext, next);
  23. } else {
  24. unparkSuccessor(node);
  25. }
  26. node.next = node; // help GC
  27. }
  28. }

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

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

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

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

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

  1. Node pred = node.prev;
  2. while (pred.waitStatus > 0)
  3. 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. Part 29 AngularJS intellisense in visual studio

    In the previous videos if you have noticed as we were typing the angular code in Script.js file we w ...

  2. Part 27 Remove # from URL AngularJS

    There are 4 simple steps to remove # from URLs in Angular. Step 1 : Enable html5mode routing. To do ...

  3. Java学习(十八)

    学习了Web中的单位. 像素是网页中最常用到的单位,一个像素是屏幕中的一个小点. 不同显示器一个像素的大小也不同,像素越小,显示效果越好. 也可以用百分比的方式: <!DOCTYPE html& ...

  4. 菜鸡的Java笔记 第五 - java 程序逻辑控制

    程序主要分为三种逻辑:顺序,分支,循环. if 分支语句 if分支语句是最为基础的分支操作,但是其有三种使用形式: if语句 if.....else   语句 if....else...if...el ...

  5. [bzoj1077]天平

    先考虑如何求出任意两数的最大差值和最小差值,直接差分约束建图跑floyd求最短路和最长路即可然后枚举i和j,考虑dA+dB和di+dj的关系,分两种情况移项,转化成dA-di和dj-dB的关系或dA- ...

  6. [bzoj3171]循环格

    如果把这个矩阵看成一张图,题目相当于要求每一个点的入度和出度都是1(也就是有很多环),否则指向环的点就无法走回自己了将所有点拆成两个,S向原来的点流(1,0)的边,拆出来的点向T连(1,0)的边,然后 ...

  7. 前端---梳理 http 知识体系 2

    为什么要有HTTPS HTTP 天生具有明文的特点,整个传输过程完全透明,任何人都能够在链路中截获.修改或者伪造请求 / 响应报文,数据不具有安全性.仅凭HTTP 自身是无法解决的,需要引入新的HTT ...

  8. 从零开始,使用Dapr简化微服务

    序言 现有的微服务模式需要再业务代码中集成大量基础设施模块,比如注册中心,服务发现,服务调用链路追踪,请求熔断,重试限流等等,使得系统过于臃肿重量级. Dapr作为新一代微服务模式,使用sidecar ...

  9. Timer定时器的使用

    import java.util.Timer; import java.util.TimerTask; public class Demo2 { //执行时间,时间单位为毫秒,读者可自行设定,不得小于 ...

  10. SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...