AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架。主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现。

AQS的使用方式是通过子类继承来实现,子类继承同步器并且实现抽象方法来完成同步,实现过程中涉及到同步状态的方法主要有:

getState():获取同步状态

setState(int newState):设置同步状态

compareAndSetState(int expect,int update):通过CAS操作来设置同步状态,保证操作的原子性

一、AQS的使用

AQS需要子类继承并实现抽象方法来实现,而需要重写的方法如下:

 protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false

 protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;

 protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;

 protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false

 protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。

另外AQS还提供了大量的模板方法供子类使用,主要分成三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程信息

acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;

acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;

tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;

acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;

acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;

tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;

release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;

releaseShared(int arg):共享式释放同步状态;

Collection<Thread> getQueuedThreads():获取等待在同步队列中的线程集合

二、AQS的实现原理

AQS内部依赖一个FIFO双向队列来完成同步状态的管理,当前线程获取同步状态失败时,会将当前线程及等待信息构成成一个Node节点加入到同步队列中,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的节点主要属性有:获取同步状态失败的线程引用、等待状态、前驱节点、后继节点等,同步器拥有首节点和尾节点,没有成功获取同步状态的线程会成为节点并加入队列的尾部。如下图示:

同步队列器中包含一个指向头节点的节点引用,一个指向尾节点的节点引用。当一个线程成功获取到了锁,则其他线程无法获取锁就会被构造成节点,加入到队列的尾部,由于尾节点只有一个,所以这个加入尾部的操作必须是线程安全的。

同步对列器采用的CAS操作来对尾节点进行设置的。compareAndSetTail(Node expect,Node update).第一个参数是之前的尾节点,第二个参数是当前节点。

1.队列的头节点就是当前线程获取到同步锁的节点,当头节点释放锁时,会唤醒后继节点,后继节点会尝试获取同步锁,获取成功就将自己设置成首节点。

2.设置首节点是通过获取同步锁成功的线程来完成的,由于只有一个线程能够获取到同步锁,所以设置头节点的方法不需要采用CAS操作,只需要将首节点设置为旧的的首节点的next节点,并将next引用断开即可

三、锁的获取和释放

这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

3.1、独占式同步状态的获取和释放源码解析

独占式同步状态的获取是通过方法acquire(int arg)方法来获取的,源码如下:

     public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

首先是执行子类实现的tryAcquire方法来独占式获取锁,如果获取成功则方法结束;如果获取失败则先通过addWaiter方法来创建节点并且加入到队列的尾部,源码如下:

 /**创建Node节点,并加入同步队列尾部*/
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.获取当前尾部节点,也就是新加入节点的前驱节点
Node pred = tail;
if (pred != null) {
//3.当前驱节点不为空时,则将新节点的prev引用指向前驱节点
node.prev = pred;
//4.由于尾部节点可能存在并发情况,获取需要CAS操作来设置
if (compareAndSetTail(pred, node)) {
//5.设置成功之后将旧的尾节点的next指向新节点,并返回
pred.next = node;
return node;
}
}
//5.如果CAS失败,则进入死循环不停自旋来CAS设置尾部节点,直到成功为止
enq(node);
return node;
}
  private Node enq(final Node node) {
for (;;) {
Node t = 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;
}
}
}
}

创建了节点并加入尾部之后通过acquiredQueued方法使得该节点不停自旋获取同步状态,获取不到则阻塞线程,而被阻塞的线程的唤醒需要依靠前驱节点的出队或阻塞线程被中断,源码如下:

  /**节点进入队列之后通过自旋来获取同步状态,直到获取同步状态成功*/
final boolean acquireQueued(final Node node, int arg) {
//失败标识
boolean failed = true;
try {
//中断标识
boolean interrupted = false;
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
/**
* 当当前节点当前驱节点为head节点时,才尝试调用子类实现的tryAcquire方法来获取同步状态,否则直接返回
* 因为队列时FIFO的,当头节点释放同步状态后,只有后继节点才可获取同步状态,其他节点暂时无权获取
*/
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);
}
}

第15行的判断意思是只有当节点前驱节点是头节点时才能够尝试获取同步锁。前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire方法返回,也就是当前线程获取到了锁。

当线程获取到了锁并执行了业务逻辑之后,会需要释放锁,并且唤醒后继节点。调用同步器的release方法可以释放同步锁,源码如下:

 /**独占式释放同步状态*/
