AQS是并发编程中非常重要的概念,它是juc包下的许多并发工具类,如CountdownLatch,CyclicBarrier,Semaphore 和锁, 如ReentrantLock, ReaderWriterLock的实现基础,提供了一个基于int状态码和队列来实现的并发框架。本文将对AQS框架的几个重要组成进行简要介绍,读完本文你将get到以下几个点:

  1. AQS进行并发控制的机制是什么

  2. AQS独占和共享模式是如何实现的

  3. 同步队列和条件等待队列的区别,和数据出入队原则

一,AQS基本概念

AQS(AbstractQueuedSynchronizer)是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量来表示状态,通过内置的FIFO(first in,first out)队列来完成资源获取线程的排队工作。

队列可分为两种,一种是同步队列,是程序执行入口出处的等待队列;而另一种则是条件等待队列,队列中的元素是在程序执行时在某个条件上发生等待。

1.1 独占or共享模式

AQS支持两种获取同步状态的模式既独占式和共享式。顾名思义,独占式模式同一时刻只允许一个线程获取同步状态,而共享模式则允许多个线程同时获取。

1.2 同步队列

当一个线程尝试获取同步状态失败时,同步器会将这个线程以及等待状态等信息构造成一个节点加入到等待队列中,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试重复获取同步队列。

1.3 条件队列

AQS内部类ConditionObject来实现的条件队列,当一个线程获取到同步状态,但是却通过Condition调用了await相关的方法时,会将该线程封装成Node节点并加入到条件队列中,它的结构和同步队列相同。

二,独占or共享模式

