AbstractQueuedSynchronizer,简称为AQS,它是构建JDK中多个并发工具的基础。下图展示了JDK中使用AQS构建的并发工具。

可见,AQS在Java并发编程中是多么的重要。所以,我们有必要搞清楚其实现的原理。

一、AQS中的数据结构

在AQS类文件的注释中,作者已经给出了内部数据结构的说明。AQS里使用的是同步队列是CLH(Craig, Landin, and Hagersten)锁队列的一个变形,其中CLH锁通常用于自旋锁。CLH队列从节点的结构与节点等待机制两方面进行了改造:

①在结构上:引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;

②在等待机制上:由原来的自旋改成阻塞唤醒

AQS的核心原理:

当多个线程在竞争获取同步状态时,如果当前线程获取同步状态成功,则AQS会将当前线程标识为锁的持有者。如果当前线程如果获取同步状态失败时,AQS则会将当前线程以及等待状态等信息包装成一个节点,并将其添加到同步队列尾部等待锁的释放,同时阻塞当前线程。而当同步状态释放时,会唤醒后继结点,使其再次尝试获取同步状态。

下面来看一下结点的声明

    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;
//等待状态:取值范围为上面的几个值,初始值为0
volatile int waitStatus; //前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//持有的线程(线程会被包装成节点)
volatile Thread thread; //下一个在Condition条件上等待的节点。
//因为condition队列仅存在于排它模式下,所以当线程在condition上等待时,我们只需要一个简单的链表队列持有节点即可。之后他们可以被转移到竞争锁的同步队列中重新获取锁。
Node nextWaiter;
}

Node结点是对每一个访问同步状态的线程的封装,其包含了需要同步的线程本身以及线程的状态,例如是否被阻塞,是否等待唤醒,是否已经被取消等。waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

  • SIGNAL:值为-1。被标识为等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
  • 初始状态:值为0,初始化状态。
  • CANCELLED:值为1。在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  • CONDITION:值为-2。与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3。与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
总结一下:

同步队列中waitStatus为SIGNAL(-1)的节点会被选为下一个获取锁的后继节点。

等待队列中waitStatus为CONDITION(-2)的节点会被在其它线程调用signal()后从等待队列转移到同步队列

等待队列中watiStatus为CANCELLED(1)的节点由于超时等待或被中断,而放弃获取锁。

同步队列中的节点如果想最终获得锁,则必须经过两个过程:被标记为CONDITION(-2)----->被标记为SIGNAL(-1)

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable { /**
* 创建AQS实例,初始同步状态为0
*/
protected AbstractQueuedSynchronizer() { } static final class Node {……} //头指针
private transient volatile Node head;
//尾指针
private transient volatile Node tail;
//同步状态
private volatile int state; }

二、获取/释放锁流程

1.排它模式获取锁acquire

    public final void acquire(int arg) {
//获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
//1.创建节点,并指定排它/模式模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。
Node pred = tail;
if (pred != null) {
//与旧尾节点连接(新节点prev指向旧尾节点)
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//与旧尾节点连接(旧尾节点next指向新节点)
pred.next = node;
return node;
}
}
//3.通过自旋+CAS方式,将节点加入到队列尾部
enq(node);
return node;
} final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;// 标识是否成功获取同步状态
try {
boolean interrupted = false;//标识等待过程中是否被中断过 //自旋操作:不停的判断自己是否排在head后面
for (;;) {
final Node p = node.predecessor();//前驱
//如果前驱是head,说明自己正排在head后面,便有资格去尝试获取资源(head释放同步状态后唤醒自己,当然也可能被interrupt)。
if (p == head && tryAcquire(arg)) {
setHead(node);//获取到同步状态后,就自己设置为head结点
p.next = null; // 便于GC回收以前的head结点
failed = false;
return interrupted;
} //执行到这里,说明:没有排在head后面,或者排在head后面但未获取到同步状态 //判断自己是否应该休息,如果是则通过park()进入waiting状态,直到被unpark。
//如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到同步状态,从而继续进入park。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//如果等待过程中被中断过,将interrupted标记为true
}
} finally {
if (failed) // 如果等待过程中没有成功获取资源(如超时,或可中断的情况下被中断了),那么取消结点在队列中的等待。
cancelAcquire(node);
}
} private static void selfInterrupt() {
Thread.currentThread().interrupt();
}

如果获取同步状态成功,则直接返回。

如果获取同步状态失败。则加入到队列尾部。

  • 首先尝试通过快速模式添加。
  • 如果不成功,则通过CAS+自旋的方式添加。

