深入理解AbstractQueuedSynchronizer(AQS)
一、AQS简介
在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理
。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现
,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:
- AQS里有一个专门描述同步状态的变量:
private volatile int state;
state是一个状态值,不同实现AbstractQueuedSynchronizer的锁所代表的意思都不同,如果我们使用过CountDownLatch,那么它的方法countDown() 就是执行的就是-1操作
,操作的其实就是这个state值,就相当于一个计数器,对于Semaphore,CyclicBarrier,他们也是通过操作state来进行计数
,ReentrantLock
中,也是通过操作state来实现线程是否获取到锁,当state为0时,代表当前锁时空闲的,没有被线程持有,如果state为1,则当前锁被线程持有,如果大于1,则证明线程重入了,state+1,而对state的操作又分为独占和共享。
对于state的操作就是整个AQS的核心
,操作包括:
- 获取、更新:
getState()、setState()、compareAndSet()
- 暴露同步状态的值:
acquire、acquireInterruptibly、tryAcquireNanos、release、acquireShared、acquireSharedInterruptibly、tryAcquireSharedNanos、releaseShared
细分如下:
- 独占式锁:独占的意思就是
同一时间只有一个线程能操作,其他线程过来都会被阻塞,只有当前线程完成任务后释放了资源,其他线程才能继续获取资源
,每一个线程都与资源进行绑定, 上面我们说的ReentrantLock就是独占锁。同一时间只有一个线程能拿到锁执行,锁的状态只有0和1两种情况。
//独占式获取同步状态,如果获取失败则插入同步队列进行等待;
void acquire(int arg)
//与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
void acquireInterruptibly(int arg)
//在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;
boolean tryAcquireNanos(int arg, long nanosTimeout)
//释放同步状态,该方法会唤醒在同步队列中的下一个节点
boolean release(int arg)
- 共享式锁:同一时间有多个线程可以拿到锁协同工作,
锁的状态大于或等于0。
//共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;
void acquireShared(int arg)
//在acquireShared方法基础上增加了能响应中断的功能;
void acquireSharedInterruptibly(int arg)
//在acquireSharedInterruptibly基础上增加了超时等待的功能;
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
共享式释放同步状态
boolean releaseShared(int arg)
- 有一个帮助AQS将线程在阻塞状态和唤醒状态中转换的
工具类LockSupport
- 队列
- FIFO队列(CLH):
用于竞争锁失败时的排队
整个框架的关键就是如何管理被阻塞的线程的队列,该队列是严格的FIFO队列,因此,框架不支持基于优先级的同步。同步队列的最佳选择是自身没有使用底层锁来构造的非阻塞数据结构,一直以来,CLH锁仅被用于自旋锁。因为CLH锁可以更容易地去实现“取消(cancellation)”和“超时”功能,因此选择了CLH锁作为实现的基础。CLH队列实际上并不那么像队列,因为它的入队和出队操作都与它的用途(即用作锁)紧密相关
。它是一个链表队列,通过两个字段head和tail来存取,这两个字段是可原子更新的,两者在初始化时都指向了一个空节点。
- 条件队列:
给维护独占同步的类以及实现Lock接口的类使用
AQS框架提供了一个ConditionObject类,给维护独占同步的类以及实现Lock接口的类使用。一个锁对象可以关联任意数目的条件对象,可以提供典型的管程风格的await、signal和signalAll操作,包括带有超时的,以及一些检测、监控的方法。
通过修正一些设计决策,ConditionObject类有效地将条件(conditions)与其它同步操作结合到了一起
。该类只支持Java风格的管程访问规则,这些规则中,仅当当前线程持有锁且要操作的条件(condition)属于该锁时,条件操作才是合法的。这样,一个ConditionObject关联到一个ReentrantLock上就表现的跟内置的管程(通过Object.wait等)一样了
。两者的不同仅仅在于方法的名称、额外的功能以及用户可以为每个锁声明多个条件。ConditionObject使用了与同步器一样的内部队列节点。但是,是在一个单独的条件队列中维护这些节点的。signal操作是通过将节点从条件队列转移到锁队列中来实现的,而没有必要在需要唤醒的线程重新获取到锁之前将其唤醒。
- 这两个队列的最基本结构就是
Node
了
abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {private static final long serialVersionUID = 7373984972572414691L;protected AbstractQueuedSynchronizer() {}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;//等待队列中的下一个节点}//队列的头指针private transient volatile Node head;//队列的尾指针private transient volatile Node tail;private volatile int state;
}
二、同步队列(CLH)
- CLH锁是一个自旋锁。
能确保无饥饿性。提供先来先服务的公平性
。
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。
就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。接下来,很显然我们至少会抱有这样的疑问:
- 节点的数据结构是什么样的?
- 是单向还是双向?
- 是带头结点的还是不带头节点的?
我们知道在AQS有一个静态内部类Node,而且我们知道了节点的数据结构类型,每个节点拥有其前驱和后继节点
,很显然这是一个双向队列
。同样的我们可以用一段demo看一下。
public class LockDemo {private static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {for (int i = 0; i < 5; i++) {Thread thread = new Thread(() -> {lock.lock();try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});thread.start();}}
}
实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示:
Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3,Thread-4)获取锁失败进入同步队列,同时也可以很清楚的看出来每个节点有两个域:prev(前驱)和next(后继),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态等信息。另外AQS中有两个重要的成员变量:
private transient volatile Node head;
private transient volatile Node tail;
也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法
。其示意图如下:
通过对源码的理解以及做实验的方式,现在我们可以清楚的知道2点:
1.节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息;
2.同步队列是一个双向队列
,AQS通过持有头尾指针管理同步队列
;
那么,节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作
,获取锁成功进行出队操作。
三、ConditionObject条件变量及条件队列
ConditionObject是AbstractQueuedSynchronizer的内部类,他是实现线程间的同步的基础设施,它是与锁结合使用的(如ReentrantLock),ConditionObject实现了Condition接口,Condition接口提供了await(),signal()等方法,实现线程的挂起和唤醒
,ConditionObject是一个条件变量,每个条件变量对应一个条件队列,当调用Condition的await()被挂起的线程将会存放在条件队列中,调用signal()时将从条件队列中移除并放入AQS队列中。
同步队列和条件队列的关系:
四、独占锁
独占锁的获取(acquire方法)
我们继续通过看源码和debug的方式来看,还是以上面的demo为例,
调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。
而lock()方法实际上会调用AQS的acquire()
方法,源码如下
public final void acquire(int arg) {//先看同步状态是否获取成功,如果成功则方法结束返回//若失败则先调用addWaiter()方法再调用acquireQueued()方法if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
关键信息请看注释,acquire根据当前获得同步状态成功与否做了两件事情:
- 成功,则方法结束返回。
- 失败,则先调用addWaiter()然后在调用acquireQueued()方法。
获取同步状态失败,进行入队操作
。当线程获取独占式锁失败后就会将当前线程加入同步队列,那么加入队列的方式是怎样的?我们接下来就应该去研究一下addWaiter()
和acquireQueued()
。addWaiter()源码如下:
private Node addWaiter(Node mode) {// 1. 将当前线程构建成Node类型Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure// 2. 当前尾节点是否为null?Node pred = tail;if (pred != null) {// 2.2 将当前节点尾插入的方式插入同步队列中node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程enq(node);return node;
}
分析可以看上面的注释。程序的逻辑主要分为两个部分:
- 当前同步队列的尾节点为null,调用方法enq()插入;
- 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。
另外还会有另外一个问题:如果 if (compareAndSetTail(pred, node))为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。因此,经过我们这样的分析,enq()方法可能承担两个任务
:
- 处理当前同步队列尾节点为null时进行入队操作;
- 如果CAS尾插入节点失败后负责自旋进行尝试。
那么是不是真的就像我们分析的一样了?只有源码会告诉我们答案,enq()源码如下:
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initialize//1. 构造头结点if (compareAndSetHead(new Node()))tail = head;} else {// 2. 尾插入,CAS操作失败自旋尝试node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
在上面的分析中我们可以看出在第1步中会先创建头结点,说明同步队列是带头结点的链式存储结构。
带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。
那么带头节点的队列初始化时机是什么?自然而然是在tail为null时,即当前线程是第一次插入同步队列。compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在for (;;)死循环中不断尝试,直至成功return返回为止
。因此,对enq()方法可以做这样的总结:
在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化;自旋不断尝试CAS尾插入节点直至成功为止。
现在我们已经很清楚获取独占式锁失败的线程包装成Node然后插入同步队列
的过程了?那么紧接着会有下一个问题?在同步队列中的节点(线程)会做什么事情了来保证自己能够有机会获得独占式锁了?带着这样的问题我们就来看看acquireQueued()
方法,从方法名就可以很清楚,这个方法的作用就是排队获取锁的过程,源码如下:
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 1. 获得当前节点的先驱节点final Node p = node.predecessor();// 2. 当前节点能否获取独占式锁 // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁if (p == head && tryAcquire(arg)) {//队列头指针用指向当前节点setHead(node);//释放前驱节点p.next = null; // help GCfailed = false;return interrupted;}// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
程序逻辑通过注释已经标出,整体来看这是一个这又是一个自旋的过程(for (;
深入理解AbstractQueuedSynchronizer(AQS)相关推荐
- AbstractQueuedSynchronizer(AQS)源码实现
AbstractQueuedSynchronizer(AQS)源码实现 大多数开发者不会直接用到AQS,但是它涵盖的范围极为广泛.AbstractQueuedSynchronizer是并发类诸如Ree ...
- 同步类的基础AbstractQueuedSynchronizer(AQS)
同步类的基础AbstractQueuedSynchronizer(AQS) 我们之前介绍了很多同步类,比如ReentrantLock,Semaphore, CountDownLatch, Reentr ...
- 初识Lock与AbstractQueuedSynchronizer(AQS)
本文转载于:https://juejin.im/post/5aeb055b6fb9a07abf725c8c 一.concurrent包的结构层次 在针对并发编程中,Doug Lea大师为我们提供了大量 ...
- 彻底理解AbstractQueuedSynchronizer(二)
写在前面 在分析 Java 并发包 java.util.concurrent 源码的时候,少不了需要了解 AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是 J ...
- 扔掉源码,15张图带你彻底理解java AQS
java中AQS是AbstractQueuedSynchronizer类,AQS依赖FIFO队列来提供一个框架,这个框架用于实现锁以及锁相关的同步器,比如信号量.事件等. 在AQS中,主要有两部分功能 ...
- AbstractQueuedSynchronizer AQS源码分析
申明:jdk版本为1.8 AbstractQueuedSynchronizer是jdk中实现锁的一个抽象类,有排他和共享两种模式. 我们这里先看排他模式,共享模式后面结合java.util.concu ...
- java并发编程——九 AbstractQueuedSynchronizer AQS详解
文章目录 AbstractQueuedSynchronizer概述 AbstractQueuedSynchronizer的使用 AQS实现分析 同步队列 独占锁的获取与释放 独占式超时获取 共享式锁的 ...
- AQS理解之七——AQS中的条件队列
AQS中的条件队列 在AQS中还实现了一个类,ConditionObject,它实现了Condition接口,实现一个绑定在锁上的条件队列. 先看看他的uml图. 主要方法 它实现了Condition ...
- 深入理解JUC——AQS 及 ReentrantLock
AQS 框架 AQS 阻塞式锁和相关的同步器工具的框架.核心思想是:如果被请求的共享资源空闲,则将资源分配给工作线程,并将共享资源设置为锁定的状态:如果资源被占用的话,就需要一套线程阻塞等待以及被唤醒 ...
最新文章
- 前端开发面试题--html
- 理解 neutron(15):Neutron Linux Bridge + VLAN/VXLAN 虚拟网络
- 用饮水机教你什么是RAID [转]
- 数据结构实验之排序三:bucket sort
- 微信 小程序组件 焦点切换
- 记录——《C Primer Plus (第五版)》第十章编程练习第六题
- 【RL-GAN-Net】强化学习控制GAN网络,用于实时点云形状的补全。
- 西门子s7-200smart——2.cpu选型
- 轻量级协作机器人到仿人机器人的关键技术演进及生态共建
- mysql ubb html_自动闭合HTML/ubb标签函数
- 东北大学材料成型工艺学中期末复习
- [图像几何变换]——图像的仿射变换、透射变换及图像金字塔
- java中形参和实参的区别
- Unable to publish SessionDestroyedEvent for session (未解决)
- 当电脑开不了机出现自动修复时
- react中使用macy插件实现瀑布流布局
- linux系统是什么意思
- Centos 无法获取IP-- No suitable device found for this connection device lo not available because
- 树莓派什么都不装(包括python)测试摄像头的方法
- 计算机密码口令管理制度,密码使用管理制度