写在前面

  在前面提到过,AQS是构建Java同步组件的基础,我们期待它能够成为实现大部分同步需求的基础。AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态,对于子类而言它并没有太多的活要做,AQS提供了大量的模板方法来实现同步,主要是分为三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程情况。自定义子类使用AQS提供的模板方法就可以实现自己的同步语义。

独占式

  独占式,同一时刻仅有一个线程持有同步状态。

  独占式同步状态获取


  acquire(int arg)方法为AQS提供的模板方法,该方法为独占式获取同步状态,但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。代码如下:

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

各个方法定义如下:

  1. tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
  2. addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
  3. acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
  4. selfInterrupt:产生一个中断。

acquireQueued方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。如下:

    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);
}
}

从上面代码中可以看到,当前线程会一直尝试获取同步状态,当然前提是只有其前驱节点为头结点才能够尝试获取同步状态,理由:

  1. 保持FIFO同步队列原则。
  2. 头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己是否为头节点。

acquire(int arg)方法流程图如下:

  独占式获取响应中断


  AQS提供了acquire(int arg)方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态。为了响应中断,AQS提供了acquireInterruptibly(int arg)方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException。

    public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

  首先校验该线程是否已经中断了,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。doAcquireInterruptibly(int arg)定义如下:

private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  doAcquireInterruptibly(int arg)方法与acquire(int arg)方法仅有两个差别。1.方法声明抛出InterruptedException异常,2.在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。

  独占式超时获取


  AQS除了提供上面两个方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)。该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。如下:

   public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

  tryAcquireNanos(int arg, long nanosTimeout)方法超时获取最终是在doAcquireNanos(int arg, long nanosTimeout)中实现的,如下:

 private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//nanosTimeout <= 0
if (nanosTimeout <= 0L)
return false;
//超时时间
final long deadline = System.nanoTime() + nanosTimeout;
//新增Node节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//自旋
for (;;) {
final Node p = node.predecessor();
//获取同步状态成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
/*
* 获取失败,做超时、中断判断
*/
//重新计算需要休眠的时间
nanosTimeout = deadline - System.nanoTime();
//已经超时,返回false
if (nanosTimeout <= 0L)
return false;
//如果没有超时,则等待nanosTimeout纳秒
//注:该线程会直接从LockSupport.parkNanos中返回,
//LockSupport为JUC提供的一个阻塞和唤醒的工具类,后面做详细介绍
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//线程是否已经中断了
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  针对超时控制,程序首先记录唤醒时间deadline ,deadline = System.nanoTime() + nanosTimeout(时间间隔)。如果获取同步状态失败,则需要计算出需要休眠的时间间隔nanosTimeout(= deadline - System.nanoTime()),如果nanosTimeout <= 0 表示已经超时了,返回false,如果大于spinForTimeoutThreshold(1000L)则需要休眠nanosTimeout ,如果nanosTimeout <= spinForTimeoutThreshold ,就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS会进行无条件的快速自旋。

  整个流程如下:

  独占式同步状态释放


   当线程获取同步状态后,执行完相应逻辑后就需要释放同步状态。AQS提供了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(int arg)方法来释放同步状态,释放成功后,会调用unparkSuccessor(Node node)方法唤醒后继节点。

总结一下:

  在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的对尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。

共享式

  共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

 共享式同步状态获取


  AQS提供acquireShared(int arg)方法共享式获取同步状态:  

    public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//获取失败,自旋获取同步状态
doAcquireShared(arg);
}

  从上面程序可以看出,方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。自选式获取同步状态如下:

    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())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  tryAcquireShared(int arg)方法尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。

  acquireShared(int arg)方法不响应中断,与独占式相似,AQS也提供了响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos),这里就不做解释了。

  共享式同步状态释放


  获取同步状态后,需要调用release(int arg)方法释放同步状态,方法如下:

    public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

  因为可能会存在多个线程同时进行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。

参考资源

Doug Lea:《Java并发编程实战》
方腾飞:《Java并发编程的艺术》