成功加入到队列尾部后,结点即将进入等待状态。但不会立即进入等待状态,因为很可能此时head结点即将释放同步状态,自己立马就有资格获取到同步状态了。所以会先经历一段自旋时间,自旋时会不停的判断自己是否为正排在head结点后。

  • 如果是排在head后则尝试获取同步状态,如果head恰好释放了同步状态,则会顺利拿到同步状态,之后将自身设为head结点。(运气好)
  • 当然更常见的情况可能是发现自己没排在head后面,或者即使排在head后却一直拿不到同步状态,说明head可能一时半会不会立马释放同步状态,那要不干脆就休息一会吧!

通过判断,如果确实需要休息,则通过park进入waiting状态,直到被unpark。

2.排它模式释放锁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)
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.
*
* 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。
* 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。
*/ //后继节点
Node s = node.next;
//后继为空或为被取消状态(CANCELED)
if (s == null || s.waitStatus > 0) {
s = null;//若是被取消状态,则设置为null有助于GC
//从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】
for (Node t = tail; t != null && t != node; t = t.prev)
//只要等待状态<=0(-1,-2,-3,0),就可以唤醒
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
} public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}

3.获取锁失败加入同步队列addWaiter

将竞争锁失败的线程加入到同步队列

    public final void acquire(int arg) {
//获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} /**
* Creates and enqueues node for current thread and given mode.
* 将当前线程包装成节点加入等待队列,并指定模式(排它模式/共享模式)
*/
private Node addWaiter(Node mode) {
//1.创建节点,并指定排它/模式模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。
Node pred = tail;
if (pred != null) {
//与旧尾节点连接(新节点prev指向旧尾节点)
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//与旧尾节点连接(旧尾节点next指向新节点)
pred.next = node;
return node;
}
}
//3.通过自旋+CAS方式,将节点加入到队列尾部
enq(node);
return node;
} /**
* Inserts node into queue, initializing if necessary. See picture above.
* 将节点入队列,必要时会进行初始化。
*/
private Node enq(final Node node) {
//自旋+CAS方式将节点加到队列尾部
for (; ; ) {
Node t = tail;
//队列为空,则先初始化创建一个空节点,并将tail指针指向它
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {//队列非空,则插入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

将竞争锁失败的线程入队列,大致分为以下3步:
①将该线程封装成节点,并指定当前所处于的模式(排它模式/共享模式)
②首先使用快捷方式尝试加入到队列:直接插入队列尾部。
③如果上述方式失败则使用自旋+CAS的方式插入队列尾部。

4.释放锁时唤醒后继unparkSuccessor

唤醒后继

head节点是获取同步状态成功的节点。首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为head节点。

    /**
* 唤醒后继节点
*/
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)
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.
*
* 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。
* 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。
*/ //后继节点
Node s = node.next;
//后继为空或为被取消状态(CANCELED)
if (s == null || s.waitStatus > 0) {
s = null;//若是被取消状态,则设置为null有助于GC
//从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】
for (Node t = tail; t != null && t != node; t = t.prev)
//只要等待状态<=0(-1,-2,-3,0),就可以唤醒
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}

三、等待通知流程

5.等待await

    public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程包装成节点,加入到Condition等待队列(非同步队列)尾部
Node node = addConditionWaiter();
//释放当前线程的独占锁(不管重入几次,都把state释放为0)
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果当前节点没有在同步队列上,即还没有被signal,则将当前线程阻塞
while (!isOnSyncQueue(node)) {
//阻塞当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//加入到同步队列成功,且从等待模式退出未抛出中断异常(非等待超时或被中断),则将中断状态标记为重新中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//从等待队列中断开该节点的连接
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

调用await方法后,当前获取了锁的线程会被包装成节点加入到等待队列尾部,同时会释放锁,通知后继线程来获取锁。

6.通知signal

    public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
} private void doSignal(Node first) {
do {
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
} //将被标记为CONDITION的节点转移到同步队列
final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}

线程调用signal后,会发通知将等待队列中的节点转移到同步队列中来获取锁。

总结:

1.AQS的实现原理?

2.CLH同步队列是怎么实现非公平和公平的?

参考:

Java并发之AQS详解

AbstractQueuedSynchronizer同步队列与Condition等待队列协同机制

JUC回顾之-AQS同步器的实现原理

AQS解析(未完成)的更多相关文章

  1. AQS解析

    什么是AQS? AQS是JUC内存的基石,它本质上是一个抽象类,定义了多线程下资源争夺与释放的规则和过程,许多实现类都是继承于AQS,使用AQS的骨架. AQS的原理 AQS总体上来看是由一个FIFO ...

  2. mybatis 3.x源码深度解析与最佳实践(最完整原创)

    mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...

  3. myBatis源码解析-二级缓存的实现方式

    1. 前言 前面近一个月去写自己的mybatis框架了,对mybatis源码分析止步不前,此文继续前面的文章.开始分析mybatis一,二级缓存的实现.附上自己的项目github地址:https:// ...

  4. Js框架设计之DomReady

    一.在介绍DomReady之前,先了解下相关的知识 1.HTML是一种标记语言,告诉我们这页面里面有什么内容,但是行为交互则要通过DOM操作来实现,但是注意:不要把尖括号里面的内容看作是DOM! 2. ...

  5. Spring源码系列(二)--bean组件的源码分析

    简介 spring-bean 组件是 Spring IoC 的核心,我们可以使用它的 beanFactory 来获取所需的对象,对象的实例化.属性装配和初始化等都可以交给 spring 来管理. 本文 ...

  6. domReady的理解

    domReady的理解 domReady是名为DOMContentLoaded事件的别称,当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表.图 ...

  7. Mall电商实战项目发布重大更新,全面支持SpringBoot 2.3.0

    1. 前言 前面近一个月去写自己的mybatis框架了,对mybatis源码分析止步不前,此文继续前面的文章.开始分析mybatis一,二级缓存的实现. 附上自己的项目github地址:https:/ ...

  8. AbstractQueuedSynchronizer(AQS)源码解析

          关于AQS的源码解析,本来是没有打算特意写一篇文章来介绍的.不过在写本学期课程作业中,有一门写了关于AQS的,而且也画了一些相关的图,所以直接拿过来分享一下,如有错误欢迎指正.       ...

  9. 高并发编程-AQS深入解析

    要点解说 AbstractQueuedSynchronizer简称AQS,它是java.util.concurrent包下CountDownLatch/FutureTask/ReentrantLock ...

随机推荐

  1. 什么是JDK?什么是JRE?JDK与JRE的区别和用途

    一.编程环境与运行环境 JDK(Java Development Kit)称为Java开发包或Java开发工具.是一个编写Java的Applet小程序和应用程序的程序开发环境.JDK是整个Java的核 ...

  2. Linux-Centos7系统下安装python2并与python3版本共存

    问题描述: 最近有个需求是想在centos下安装python3.5 因为django这边用到是这个版本 1.查看系统版本和python版本 Centos7.6版本默认安装的是python2.7.5版本 ...

  3. June 11. 2018 Week 24th, Monday

    Love is the beauty of the soul. 爱是灵魂之美. From Saint Augustine. The complete version of this quote goe ...

  4. [福大软工] Z班 团队Beta阶段成绩汇总

    Beta敏捷冲刺得分 队伍名 1 2 3 4 5 总分 Dipper 10 10 10 10 10 50 SWSD 9 9 9 9 7 43 五成胜算 10 10 10 10 10 50 人月神教 0 ...

  5. 《Java大学教程》—第7章 类的实现

    统一建模语言(UML)用方框代表类.方框被分成3部分,第一部分是类名,第二部分是类的属性,第三部分是类的方法.类的属性(属性名称:属性类型):类的方法(方法名称(参数类型):返回值类型).静态的类的属 ...

  6. python 进程介绍 进程简单使用 join 验证空间隔离

    一.多道程序设计技术(详情参考:https://www.cnblogs.com/clschao/articles/9613464.html) 所谓多道程序设计技术,就是指允许多个程序同时进入内存并运行 ...

  7. PostgreSQL 数据库NULL值的默认排序行为与查询、索引定义规范 - nulls first\last, asc\desc

    背景 在数据库中NULL值是指UNKNOWN的值,不存储任何值,在排序时,它排在有值的行前面还是后面通过语法来指定. 例如 -- 表示null排在有值行的前面 select * from tbl or ...

  8. 从 0 → 1,学习Linux该这么开始!

    首先我们还是来普及以下概念,讲点虚的.现在是图形系统的天下,windows我们用了20多年.成功归功与它图形界面,你会点鼠标吗你会敲键盘吗?所以你会上网会聊天会玩游戏了.那么,0基础接触的Linux, ...

  9. Hibernate基本映射类型

  10. 关于connect by rownum与connect by leve

    http://www.itpub.net/forum.php?mod=viewthread&tid=1570306 http://www.itpub.net/forum.php?mod=vie ...