是心里有火,眼里有光。
目录
摘要
AQS是什么?我们为什么需要它?- 为什么要学习
AQS?不学可不可以? - 从实际生活中的面试场景出发,我们来看看
AQS究竟做了什么事情。 - 让我们一步步来分析一下CountDownLatch底层是如何实现的,是否我们可以利用
AQS写一个自己的并发工具类?
AQS
一、AQS是什么?
行不改姓,坐不改名,AbstractQueuedSynchronizer
是也。
现在集中一下注意力,先观察观察,锁和协助类是不是都有一个共同点:闸门
- 思考一下,有没有发现我们在上一章中讲的
ReentrantLock和Semaphore很相似?每次只允许一定数量的线程通过?
- 不止这两货,还有
CountDownLatch、ReentrantReadWriteLock都有这样的类似的协作(同步)功能,其实,他们底层都用了一个共同的基类:AQS - 所以这些相似点就被抽取出来,造就了
AQS
AQS的类。
总结一下:AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。有了AQS之后,更多的协作工具类都可以很方便的被写出来。
二、为什么需要AQS?
当我们问为什么需要一个东西的时候,其实可以想一想,如果没有会怎么样?
如果没有AQS,就需要每一个协作工具类(如上图中)自己实现:
- 同步状态的原子管理(例如:现在到底是锁住呢还是没锁住呢?)
- 线程的阻塞和解除阻塞(一个线程啥时候接入等锁状态?又如何被唤醒?)
- 队列的管理(线程等锁去哪儿等?)
场景举例
面试类别有群面、单面。
Semaphore、CountDownLatch这些就相当于面试官;
而安排就坐、叫号、处理先来后到等HR工作的就是AQS。
面试官不关心面试者应该坐在哪、轮到谁了、哪个先来的,他只需要告诉HR,我一次面几个人(即我的面试规则)。
这样就有了一个对应比喻:
- Semaphore:一个人面完后,下一个才能进来
- CountDownLatch:群面,等5人到齐一起面
那我们为什么要学习AQS?不学可不可以?
不学当然可以,因为就算是你学了,你也基本上不可能去使用AQS写一个协作类什么的。
但是我们把话说回来,多了解一些经典的设计,对我们以后设计复杂系统具有很大的帮助,或许我们不会再去写并发类,但是这种思想,我们可以用在自己的业务场景上,可能就会出奇制胜。
三、AQS的底层原理是什么?
三大核心要素
- 同步状态控制:state
- 这里state含义会根据具体实现类的不同而不同,比如在
Semaphore里,它表示“剩余的许可证数量”;而在CountDownLatch里,它表示“还需要倒数的数量”;在ReentrantLock里,它用来表示“锁的占有情况”,当state为0的时候,标识Lock不被如何线程所占有。 - state是被volatile修饰的,会被并发的修改,所以所有修改的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。
- 这里state含义会根据具体实现类的不同而不同,比如在
- 控制线程抢锁和配合的FIFO队列(线程排队管理器)
-
期望协助工具类去实现的获取/释放等重要方法
先看一个经典的实现
CountDownLatch,它就在Sync中实现了AQS的获取和释放方法:-
获取方法:依赖state变量,经常会阻塞(比如获取不到锁的时候)
在
Semaphore中,获取就是acquire方法,表示获取许可证。那我们来追踪一下这条链路上AQS的作用:// Semaphore 的 acquire 方法,用于获取许可证 public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); } /* * Semaphore 的内部由三个类与AQS有关联 * Sync, NonfairSync, FairSync * 我们这次就挑 NonfairSync 来看看 */ // 进入 sync.acquireSharedInterruptibly 我们就来到了 AQS 的方法 // 在这里调用了 tryAcquireShared 方法,这个方法就是由 NonfairSync 实现的 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) // 处理线程阻塞等操作 doAcquireSharedInterruptibly(arg); } // 来到这儿我们就发现了,是不是终于找到了 “获取许可证” 逻辑 protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; // 判断剩余许可证数量,并使用CAS修改 if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }在
CountDownLatch中,获取就是await方法,作用是“等待,直到倒数结束” -
释放方法:释放操作不会阻塞。
Semaphore里,释放就是release方法,表示“归还许可证”;在CountDownLatch中,释放就是countDown方法,代表“倒数一个数”。/* * 这次我们就直接来 Semaphore 的 * Sync 类 * tryReleaseShared 方法 * 调用链: * Semaphore.release(i) -> AQS.releaseShared(i) -> Semaphore.Sync.tryReleaseShared(i) */ protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } }
四、利用AQS实现一个并发工具类
先分析一下CountDownLatch是如何实现的
主要分析:
- 构造函数:一个CountDownLatch是如何被初始化的
- getCount:count在AQS里到底代表了什么?
- await:如何实现等待
- countDown:如何实现“倒数”
/** * 1. 构造函数 */ public CountDownLatch(int count) { // 校验count值 if (count < 0) throw new IllegalArgumentException("count < 0"); // 初始化内部继承了AQS的Sync类 this.sync = new Sync(count); } // Sync类构造函数 Sync(int count) { // 设置AQS的成员变量 state setState(count); } // AQS的 state 赋值函数,非线程安全 protected final void setState(int newState) { state = newState; } /** * 2. getCount */ public long getCount() { // 调用Sync的getCount方法获取count值 return sync.getCount(); } int getCount() { // 调用AQS的getState方法获取state值 return getState(); } protected final int getState() { // 返回state值 return state; } /** * 3. await 等待倒数结束 */ public void await() throws InterruptedException { // 调用AQS的可中断的获取方法 // 这里传入的参数 1 对CountDownLatch的await本身并无意义 sync.acquireSharedInterruptibly(1); } // AQS提供的可中断的获取方法 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 判断是否被中断,如果是,直接返回中断异常 if (Thread.interrupted()) throw new InterruptedException(); // 判断是否满足获取条件,如果不满足,则线程放入等待队列 if (tryAcquireShared(arg) < 0) // 调用CountDownLatch.Sync.tryAcquireShared doAcquireSharedInterruptibly(arg); } // CountDownLatch.Sync的可并发获取的方法 protected int tryAcquireShared(int acquires) { // 如果state = 0,则返回 1,表示此时已经倒数为 0,调用await的线程无需阻塞 // 如果state > 0,则返回 -1,表示仍然需要倒数,调用await的线程需要放入等待队列 return (getState() == 0) ? 1 : -1; } // AQS提供的方法,基本作用为将当前线程放入等待队列并让它阻塞 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 将当前线程包装为Node节点 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); if (p == head) { // 如果p为头结点,说明当前节点在真实数据队列的首部(头结点是虚节点) // 尝试判断state是否为 0 int r = tryAcquireShared(arg); if (r >= 0) { // state为0,该线程不用阻塞 // 设置当前节点为头结点(虚节点) setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } // 看方法意思:需要被阻塞因为获取失败 /* * 到这一步由两种情况 * 1. p为头节点但当前线程没有获取(tryAcquire)成功 * 2. p不是头节点 */ // 判断当前节点是否需要被阻塞 if (shouldParkAfterFailedAcquire(p, node) // 靠前驱节点判断当前线程是否应该被阻塞 && parkAndCheckInterrupt() // 挂起当前线程,阻塞调用栈,返回当前线程的中断状态 ) throw new InterruptedException(); } } finally { if (failed) // 将当前节点的状态标记为CANCELLED cancelAcquire(node); } } /** * 4. countDown 倒数一个数 */ public void countDown() { // 调用Sync的释放方法,本质上为count - 1,可并发调用 sync.releaseShared(1); } // AQS的释放方法 public final boolean releaseShared(int arg) { // 调用CountDownLatch.Sync.tryReleaseShared方法 if (tryReleaseShared(arg)) { // count已经为0,唤醒等待队列中的线程,即执行await后等待的线程 doReleaseShared(); return true; } return false; } // Sync.tryReleaseShared方法,可并发访问 // 将count减一,如果count == 0则唤醒队列中等待的线程 protected boolean tryReleaseShared(int releases) { // 使用CAS将state - 1 for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) // 如果减一后state为0,则执行唤醒操作 return nextc == 0; } } // AQS提供的唤醒线程方法,可并发访问 private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; 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的用法
- 写一个类,想好协作逻辑,实现获取/释放方法(比如
CountDownLatch的await和countDown方法) - 内部写一个
Sync类,继承AbstractQueuedSynchronizer - 根据是否独占来重写 tryAcquire/tryRelease方法(独占) 或 tryAcquireShared/tryReleaseShared方法(共享),在之前写的获取/释放方法(如await)中调用
AQS的acquire/release or Shared方法
实现自己的协作类:一次性门闩
协作逻辑:类似CountDownLatch(1)。现在有一个门闩,刚开始是关闭的,如果此很多线程过来想通过这个门闩,则全部陷入等待;当门闩被打开,则等待中的线程全部被唤醒执行。
小伙伴们看到这儿可以先停一下,如果让你来实现,你该怎么写?
话不多说,上代码,可以从主方法开始看起:
public class OneShotLatch { /** * 主方法 */ public static void main(String[] args) throws InterruptedException { // 初始化一个门闩 OneShotLatch oneShotLatch = new OneShotLatch(); // 开启10个线程请求通过门闩,此时线程均会阻塞 for (int i = 0; i < 10; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 尝试请求通过门闩,失败就等待"); oneShotLatch.await(); System.out.println("开门放行 " + Thread.currentThread().getName() + " 继续运行"); }).start(); } Thread.sleep(5000); // 开启门闩,10个线程会全部被唤醒,继续执行 oneShotLatch.signal(); // 再开启一个线程去请求通过门闩,此时直接通过 new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 尝试请求通过门闩,失败就等待"); oneShotLatch.await(); System.out.println("开门放行 " + Thread.currentThread().getName() + " 继续运行"); }).start(); } /** * 内部类继承AQS */ private class Sync extends AbstractQueuedSynchronizer { /** * 重写 获取 方法 */ @Override protected int tryAcquireShared(int arg) { // 初始state为 0,表示门闩关闭 // 如果state为 0,则返回 -1,因为AQS的acquireShared方法判断该方法返回值 < 0时会将线程放入等待队列 // 如果state为 1,则返回 1,表示线程无需阻塞,直接通过 return (getState() == 1) ? 1 : -1; } /** * 重写 释放 方法 */ @Override protected boolean tryReleaseShared(int arg) { // 将state置为1,表示门闩打开了 setState(1); // 返回true,让AQS执行唤醒队列中的线程操作 return true; } } /** * 初始化,state为 0,标识门闩关闭 */ private final Sync sync = new Sync(); /** * 打开门闩 */ public void signal() { // 调用AQS提供的释放方法,AQS会调用我们重写的释放方法,即类Sync的tryReleaseShared方法 // 参数 0 无实际意义,因为在Sync我们暂未用到 sync.releaseShared(0); } /** * 请求通过门闩 */ public void await() { // 调用AQS提供的获取方法,AQS会调用我们重写的获取方法,即类Sync的tryAcquireShared方法 // 参数 0 无实际意义,因为在Sync我们暂未用到 sync.acquireShared(0); } }运行示意图:
总结
本篇脉络较为简单,开篇先概览一下
AQS是什么,如果没有AQS我们会怎么样?然后就开始举例介绍
AQS的三个核心(state、队列和获取/释放)。接下来就到了自己实现协作类的环节,不过在这个环节之前我们必须捋一捋,该如何去写这样一个玩意?
其实也很简单,我们先挑一个例子
CountDownLatch来分析一下它是怎么实现的,然后总结一下它的实现套路,最后再自己一步步写出来。预告:下一节我们来讲讲线程治理,当我们需要获取一个子线程的执行结果时候,我们该怎么办?这里面又是如何实现的?
PS:推荐一些很不错的资料
美团技术团队《从ReentrantLock的实现看AQS的原理及应用》:https://mp.weixin.qq.com/s/sA01gxC4EbgypCsQt5pVog 老钱《打通 Java 任督二脉 —— 并发数据结构的基石》:https://juejin.cn/post/6844903736578408461 HongJie《一行一行源码分析清楚AbstractQueuedSynchronizer》:https://javadoop.com/post/AbstractQueuedSynchronizer 爱吃鱼的KK《AbstractQueuedSynchronizer 源码分析 (基于Java 8)》:https://www.jianshu.com/p/e7659436538b waterystone《Java并发之AQS详解》:https://www.cnblogs.com/waterystone/p/4920797.html 英文论文的中文翻译:https://www.cnblogs.com/dennyzhangdd/p/7218510.html AQS作者的英文论文:http://gee.cs.oswego.edu/dl/papers/aqs.pdf -