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. Eclipse修改Maven仓库配置

    修改Apache-Maven\conf下的settings.xml文件 <?xml version="1.0" encoding="UTF-8"?> ...

  2. python基本数据类型之------列表

    一.列表----list 列表是有序的,列表元素可以被修改 =================list之灰魔法================ 1.列表格式: 中括号括起来, 逗号分隔每个元素, 列表 ...

  3. Kernel数据结构移植(list和rbtree)

    主要移植了内核中的 list,rbtree.使得这2个数据结构在用户态程序中也能使用. 同时用 cpputest 对移植后的代码进行了测试.(测试代码其实也是使用这2个数据结构的方法) 内核代码的如下 ...

  4. [福大软工] Z班 团队第一次作业—团队展示成绩公布

    [福大软工] Z班 团队第一次作业-团队展示成绩公布 作业地址 http://www.cnblogs.com/easteast/p/7511264.html 作业要求 队员姓名与学号(标记组长),其中 ...

  5. 16.ajax_case01

    # 抓取北京市2018年积分落户公示名单 # 'http://www.bjrbj.gov.cn/integralpublic/settlePerson' import csv import json ...

  6. leetcode第一刷_Merge Intervals

    看到这个题我就伤心啊,去微软面试的时候,第一个面试官让我做的题目就是实现集合的交操作,这个集合中的元素就像这里的interval一样.是一段一段的.当时写的那叫一个慘不忍睹.最后果然被拒掉了. .好好 ...

  7. golang 开发gui

    可能因为我电脑上的mingw下只有gcc,没有g++的原因,之前用walk和andlabs都不成功 最后用github上gxui的sample代码终于编译出来一个丑陋的GUI,但编译过程也提示了一堆类 ...

  8. Jenkins以root用户运行的方法

    以centOS系统为例,记录下修改Jenkins以root用户运行的方法. 修改Jenkins配置文件 # 打开配置文件 vim /etc/sysconfig/jenkins # 修改$JENKINS ...

  9. Nginx入门讲解——初步认识了解nginx.conf配置文件以及配置多个虚拟主机

    本文引自网络进攻学习之用https://blog.csdn.net/weixin_38111957/article/details/81080539 一. 引言上节文章讲述了如何用信号控制Nginx服 ...

  10. Python:Day25 成员修饰符、特殊成员、反射、单例

    一.成员修饰符 共有成员 私有成员,__字段名,__方法 - 无法直接访问,只能间接访问 class Foo: def __init__(self,name,age): self.name = nam ...