public final boolean release(int arg) {
//调用子类释放同步状态方法
if (tryRelease(arg)) {
//释放成功获取当前head节点
Node h = head;
//当当前节点的状态不为0时,则尝试唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

3.2、共享式同步状态的获取和释放源码解析

调用AQS的acquireShared方法获取同步状态,直接调用子类实现的tryAcquireShared方法,如果返回值大于等于0则表示获取同步状态成功直接返回;如果小于0则表示获取同步状态失败,则调用doAcquireShared方法

 /**共享式获取同步状态*/
public final void acquireShared(int arg) {
//调用子类获取同步状态方法,小于0表示获取同步状态失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);//失败之后调用AQS的doAcquireShared(int arg)方法
}

doAcquireShared方法源码如下:

 private void doAcquireShared(int arg) {
//创建共享模式的节点调用addWaiter方法添加到同步队列的尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;//失败标识
try {
boolean interrupted = false;//中断标识
//死循环尝试获取同步状态
for (;;) {
//获取新节点的前驱节点
final Node p = node.predecessor();
//当前驱节点为head节点时继续尝试执行tryAcquireShared方法
if (p == head) {
int r = tryAcquireShared(arg);
//当返回值大于等于0时表示获取同步状态成功,则
if (r >= 0) {
//设置head节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

可以看出共享式和独占式的获取同步状态的流程,都是先调用子类的获取同步状态的方法,如果成功则直接返回;如果失败则通过死循环来不停尝试当前驱节点为首节点时开始尝试获取同步状态,直到成功。

共享式释放同步状态的源码如下示:

 /**共享式释放同步状态*/
public final boolean releaseShared(int arg) {
//当调用子类当tryReleaseShared方法成功时调用AQS的doReleaseShared方法;否则直接返回false
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
 private void doReleaseShared() {
for (;;) {
Node h = head;//获取当前首节点
if (h != null && h != tail) {
int ws = h.waitStatus;
//判断首节点状态释放为signal,如果是则需要唤醒后继节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

共享式锁和独占式锁的主要区别在于同一时刻是否允许多个线程同时获取到锁。比如一个文件,写操作同一时间只允许一个线程在写,可以使用独占式锁,但是读操作是可以允许多个线程同时进行读操作的,则可以使用共享式锁。

四、AQS等待通知机制

Java并发编程3-抽象同步队列AQS详解的更多相关文章

  1. Java并发编程:线程封闭和ThreadLocal详解

    转载请标明出处: http://blog.csdn.net/forezp/article/details/77620769 本文出自方志朋的博客 什么是线程封闭 当访问共享变量时,往往需要加锁来保证数 ...

  2. Java并发编程--多线程中的join方法详解

    Java Thread中, join()方法主要是让调用该方法的thread在完成run方法里面的部分后, 再执行join()方法后面的代码 例如:定义一个People类,run方法是输出姓名年龄. ...

  3. java并发编程笔记(六)——AQS

    java并发编程笔记(六)--AQS 使用了Node实现FIFO(first in first out)队列,可以用于构建锁或者其他同步装置的基础框架 利用了一个int类型表示状态 使用方法是继承 子 ...

  4. 【Java并发编程二】同步容器和并发容器

    一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...

  5. java并发编程基础——线程同步

    线程同步 一.线程安全问题 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安 ...

  6. 并发编程补充--方法interrupted、isinterrupted详解

    并发编程 interrupted()源码 /** * Tests whether the current thread has been interrupted. The * <i>int ...

  7. Java 异步编程 (5 种异步实现方式详解)

    ​ 同步操作如果遇到一个耗时的方法,需要阻塞等待,那么我们有没有办法解决呢?让它异步执行,下面我会详解异步及实现@mikechen 目录 什么是异步? 一.线程异步 二.Future异步 三.Comp ...

  8. 王家林系列之scala--第69讲:Scala并发编程react、loop代码实战详解

    刚才看了一下,群里王家林老师又更新课程了,真为王老师的勤奋感到佩服,于是迫不及待的下载下来观看学习.本期讲的是关于scala并发编程的react.loop代码实战. 信息来源于 DT大数据梦工厂微信公 ...

  9. Java并发编程,互斥同步和线程之间的协作

    互斥同步和线程之间的协作 互斥同步 Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLo ...

随机推荐

  1. 新版gitbook导出pdf

    文章目录 gitbook自带的npm模块gitbook 使用vscode的插件Markdown PDF 使用CommandBox GitBook Exporter 最近想把自己写的一个gitbook转 ...

  2. vue + ArcGIS 地图应用系列一:arcgis api本地部署(开发环境)

    封面 1. 下载 ArcGIS API for JavaScript 官网地址: https://developers.arcgis.com/javascript/3/ 下载地址:http://lin ...

  3. 【DNS域名解析命令】 ping

    ping, ping6 - send ICMP ECHO_REQUEST to network hosts ping命令向网络主机发送ICMP回传请求 详细描述: ping使用ICMP协议强制ECHO ...

  4. vue-cli创建的webpack工程中引用ExtractTextPlugin导致css背景图设置无效的解决方法

    当我们用vue-cli创建项目后,如果在我们的template模板文件中的css样式设置中,有设置了background-image的属性,并且url值传入的是相对路径,那么当我们在打包生产代码时,w ...

  5. KafkaConsumer assign VS subscribe

    背景 在kafka中,正常情况下,同一个group.id下的不同消费者不会消费同样的partition,也即某个partition在任何时刻都只能被具有相同group.id的consumer中的一个消 ...

  6. Mockjs+Ajax实践

    需要完成的工作:利用mock js随机生成数据,通过ajax请求,获取这些数据并展示在网页中. 一 mock js随机生成数据 官方文档中,Mock.mock( ),可以说是mock的精髓所在. Mo ...

  7. 谈谈你对vuex的理解

    vuex创建公有仓库的插件 1.储存公共状态 2.能够根据事件来修改状态 3.多个组件都需要变化,有机制把这个新的状态通知给所有的组件 vuex中的四个类 1.state    定义需要共享的状态 2 ...

  8. 爱创课堂每日一题第五十四天- 列举IE 与其他浏览器不一样的特性?

    IE支持currentStyle,FIrefox使用getComputStyle IE 使用innerText,Firefox使用textContent 滤镜方面:IE:filter:alpha(op ...

  9. Node Mysql事务处理封装

    node回调函数的方式使得数据库事务貌似并没有像java.php那样编写简单,网上找了一些事务处理的封装并没有达到自己预期的那样简单编写,还是自己封装一个吧.封装的大体思路很简单:函数接受一个事务处理 ...

  10. redis系列之5----redis实战(redis与spring整合,分布式锁实现)

    本文是redis学习系列的第五篇,点击下面链接可回看系列文章 <redis简介以及linux上的安装> <详细讲解redis数据结构(内存模型)以及常用命令> <redi ...