本文部分摘自《Java 并发编程的艺术》

概述

队列同步器 AbstractQueuedSynchronize(以下简称同步器),是用来构建锁(Lock)或者其他同步组件(JUC 并发包)的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,子类推荐被定义为自定义同步组件的静态内部类。同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义组件使用

一言以蔽之,同步器是实现锁(也可以是任意同步组件)的一种方式,它屏蔽了更加底层的一些机制,使开发者更易于理解和使用

队列同步器的接口

同步器的设计是基于模板方法模式的,使用者需要继承队列同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法

1. 访问或修改同步状态

重写同步器指定的方法时,需要使用同步器提供的如下三个方法来访问或修改同步状态:

  • getState()

    获取当前同步状态

  • setState(int newState)

    设置当前同步状态

  • compareAndSetState(int expect, int update)

    使用 CAS 设置当前状态,该方法能保证状态设置的原子性

2. 同步器可重写的方法

方法名称 描述
protected boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态,并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态
protected boolean tryRelease(int arg) 独占式地释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于 0 的值,表示获取成功,否则获取失败
protected boolean tryReleaseShared(int arg) 共享式释放同步状态
protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占有,一般该方法表示是否被当前线程所独占

3. 同步器提供的模板方法

方法名称 描述
void acquire(int arg) 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的 tryAcquire(int arg) 方法
void acquireInterruptibly(int arg) 与 acquire(int arg) 相同,但该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出 InterruptedException 并返回
boolean tryAcquireNanos(int arg, long nanos) 在 acquireInterruptibly(int arg) 的基础上增加了超时限制
void acquireShared(int arg) 共享式的获取同步状态,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg) 与 acquireShared(int arg) 相同,该方法响应中断
boolean tryAcquireSharedNanos(int arg, long nanos) 在 acquireSharedInterruptibly 的基础上增加了超时限制
boolean release(int arg) 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg) 共享式的释放同步状态
Collection<Thread> getQueuedThreads() 获取等待在同步队列上的线程集合

4. 示例

下面通过一个独占锁的示例来深入了解一下同步器的工作原理。顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能获取锁

  1. public class Mutex implements Lock {
  2. /**
  3. * 自定义同步器
  4. */
  5. private static class Sync extends AbstractQueuedSynchronizer {
  6. @Override
  7. protected boolean isHeldExclusively() {
  8. // 是否处于占用状态
  9. return getState() == 1;
  10. }
  11. @Override
  12. public boolean tryAcquire(int acquires) {
  13. // 当状态为 0 时获取锁
  14. if (compareAndSetState(0, 1)) {
  15. setExclusiveOwnerThread(Thread.currentThread());
  16. return true;
  17. }
  18. return false;
  19. }
  20. @Override
  21. protected boolean tryRelease(int releases) {
  22. // 释放锁,将状态设置为 0
  23. if (getState() == 0) {
  24. throw new IllegalMonitorStateException();
  25. }
  26. setExclusiveOwnerThread(null);
  27. setState(0);
  28. return true;
  29. }
  30. /**
  31. * 返回一个 Condition, 每个 condition 都包含一个 condition 队列
  32. */
  33. Condition newCondition() {
  34. return new ConditionObject();
  35. }
  36. }
  37. private final Sync sync = new Sync();
  38. @Override
  39. public void lock() {
  40. sync.acquire(1);
  41. }
  42. @Override
  43. public void lockInterruptibly() throws InterruptedException {
  44. sync.acquireInterruptibly(1);
  45. }
  46. @Override
  47. public boolean tryLock() {
  48. return sync.tryAcquire(1);
  49. }
  50. @Override
  51. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  52. return sync.tryAcquireNanos(1, unit.toNanos(time));
  53. }
  54. @Override
  55. public void unlock() {
  56. sync.release(1);
  57. }
  58. @Override
  59. public Condition newCondition() {
  60. return sync.newCondition();
  61. }
  62. }

