Java并发编程之AQS
一、什么是AQS
AQS(AbstractQueuedSynchronize:队列同步器)是用来构建锁或者其他同步组件的基础框架,很多同步类都是在它的基础上实现的,比如常用的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore。
二、实现原理
在AQS内部,定义了一个 volatile int state 变量来标识同步状态,通过改变state的状态来控制对共享资源的访问,根据不同的实现,state可以表示不同的状态,例如:在 ReentrantLock
中它表示拥有该锁的线程请求了多少次该锁;在 Semaphore 中表示
剩余的许可数,在 FutureTask
中表示任务的状态(尚未开始、运行、完成和取消)。同时定义了一个 FIFO 队列维护争用资源时被阻塞的线程,当线程尝试获取锁时,如果锁已经被占用,那么该线程就会被构造成一个Node节点放到同步队列的尾部;队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。
AQS主要通过继承的方式来使用,子类通过继承AQS并实现它的抽象方法来定义state变量的具体的访问规则,从而可以实现不同类型的同步组件。AQS定义了两种资源共享的方式:独占式和共享式。
- 独占式:同时只有一个线程能访问该共享资源。
- 共享式:多个线程可以同时访问该共享资源。
AQS中可重写的方法如下:
- protected boolean tryAcquire(int arg):独占式获取同步状态,成功则返回true,失败则返回false。先查询同步状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
- protected boolean tryRelease(int arg):独占式释放同步状态,成功则返回true,失败则返回false。等待获取同步状态的线程将有机会获取同步状态。
- protected int tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值,表示成功,该值表示剩余可用资源数,小于0则表示获取失败。
- protected boolean tryReleaseShared(int arg):共享式释放同步状态,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- protected boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用,只在 AbstractQueuedSynchronizer.ConditionObject 的方法内进行内部调用,不使用Condition可以不重写。
AQS自身没有实现任何同步接口,为了保证对state的访问修改操作是安全的,重写AQS指定的方法时,需要使用它提供的如下3个方法来访问或修改同步状态:
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
AQS是实现锁(同步组件)的关键,它在底层对同步状态管理、线程的排队、等待与唤醒做了实现,简化锁的实现。AQS是基于模板方法模式设计的,使用时需要继承AQS并重写对应的方法,再将其组合在同步组件中,当使用同步组件访问共享资源时,调用AQS提供的模板方法,然后模板方法会调用重写的方法。AQS提供的模板方法大体上分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
- 独占式获取与释放同步状态
- public final void acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则返回,否则进入同步队列等待,该方法会调用tryAcquire(int arg)方法。
- public final void acquireInterruptibly(int arg):与 acquire(int arg) 相同,但是该方法响应中断,如果当前线程没有获取到同步状态,就进入到同步队列中,如果当前线程被中断(Thread().interrupt()),该方法会抛出InterruptedException并返回。
- public final boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly(int arg)的基础上增加了超时限制,如果在超时时间范围内没有获取到同步状态,就返回false,获取到了返回true。
- public final boolean release(int arg):独占式的释放同步状态,释放成功后,会释放同步队列第一个节点中的线程。
以上四个方法,获取同步状态的三个方法会调用重写的tryAcquire(int arg),release(int arg)会调用tryRelease(int arg)。
- 共享式获取与释放同步状态
public final void acquireShared(int arg):共享式的获取同步状态,如果当前线程未获取到同步状态,进入同步队列等待,与独占式获取的主要区别是可以有多个线程同时获取到同步状态。
public final void acquireSharedInterruptibly(int arg):与acquireShared(int arg)相同,可响应中断。
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly(int arg)的基础上增加了超时限制。
public final boolean releaseShared(int arg):共享式的释放同步状态。
以上四个方法,获取同步状态的三个方法会调用重写的tryAcquireShared(int arg),release(int arg)会调用tryReleaseShared(int arg)。
- 查询同步队列中的等待线程情况
public final Collection<Thread> getQueuedThreads():获取等待在同步队列上的线程集合。
public final boolean hasQueuedThreads():查询是否有正在等待获取的任何线程,如果可能有其他线程正在等待获取锁定,则返回 true。注意,随时可能因为中断和超时而导致取消操作,返回 true 并不能保证其他任何线程在等待获取对象。
三、源码实现
主要通过查看独占式同步状态的获取和释放(acquire(int arg)、release(int arg))、共享式同步状态的获取和释放(acquireShared(int arg)、releaseShared(int arg))这几个方法的源码分析AQS的实现。
- acquire(int arg):此方法是独占模式下线程获取共享资源的顶层入口。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}先来看下acquire方法中大致的工作流程:
- tryAcquire(arg):自定义同步器重写的获取同步状态的方法,如果成功,就直接返回,否则继续执行。
- addWaiter(Node.EXCLUSIVE):将当前线程封装成Node,并设置Node为独占模式,然后添加到同步队列的尾部。
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;
}addWaiter(Node mode)
- acquireQueued(Node,arg):使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
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);
}
}acquireQueued(final Node node, int arg)
- selfInterrupt():根据acquireQueued()的结果,如果线程被中断过,则会调用selfInterrupt()做中断操作,否则不调用该方法。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}selfInterrupt()
- release(int arg):此方法是独占模式下线程释放共享资源的顶层入口。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}释放共享资源的操作相对会简单一点,首先调用重写的tryRelease(arg),释放成功(state = 0),会获取同步队列的头结点,如果头结点不为空并且waitStatus != 0(0代表初始化状态),则调用unparkSuccessor(h)唤醒该节点中的线程并返回true。释放失败直接返回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.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}unparkSuccessor(Node node)
注:waitStatus 表示当前Node结点中线程的等待状态,共有5种状态INITIAL、CANCELLED、SIGNAL、CONDITION、PROPAGATE。初始化时为0,大于0表示取消状态,小于0表示有效状态。
- INITIAL:值为0,表示初始状态。
CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
3. acquireShared(int arg):此方法是共享模式下线程获取共享资源的顶层入口。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- tryAcquireShared(arg):先调用重写的获取同步状态的方法,如果获取成功,直接返回,获取失败则继续执行。返回值大于等于0表示获取成功,返回值表示剩余可用资源的个数;反之表示获取失败。
- 这里有一点需要注意:假设当前线程获取资源时发现可用资源不够,当前线程会继续阻塞等待其他线程释放资源,而不会唤醒后面的线程。
- doAcquireShared(arg):将线程加入同步队列并设置nextWait为SHARED常量,表示当前节点是共享的,然后在队列中获取资源,直到获取到资源后才返回。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//添加到同步队列尾部
boolean failed = true;//失败标志
try {
boolean interrupted = false;//中断标志
for (;;) {
final Node p = node.predecessor();//获取上一个节点
if (p == head) {//上一个节点是头节点才会尝试获取资源
int r = tryAcquireShared(arg);//尝试获取资源
if (r >= 0) {//获取成功
setHeadAndPropagate(node, r);//设置为头节点,如果还有剩余资源则继续唤醒后面节点中的线程
p.next = null; // help GC
if (interrupted)//如果过程中有发生过中断,则进行中断操作
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())//shouldParkAfterFailedAcquire()判断是否需要阻塞当前线程,如果该方法返回true,则调用parkAndCheckInterrupt()方法来阻塞线程,阻塞后返回当前线程的中断标志,如果为true,则将interrupted 改为true。
interrupted = true;
}
} finally {
if (failed)//当前线程是否已经中断,如果中断,failed为true
cancelAcquire(node);//将线程从同步队列中移除,并唤醒下一个节点。
}
}doAcquireShared(int arg)
然后看一下setHeadAndPropagate(Node node, int propagate)、shouldParkAfterFailedAcquire(Node pred, Node node)和parkAndCheckInterrupt()这几个方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);//设置为头节点
//如果还有剩余资源则唤醒下一个线程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
setHeadAndPropagate(Node node, int propagate)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)//如果前驱节点为SIGNAL,后继节点为等待状态 - 阻塞
return true;
if (ws > 0) {//CANCELLED,如果前驱节点已经被中断或取消,则跳过所有状态为Node.CANCELLED的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//CAS设置状态为Node.SINGAL
}
return false;
}
shouldParkAfterFailedAcquire(Node pred, Node node)
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞当前线程
return Thread.interrupted();//返回中断状态并设置为false
}
parkAndCheckInterrupt()
4. releaseShared(int arg):此方法是共享模式下线程释放共享资源的顶层入口。这个方法和独占式同步状态的释放方法(release(int arg))差不多,区别就在独占方式下要完全释放资源后(即state=0,因为独占下可重入,所以state的值可能会大于1)才会唤醒后面的线程,而releaseShared(int arg)在释放了资源后(可以释放部分资源)就可以唤醒后面的线程。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
- tryReleaseShared(int arg):调用重写的释放共享资源的方法尝试释放资源,释放成功唤醒下一个节点中并返回true,失败则返回false。
- doReleaseShared():唤醒下一个节点中的线程。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {//同步队列不为空并且有阻塞的节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//如果都节点状态为SINGAL
//设置头节点状态为初始状态,成功则唤醒下一个节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//如果头节点状态等于0,表示已经获取共享状态成功,通过CAS将状态设置为PROPAGATE,如果CAS操作失败,就一直循环
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}doReleaseShared()
到这里,独占式同步状态的获取和释放(acquire(int arg)、release(int arg))、共享式同步状态的获取和释放(acquireShared(int arg)、releaseShared(int arg))这几个方法的源码大致过了一遍,对AQS内部的实现算是有了一个基本的了解。其他几个响应中断的方法和前面看的几个类似,后面再看看具体的使用。
参考资料:
书籍:《Java并发编程的艺术》
博客:http://www.cnblogs.com/waterystone/p/4920797.html
博客:https://blog.csdn.net/u014674862/article/details/83021022
Java并发编程之AQS的更多相关文章
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- Java并发编程之CAS第一篇-什么是CAS
Java并发编程之CAS第一篇-什么是CAS 通过前面几篇的学习,我们对并发编程两个高频知识点了解了其中的一个—volatitl.从这一篇文章开始,我们将要学习另一个知识点—CAS.本篇是<凯哥 ...
- Java并发编程之CAS二源码追根溯源
Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...
- Java并发编程之CAS第三篇-CAS的缺点及解决办法
Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...
- Java并发编程之set集合的线程安全类你知道吗
Java并发编程之-set集合的线程安全类 Java中set集合怎么保证线程安全,这种方式你知道吗? 在Java中set集合是 本篇是<凯哥(凯哥Java:kagejava)并发编程学习> ...
- Java并发编程之Lock
重入锁ReentrantLock 可以代替synchronized, 但synchronized更灵活. 但是, 必须必须必须要手动释放锁. try { lock.lock(); } finally ...
- Java并发编程之synchronized关键字
整理一下synchronized关键字相关的知识点. 在多线程并发编程中synchronized扮演着相当重要的角色,synchronized关键字是用来控制线程同步的,可以保证在同一个时刻,只有一个 ...
- Java 并发编程之 Condition 接口
本文部分摘自<Java 并发编程的艺术> 概述 任意一个 Java 对象,都拥有一个监视器方法,主要包括 wait().wait(long timeout).notify() 以及 not ...
- Java 并发编程之volatile关键字解析
摘录 1. 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执 ...
随机推荐
- C++动态库的几点认识
1.动态库也有lib文件,称为导入库,一般大小只有几k: 2.动态库有静态调用和动态调用两种方式: 静态调用:使用.h和.lib文件 动态调用: 先LoadLibrary,再GetProcAddres ...
- MacBook上那些好用的工具们
https://blog.csdn.net/qq_33833327/article/details/78454703
- C#在SharePoint文档库下动态新增文件夹
/// <summary> /// 在创建SP文库库下动态新增文件夹 /// </summary> /// <param name="spList"& ...
- 卸载HDP大数据平台
使用以下说明卸载HDP: 停止所有已安装的HDP服务.请参阅HDP参考指南中的停止HDP服务. 如果安装了Knox,请在所有群集节点上运行以下命令: 对于RHEL / CentOS / Oracle ...
- bootstrap table使用参考
https://www.cnblogs.com/landeanfen/p/5821192.html 转载 阅读目录 一.x-editable组件介绍 二.bootstrapTable行内编辑初始方案 ...
- navicat连接mysql出现2059错误
最近在学习django的时候需要用到数据库,于是便下载了navicat准备和mysql配套使用,但是在连接的时候确出现了如下问题: 网上查询过后,发现这个错误出现的原因是在mysql8之前的版本中加密 ...
- app -webkit-box-orient: vertical 打包后不显示
先说明问题是什么: -webkit-box-orient: vertical 这个属性在本地运行调试是存在的,但是打包后这个属性消失了: 解决办法: 1.将-webkit-box-orient: ve ...
- Exp2 后门原理与实践-------20164325王晓蕊
Exp2 后门原理与实践 任务一:使用netcat获取主机操作Shell,cron启动 1.windows获取linux shell 1.1 ipconfig 查看本机查看以太网适配器(8)的ipv ...
- 剑指C++面试
传闻公司老总欠下巨款,带着小姨子跑路了~ 树倒猢狲散,接下来要好好准备面试,以期找到一份满意的工作. 面试准备分下面几个方面进行,形成面试系列文章,文章内容以问答的方式呈现. 1.C++语言基础 传 ...
- 【面试必备】常见Java面试题大综合
一.Java基础 1.Arrays.sort实现原理和Collections.sort实现原理答:Collections.sort方法底层会调用Arrays.sort方法,底层实现都是TimeSort ...