j.u.c系列(04)---之AQS:同步状态的获取与释放的更多相关文章

  1. J.U.C之AQS:同步状态的获取与释放

    此篇博客所有源码均来自JDK 1.8 在前面提到过,AQS是构建Java同步组件的基础,我们期待它能够成为实现大部分同步需求的基础.AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象 ...

  2. Java并发包源码学习系列:CLH同步队列及同步资源获取与释放

    目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...

  3. 2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放

    首先我们从java.util.concurrent.locks包中的AbstraceQueuedSynchronizer说起,在下文中称为AQS. AQS是一个用于构建锁和同步器的框架.例如在并发包中 ...

  4. 3.从AbstractQueuedSynchronizer(AQS)说起(2)——共享模式的锁获取与释放

    在上节中解析了AbstractQueuedSynchronizer(AQS)中独占模式对同步状态获取和释放的实现过程.本节将会对共享模式的同步状态获取和释放过程做一个解析.上一节提到了独占模式和共享模 ...

  5. j.u.c系列(03)---之AQS:AQS简介

    写在前面 Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchron ...

  6. j.u.c系列(07)---之读写锁:ReentrantReadWriteLock

    写在前面 重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少.然而读服务不存在数据竞争问题,如果一 ...

  7. 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

  8. 013-并发编程-java.util.concurrent.locks之-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放

    一.概述 AbstractQueuedSynchronizer (简称AQS),位于java.util.concurrent.locks.AbstractQueuedSynchronizer包下, A ...

  9. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

随机推荐

  1. .net中的一般处理程序实例

    最近在学习一般处理程序,也学习了一些jQuery的异步操作,于是就想着亲手做一个小的登陆,锻炼一下自己. 1.首先新建了一个项目LoginDemo,在此基础上又添加了一个一般处理程序BackLogin ...

  2. 【SSH异常】InvalidDataAccessApiUsageException异常

    今天在整合SSH的时候,一开始我再测试的时候service层添加了注解事务调用DAO可以正常的保存,在环境中我在XML中采用了spring的OpenSessionInViewFilter解决hiber ...

  3. 使用Docx4j创建word文档

    原文标题:Creating Word documents with Docx4j 原文链接:http://blog.iprofs.nl/2012/09/06/creating-word-documen ...

  4. Quartus II 破解教程—FPGA入门教程【钛白Logic】

    这一节主要说明如何破解Quartus II 13.1.首先找到我们提供的破解工具,这里我们的电脑是64位的,所以使用64位破解器.如下图. 第一步:将破解工具拷贝到安装目录下“D:\altera\13 ...

  5. win7下iis中配置php.ini文件

    将php.ini-development配置文件重命名为php.ini配置文件即可. 接着做如下配置操作: 1.修改php.ini配置文件 打开php.ini配置文件,找到 12 ; On windo ...

  6. JPA 菜鸟教程 15 继承-一个表-SINGLE_TABLE

    原地址:http://blog.csdn.net/JE_GE/article/details/53678422 继承映射策略 一个类继承结构一个表的策略,最终只生成一个表,这是继承映射的默认策略. 举 ...

  7. Redis Scan命令

    原地址:https://www.cnblogs.com/tekkaman/p/4887293.html [Redis Scan命令] SCAN cursor [MATCH pattern] [COUN ...

  8. Ubuntu下mysql使用

    1. 从网上安装 sudo apt-get install mysql-server.装完已经自动配置好环境变量,可以直接使用mysql的命令. 注:建议将/etc/apt/source.list中的 ...

  9. Java实现继承过程概述

    super(); 在调用子类的构造器的时候,如果没有显示的写出 super(); ,那么,编译器会在佛那个加上 super(); 无参构造器 如果想调用父类的有参构造器,那么,必须显示的调用,编译器不 ...

  10. Java IO流杂谈

    创建文件和删除文件(File类) import java.io.File; import java.io.IOException; public class T2 { public static vo ...