Mutex 中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。用户使用 Mutex 时并不会直接和内部同步器实现打交道,而是调用 Mutex 提供的方法,大大降低了实现一个可靠自定义组件的门槛

队列同步器的实现

1. 同步队列

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

节点是构成同步队列的基础,同步器拥有首节点(head)和尾结点(tail),没有成功获取同步状态的线程将会成为节点并加入该队列的尾部

同步队列的基本结构如下:

同步器将节点加入到同步队列的过程如图所示:

首节点是获取同步状态成功的节点,首节点线程在释放同步状态时,会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,过程如下:

设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取同步状态,因此设置头节点的方法并不需要使用 CAS 来保证,只需要将首节点设置成原首节点的后继节点并断开原首节点的 next 引用即可

2. 独占式同步状态获取与释放

通过调用同步器的 acquire(int arg) 方法可以获取同步状态,该方法对中断不敏感,线程获取同步状态失败则进入同步队列中,后续对线程进行中断操作,线程不会从同步队列中移出

独占式同步状态获取流程,也就是 acquire(int arg) 方法调用流程如图所示:

如果当前线程获取同步状态失败,就会生成一个节点(独占式 Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态),并加入到队列尾部。一个队列里有很多节点,而只有前驱节点是头节点的节点才能尝试获取同步状态,原因有两个:

  • 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点
  • 维护同步队列的 FIFO 原则

因此,如果队列中的非头节点线程的前驱节点出队或者被中断而从等待状态返回,那么其随后会检查自己的前驱是否为头节点,如果是则尝试获取同步状态

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后继节点能够继续获取同步状态。通过调用同步器的 release(int arg) 方法可以释放同步状态,该方法执行时,会唤醒头节点的后继节点线程

3. 共享式同步状态获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,若一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如下图所示:

通过调用同步器的 acquireShared(int arg) 方法可以共享式地获取同步状态,其代码核心逻辑和 acquire() 差不多,也是判断当前节点的前驱是否为头节点,如果是就尝试获取同步状态。头节点在释放同步状态之后,也会唤醒后续处于等待状态的节点

问题的关键在于如何做到多个线程访问同步状态,因为按照上面所讲的过程,和独占式几乎没有任何区别。独占式与共享式在实现上的差别其实仅仅在于:每次头节点释放同步状态之后,独占式只是把其后继节点设置为头节点,而共享式还多了一个传播的过程(笔者能力有限,这一块没搞明白,就不瞎写了。。)

与独占式一样,共享式获取也需要释放同步状态,通过调用 releaseShared(int arg) 方法可以释放同步状态,并唤醒后续处于等待状态的节点

4. 独占式超时获取同步状态

通过调用同步器的 doAcquireNanos(int arg, long nanosTimeout) 方法可以超时获取同步状态,即在指定的时间段内获取同步状态

在介绍这个方法之前,先介绍一下响应中断的同步状态获取过程。Java5 以后,同步器提供了 acquireInterruptibly(int arg) 方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出 InterruptedException

超时获取同步状态可以视为响应中断获取同步状态的增强版。独占式超时和非独占式获取在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int arg) 在未获取到同步状态时,会使当前线程一致处于等待状态,而 doAcquireNanos(int arg, long nanosTimeout) 会使当前线程等待 nanosTimeout 纳秒,如果当前线程在 nanosTimeout 纳秒内没有获取同步状态,将会从等待逻辑中自动返回

自定义同步组件

设计一个同步工具:同一时刻,只能允许至多两个线程同时访问,超过两个线程的访问将被阻塞。显然这是共享式访问,主要设计思路如下:

  • 重写 tryAcquireShared(int args) 方法和 tryReleaseShared(int args) 方法
  • 定义初始状态 status 为 2,当一个线程进行获取,status 减 1,该线程释放,status 加 1,为 0 时再有其他线程进行获取,则阻塞