AQS框架中,通过维护一个int类型的状态,来进行并发控制,线程通常通过修改此状态信息来表明当前线程持有此同步状态。AQS则是通过保存修改状态线程的引用来实现独占和共享模式的。

  1. /**
  2. * 获取同步状态
  3. */
  4. public final void acquire(int arg) {
  5. //尝试获取同步状态, 如果尝试获取到同步状态失败,则加入到同步队列中
  6. if (!tryAcquire(arg) &&
  7. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  8. selfInterrupt();
  9. }
  10. /**
  11. * 尝试获取同步状态【子类中实现】,因为aqs基于模板模式,仅提供基于状态和同步队列的实
  12. * 现思路,具体的实现由子类决定
  13. */
  14. protected final boolean tryAcquire(int acquires) {
  15. final Thread current = Thread.currentThread();
  16. int c = getState();
  17. if (c == 0) {
  18. // 如果当前状态值为0,并且等待队列中没有元素,执行修改状态值操作
  19. if (!hasQueuedPredecessors() &&
  20. compareAndSetState(0, acquires)) {
  21. // 修改状态值成功,记录当前持有同步状态的线程信息
  22. setExclusiveOwnerThread(current);
  23. return true;
  24. }
  25. // 如果当前线程已经持有同步状态,继续修改同步状态【重入锁实现原理】
  26. } else if (current == getExclusiveOwnerThread()) {
  27. int nextc = c + acquires;
  28. if (nextc < 0)
  29. throw new Error("Maximum lock count exceeded");
  30. setState(nextc);
  31. return true;
  32. }
  33. return false;
  34. }
  35. /**
  36. * 根据传入的模式以及当前线程信息创建一个队列的节点并加入到同步队列尾部
  37. */
  38. private Node addWaiter(Node mode) {
  39. Node node = new Node(Thread.currentThread(), mode);
  40. // Try the fast path of enq; backup to full enq on failure
  41. Node pred = tail;
  42. if (pred != null) {
  43. node.prev = pred;
  44. if (compareAndSetTail(pred, node)) {
  45. pred.next = node;
  46. return node;
  47. }
  48. }
  49. enq(node);
  50. return node;
  51. }
  52. /**
  53. * 同步队列中节点,尝试获取同步状态
  54. */
  55. final boolean acquireQueued(final Node node, int arg) {
  56. boolean failed = true;
  57. try {
  58. boolean interrupted = false;
  59. // 自旋(死循环)
  60. for (;;) {
  61. // 只有当前节点的前驱节点是头节点时才会尝试执行获取同步状态操作
  62. final Node p = node.predecessor();
  63. if (p == head && tryAcquire(arg)) {
  64. setHead(node);
  65. p.next = null; // help GC
  66. failed = false;
  67. return interrupted;
  68. }
  69. if (shouldParkAfterFailedAcquire(p, node) &&
  70. parkAndCheckInterrupt())
  71. interrupted = true;
  72. }
  73. } finally {
  74. if (failed)
  75. cancelAcquire(node);
  76. }
  77. }

独占式是如何控制得?

当修改状态信息成功后,如果执行的是独占式操作,AQS的具体实现类中会保存当前线程的信息来声明同步状态已被当前线程占用,此时其他线程再尝试获取同步状态会返回false。

三,同步队列

3.1 队列中保存那些信息?

同步队列节点中主要保存着线程的信息以及模式(共享or独占)。

3.2 何时执行入队操作?

  1. /**
  2. * 获取同步状态
  3. */
  4. public final void acquire(int arg) {
  5. //尝试获取同步状态, 如果尝试获取到同步状态失败,则加入到同步队列中
  6. if (!tryAcquire(arg) &&
  7. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  8. selfInterrupt();
  9. }

复用上文中的代码,不难看出再获取同步状态失败后,会执行入队操作。

3.3 何时执行出队操作?

当线程获取同步状态失败时,会被封装成Node节点加入到等待队列中,此时所有节点都回进入自旋过程,首先判断自己prev是否时头节点,如果是则尝试获取同步状态。
被阻塞线程的唤醒主要以靠前驱节点的出队或阻塞线程被中断来实现。

  1. /**
  2. * 同步队列中节点,尝试获取同步状态
  3. *
  4. * 1. 当一个线程获取到同步状态时,会将当前线程构造程Node并设置为头节点
  5. * 2. 并将原始的head节点设置为null,以便于垃圾回收
  6. */
  7. final boolean acquireQueued(final Node node, int arg) {
  8. boolean failed = true;
  9. try {
  10. boolean interrupted = false;
  11. for (;;) {
  12. final Node p = node.predecessor();
  13. if (p == head && tryAcquire(arg)) {
  14. setHead(node);
  15. p.next = null; // help GC
  16. failed = false;
  17. return interrupted;
  18. }
  19. if (shouldParkAfterFailedAcquire(p, node) &&
  20. parkAndCheckInterrupt())
  21. interrupted = true;
  22. }
  23. } finally {
  24. if (failed)
  25. cancelAcquire(node);
  26. }
  27. }

四,条件等待队列

条件变量(ConidtionObject)是AQS中的一个内部类,用来实现同步队列机制。同步队列复用了等待队列中Node节点,所以同步队列到等待队列中不需要进行额外的转换。

4.1 什么时候执行入队操作?

当线程获取到同步状态,但是在临界区中调用了await()方法,此时该线程会被加入到对应的条件队列汇总。
ps: 临界区,加锁和释放锁之间的代码区域

  1. /**
  2. * ConditionObject中的await方法,调用后使得当前执行线程加入条件等待队列
  3. */
  4. public final void await() throws InterruptedException {
  5. if (Thread.interrupted())
  6. throw new InterruptedException();
  7. Node node = addConditionWaiter();
  8. // -----省略代码------
  9. }
  10. /**
  11. * 添加等待线程
  12. */
  13. private Node addConditionWaiter() {
  14. Node t = lastWaiter;
  15. // -----省略代码------
  16. // 将当前线程构造程条件队列节点,并加入到队列中
  17. Node node = new Node(Thread.currentThread(), Node.CONDITION);
  18. if (t == null)
  19. firstWaiter = node;
  20. else
  21. t.nextWaiter = node;
  22. lastWaiter = node;
  23. return node;
  24. }

4.2 什么时候执行出队操作?

当对应的Conditioni调用signial/signalAll()方法时回选择从条件队列中出队列,同步队列是通过自旋的方式获取同步状态,而条件队列中的节点则通过通知的方式出队。条件队列中的节点被唤醒后会加入到入口等待队列中。

  1. /**
  2. * 唤醒当前条件等到队列中的所有等待线程
  3. */
  4. public final void signalAll() {
  5. if (!isHeldExclusively())
  6. throw new IllegalMonitorStateException();
  7. Node first = firstWaiter;
  8. if (first != null)
  9. doSignalAll(first);
  10. }
  11. /**
  12. * 遍历队列,将元素从条件队列 加入到 同步队列
  13. */
  14. private void doSignalAll(Node first) {
  15. lastWaiter = firstWaiter = null;
  16. do {
  17. Node next = first.nextWaiter;
  18. first.nextWaiter = null;
  19. transferForSignal(first);
  20. first = next;
  21. } while (first != null);
  22. }
  23. final boolean transferForSignal(Node node) {
  24. // -----省略代码------
  25. // 执行入队操作,将node添加到同步队列中
  26. Node p = enq(node);
  27. int ws = p.waitStatus;
  28. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  29. LockSupport.unpark(node.thread);
  30. return true;
  31. }

五,总结

  1. 使用Node实现的FIFO队列,可以用于构建锁或者其他同步装置的基础框架
  2. 利用一个int类型的属性表示状态
  3. 使用模板方法模式,子类可以通过继承它来管理状态实现各种并发工具
  4. 可以同时实现独占和共享模式

本文对AQS的基本原理进行的简要的描述,对于子类的公平性和非公平行实现,中断,队列中节点的等待状态,cas等操作没有进行探讨,感兴趣的小伙伴可以进行源码阅读或者查阅相关资料。

六,Q&A

Question1: 在java中通常使用synchronized来实现方法同步,AQS中通过CAS保证了修改同步状态的一致性问题,那么对比synchronized,cas有什么优势不同与优势呢?你还知道其他无锁并发的策略吗?

并发编程-深入浅出AQS的更多相关文章

  1. JUC并发编程基石AQS之主流程源码解析

    前言 由于AQS的源码太过凝练,而且有很多分支比如取消排队.等待条件等,如果把所有的分支在一篇文章的写完可能会看懵,所以这篇文章主要是从正常流程先走一遍,重点不在取消排队等分支,之后会专门写一篇取消排 ...

  2. JUC并发编程基石AQS源码之结构篇

    前言 AQS(AbstractQueuedSynchronizer)算是JUC包中最重要的一个类了,如果你想了解JUC提供的并发编程工具类的代码逻辑,这个类绝对是你绕不过的.我相信如果你是第一次看AQ ...

  3. 【并发编程】AQS学习

    一个简单的示例: package net.jcip.examples; import java.util.concurrent.locks.*; import net.jcip.annotations ...

  4. JUC 并发编程--11, AQS源码原理解析, ReentrantLock 源码解读

    这里引用别人博客,不重复造轮子 https://blog.csdn.net/u012881584/article/details/105886486 https://www.cnblogs.com/w ...

  5. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  6. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  7. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  8. Scala 深入浅出实战经典 第68讲:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载: 百度云盘:http://pan.baidu.com/s/1c0noOt ...

  9. Scala 深入浅出实战经典 第67讲:Scala并发编程匿名Actor、消息传递、偏函数解析

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-87讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

随机推荐

  1. 解决VS2017授权问题及没有Add ArcGIS License Checking问题

    内容源自:ArcGIS Engine+C#入门经典 老版本采用: 控件布局好后,需要对程序添加License许可.在Visual Studio的菜单栏上单击“项目”→单击“Add ArcGIS Lic ...

  2. PHP array_shift

    1.函数的作用:删除数组的头个元素并返回 2.函数的参数: @params array  &$array 3.需要注意的例子: <?php /** * http://php.net/ma ...

  3. [NOIp2014] luogu P2296 寻找道路

    不知道是因为我菜还是别的,最近老是看错题. 题目描述 在有向图 GGG 中,每条边的长度均为 1,现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件: 路径上的所有点的出边所指向 ...

  4. 【Redis】Could not get a resource from the pool 实乃集群配置问题

    先说些题外话~自上次确诊为鼻窦炎+过敏性鼻炎到现在已经一个月了,最初那会,从下午到晚上头疼难忍.大概是积劳成疾,以前流鼻涕.打喷嚏的时候从来没有注意过,结果病根一下爆发. 关键在于锁定问题,开始治疗一 ...

  5. Redis集群与高可用性技术小结

    客户端分片,这种方式需要实现特定的客户端,需要手工配置redis实例并根据算法进行访问,对于redis实例的增减,调整灵活性很差,一般不推荐. 代理分片,常见的有Twemproxy架构(豆瓣创建了co ...

  6. SpringBoot2.x--入门篇--01--HelloWorld

    很多人说,学习springboot至少需要spring基础,servlet基础等等,笔者不敢苟同.凡是有一定java基础的人,都可以直接学习springboot,当学到原理和源码时,通过查缺补漏的方式 ...

  7. Git & Github 使用教程【1】入门篇

    Github教程 1-1 版本管理工具简介 主要作用:备份文件.记录历史.回到过去.多端共享.独挡一面.团队协作 2-1 版本管理工具发展历史 3-1 Git下载和安装[略] 3-2 linux下安装 ...

  8. 最近学到的ABTest知识

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 如果之前看过我文章的同学就知道我在工作中搞的是推送系 ...

  9. The usage of Markdown---表格

    更新时间:2019.09.14   谈到怎么在Markdown中插入表格,其实只要熟知以下几点就可以了: 使用管道符|进行内容的分割 使用冒号:和连号符-表示表格内容的对齐情况,连号符-在中间,冒号: ...

  10. Android9.0 如何区分SDK接口和非 SDK接口

    刚刚有同学问我,不太了解 "非SDK接口" 是什么意思?android9.0有什么限制 ?apache的http也有限制 ? 而且现在的大部分系统都升级上来了,黑名单.灰名单和白名 ...