AQS - JUC(四)

572 阅读10分钟

是心里有火,眼里有光。

目录

  1. 线程池与ThreadLocal - JUC(一)
  2. 锁与CAS - JUC(二)
  3. 并发容器与并发控制 - JUC(三)
  4. AQS - JUC(四)

摘要

  • AQS是什么?我们为什么需要它?
  • 为什么要学习AQS?不学可不可以?
  • 从实际生活中的面试场景出发,我们来看看AQS究竟做了什么事情。
  • 让我们一步步来分析一下CountDownLatch底层是如何实现的,是否我们可以利用AQS写一个自己的并发工具类?

AQS

一、AQS是什么?

行不改姓,坐不改名,AbstractQueuedSynchronizer 是也。

现在集中一下注意力,先观察观察,锁和协助类是不是都有一个共同点:闸门

  • 思考一下,有没有发现我们在上一章中讲的ReentrantLockSemaphore很相似?每次只允许一定数量的线程通过?
  • 不止这两货,还有CountDownLatchReentrantReadWriteLock都有这样的类似的协作(同步)功能,其实,他们底层都用了一个共同的基类:AQS
  • 所以这些相似点就被抽取出来,造就了AQS
上图就是有使用到AQS的类。

总结一下:AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。有了AQS之后,更多的协作工具类都可以很方便的被写出来。

二、为什么需要AQS?

当我们问为什么需要一个东西的时候,其实可以想一想,如果没有会怎么样?

如果没有AQS,就需要每一个协作工具类(如上图中)自己实现:

  1. 同步状态的原子管理(例如:现在到底是锁住呢还是没锁住呢?)
  2. 线程的阻塞和解除阻塞(一个线程啥时候接入等锁状态?又如何被唤醒?)
  3. 队列的管理(线程等锁去哪儿等?)

场景举例

面试类别有群面、单面。

Semaphore、CountDownLatch这些就相当于面试官;

而安排就坐、叫号、处理先来后到等HR工作的就是AQS。

面试官不关心面试者应该坐在哪、轮到谁了、哪个先来的,他只需要告诉HR,我一次面几个人(即我的面试规则)。

这样就有了一个对应比喻:

  • Semaphore:一个人面完后,下一个才能进来
  • CountDownLatch:群面,等5人到齐一起面

那我们为什么要学习AQS?不学可不可以?

不学当然可以,因为就算是你学了,你也基本上不可能去使用AQS写一个协作类什么的。

但是我们把话说回来,多了解一些经典的设计,对我们以后设计复杂系统具有很大的帮助,或许我们不会再去写并发类,但是这种思想,我们可以用在自己的业务场景上,可能就会出奇制胜。

三、AQS的底层原理是什么?

三大核心要素

  1. 同步状态控制:state
    • 这里state含义会根据具体实现类的不同而不同,比如在Semaphore里,它表示“剩余的许可证数量”;而在CountDownLatch里,它表示“还需要倒数的数量”;在ReentrantLock里,它用来表示“锁的占有情况”,当state为0的时候,标识Lock不被如何线程所占有。
    • state是被volatile修饰的,会被并发的修改,所以所有修改的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。
  1. 控制线程抢锁和配合的FIFO队列(线程排队管理器)
  1. 期望协助工具类去实现的获取/释放等重要方法

    先看一个经典的实现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是如何实现的

    主要分析:

    1. 构造函数:一个CountDownLatch是如何被初始化的
    2. getCount:count在AQS里到底代表了什么?
    3. await:如何实现等待
    4. 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的用法

    1. 写一个类,想好协作逻辑,实现获取/释放方法(比如CountDownLatch的await和countDown方法)
    2. 内部写一个Sync类,继承AbstractQueuedSynchronizer
    3. 根据是否独占来重写 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