示例代码如下:

  1. public class TwinsLock implements Lock {
  2. private final Sync sync = new Sync(2);
  3. private static final class Sync extends AbstractQueuedSynchronizer {
  4. Sync(int count) {
  5. if (count <= 0) {
  6. throw new IllegalArgumentException("count must large than zero");
  7. }
  8. setState(count);
  9. }
  10. @Override
  11. public int tryAcquireShared(int reduceCount) {
  12. while (true) {
  13. int current = getState();
  14. int newCount = current - reduceCount;
  15. if (newCount < 0 || compareAndSetState(current, newCount)) {
  16. return newCount;
  17. }
  18. }
  19. }
  20. @Override
  21. protected boolean tryReleaseShared(int reduceCount) {
  22. while (true) {
  23. int current = getState();
  24. int newCount = current + reduceCount;
  25. if (compareAndSetState(current, newCount)) {
  26. return true;
  27. }
  28. }
  29. }
  30. Condition newCondition() {
  31. return new ConditionObject();
  32. }
  33. }
  34. @Override
  35. public void lock() {
  36. sync.acquireShared(1);
  37. }
  38. @Override
  39. public void lockInterruptibly() throws InterruptedException {
  40. sync.acquireInterruptibly(1);
  41. }
  42. @Override
  43. public boolean tryLock() {
  44. return sync.tryAcquireShared(1) > 0;
  45. }
  46. @Override
  47. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  48. return sync.tryAcquireSharedNanos(1, unit.toNanos(time));
  49. }
  50. @Override
  51. public void unlock() {
  52. sync.releaseShared(1);
  53. }
  54. @Override
  55. public Condition newCondition() {
  56. return sync.newCondition();
  57. }
  58. }

再编写一个测试来验证 TwinsLock 是否按预期工作

  1. public class TwinsLockTest {
  2. public static void main(String[] args) {
  3. final Lock lock = new TwinsLock();
  4. class Worker extends Thread {
  5. @Override
  6. public void run() {
  7. while (true) {
  8. lock.lock();
  9. try {
  10. SleepUtils.second(1);
  11. System.out.println(Thread.currentThread().getName());
  12. SleepUtils.second(1);
  13. } finally {
  14. lock.unlock();
  15. }
  16. }
  17. }
  18. }
  19. for (int i = 0; i < 10; i++) {
  20. Worker worker = new Worker();
  21. worker.setDaemon(true);
  22. worker.start();
  23. }
  24. for (int i = 0; i < 10; i++) {
  25. SleepUtils.second(1);
  26. System.out.println();
  27. }
  28. }
  29. }

运行该测试用例,发现线程名称成对输出,说明同一时刻只有两个线程能够获取到锁

Java 队列同步器 AQS的更多相关文章

  1. Java 显示锁 之 队列同步器AQS(六)

    1.简述 锁时用来控制多个线程访问共享资源的方式,一般情况下,一个锁能够防止多个线程同时访问共享资源.但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁. 在Java 5.0之前,在协调对共享对 ...

  2. Java 中队列同步器 AQS(AbstractQueuedSynchronizer)实现原理

    前言 在 Java 中通过锁来控制多个线程对共享资源的访问,使用 Java 编程语言开发的朋友都知道,可以通过 synchronized 关键字来实现锁的功能,它可以隐式的获取锁,也就是说我们使用该关 ...

  3. 并发——抽象队列同步器AQS的实现原理

    一.前言   这段时间在研究Java并发相关的内容,一段时间下来算是小有收获了.ReentrantLock是Java并发中的重要部分,所以也是我的首要研究对象,在学习它的过程中,我发现它是基于抽象队列 ...

  4. Java中的队列同步器AQS

    一.AQS概念 1.队列同步器是用来构建锁或者其他同步组件的基础框架,使用一个int型变量代表同步状态,通过内置的队列来完成线程的排队工作. 2.下面是JDK8文档中对于AQS的部分介绍 public ...

  5. JAVA并发-同步器AQS

    什么是AQS aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLo ...

  6. Java并发包下锁学习第二篇Java并发基础框架-队列同步器介绍

    Java并发包下锁学习第二篇队列同步器 还记得在第一篇文章中,讲到的locks包下的类结果图吗?如下图: ​ 从图中,我们可以看到AbstractQueuedSynchronizer这个类很重要(在本 ...

  7. AbstractQueuedSynchronizer 队列同步器源码分析

    AbstractQueuedSynchronizer 队列同步器(AQS) 队列同步器 (AQS), 是用来构建锁或其他同步组件的基础框架,它通过使用 int 变量表示同步状态,通过内置的 FIFO ...

  8. Java并发之AQS原理解读(一)

    前言 本文简要介绍AQS以及其中两个重要概念:state和Node. AQS 抽象队列同步器AQS是java.util.concurrent.locks包下比较核心的类之一,包括AbstractQue ...

  9. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

随机推荐

  1. P3164 [CQOI2014]和谐矩阵(高斯消元 + bitset)

    题意:构造一个$n*m$矩阵 使得每个元素和上下左右的xor值=0 题解:设第一行的每个元素值为未知数 可以依次得到每一行的值 然后把最后一行由题意条件 得到$m$个方程 高斯消元解一下 bitset ...

  2. HDU 6880 Permutation Counting dp

    题意: 给你一个n和一个长度为n-1的由0/1构成的b序列 你需要从[1,n]中构造出来一个满足b序列的序列 我们设使用[1,n]构成的序列为a,那么如果ai>ai+1,那么bi=1,否则bi= ...

  3. Codeforces Round #582 (Div. 3) E. Two Small Strings (构造,思维,全排列)

    题意:给你两个长度为\(2\)的字符串\(s\)和\(t\),你需要构造一个长度为\(3n\)的字符串,满足:含有\(n\)个\(a\),\(n\)个\(b\),\(n\)个\(c\),并且\(s\) ...

  4. Codeforces Round #660 (Div. 2) C. Uncle Bogdan and Country Happiness (DFS)

    题意:有\(n\)个人,每个人居住在某个节点,所有人都在节点\(1\)上班,下班后沿着最短路径回家,在回家途中心情可能会变差(心情只会变差不会变好),每个节点都有一个开心值,开心值等于所有经过时的好心 ...

  5. Qt开发Activex笔记(一):环境搭建、基础开发流程和演示Demo

    前言   使用C#开发动画,绘图性能跟不上,更换方案使用Qt开发Qt的控件制作成OCX以供C#调用,而activex则是ocx的更高级形式.  QtCreator是没有Active控件项目的,所有需要 ...

  6. codeforces 1013B 【思维+并查集建边】

    题目链接:戳这里 转自:参考博客 题意:给一个n*m的矩阵,放入q个点,这q个点之间的关系是,若已知这样三个点(x1,y1),(x2,y1),(x1,y2),可以在(x2,y2)处生成一个新的点,对于 ...

  7. 51nod-1065 最小正子段和 【贪心 + 思维】

    N个整数组成的序列a[1],a[2],a[3],-,a[n],从中选出一个子序列(a[i],a[i+1],-a[j]),使这个子序列的和>0,并且这个和是所有和>0的子序列中最小的. 例如 ...

  8. 杭电多校HDU 6579 Operation (线性基 区间最大)题解

    题意: 强制在线,求\(LR\)区间最大子集异或和 思路: 求线性基的时候,记录一个\(pos[i]\)表示某个\(d[i]\)是在某个位置更新进入的.如果插入时\(d[i]\)的\(pos[i]\) ...

  9. 链接脚本再探和VMA与LMA

    链接脚本简单描述 连接脚本的描述都是以节(section)的单位的,网上也有很多描述链接脚本语法的好文章,再不济还有官方的说明文档可以用来学习,其实主要就是对编译构建的整个过程有了深入的理解后就能对链 ...

  10. <U+200B> for, Zero Width Space ❌

    <U+200B> for, Zero Width Space zsh, bash https://www.cnblogs.com/xgqfrms/p/14233264.html#47944 ...