Concurrent -- 03 -- AQS源码解析

原文链接:Concurrent – 03 – AQS源码解析


相关文章:


AQS (AbstractQueuedSynchronizer) 是用于构建锁或其他同步组件的基础框架类,在 Java 中许多并发工具类的内部类实现都依赖于 AQS,如:ReentrantLock、Semaphore、CountDownLatch 等,是 JUC 包的核心组件


一、实现原理

  • AQS 的核心思想是

    • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态

    • 如果被请求的共享资源被占用,则会阻塞当前请求资源的线程,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,当持有锁的线程释放锁后,会唤醒队列中的后继线程


二、核心概念

  • Node

    • 是 AQS 的核心内部类,用于包裹线程以及相关状态信息,是其他一切操作的基础
  • Condition

    • 是 JUC 包提供的一个与 Object 类中 wait()、notify()、notifyAll() 方法类似功能的一个接口

    • 其定义了 await()、awaitNanos(long)、signal()、signalAll() 等方法,配合对象锁 (如:ReentrantLock) 可以用于实现线程的选择性通知

    • 在 AQS 中的具体实现为内部类 ConditionObject

  • CLH

    • CLH (是算法提出者 Craig、Landin、Hagersten 的名字简称) 是由 AQS 内部维护的一个 FIFO (先进先出) 的虚拟双向队列 (即不存在队列实例,仅存在节点之间的关联关系),AQS 依赖其来管理等待中的线程

      image

  • waitStatus (节点状态)

    • waitStatus (volatile int waitStatus;) 用于表示 Node 节点的状态,有以下 5 个值

      状态作用
      CANCELLED (1)表示当前节点已取消调度,
      当节点中线程等待超时或被中断时,进入该状态的节点不会再发生变化
      SIGNAL (-1)表示后继节点正在等待当前节点的唤醒,
      后继节点进入队列后,会将前驱节点的状态更改为 SIGNAL (-1)
      CONDITION (-2)表示当前节点等待在 Condition 上,
      当其他线程调用了 Condition 的 signal() 方法后,
      该节点会从等待队列转移到同步队列中,等待获取同步锁
      PROPAGATE (-3)表示在共享模式下,
      当前节点不仅会唤醒其后继节点,同时也可能会唤醒后继的后继节点
      0新节点加入队列时的默认状态
  • state (同步状态)

    • state (private volatile int state;) 用于表示同步状态,子类可以根据实际需要,灵活地定义该变量的值来达到想要的效果

      • 在 ReentrantLock 中,使用 state 来表示某个线程获取独占锁的次数,获取锁时 + 1,释放锁时 - 1

      • 在 Semaphore 中,使用 state 来表示许可数目 (在初始化时设定),获取许可时 - 1,释放许可时 + 1

      • 在 CountDownLatch 中,使用 state 来表示计数器 (在初始化时设定),倒计时一次 -1,直至计数器为 0

  • 资源共享方式

    共享方式含义
    Exclusive (独占)static final Node EXCLUSIVE = null;
    只有一个线程能执行,如:ReentrantLock,可分为公平锁和非公平锁
    Share (共享)static final Node SHARED = new Node();
    多个线程可同时执行,如:Semaphore、CountDownLatch

三、设计模式

  • AQS 底层使用了模板方法模式,如果需要自定义同步器,一般方式如下

    • 使用者继承 AQS,并重写特定的方法,主要重写以下 5 个方法

      方法作用
      tryAcquire(int)尝试获取独占锁
      tryRelease(int)尝试释放独占锁
      tryAcquireShared(int)尝试获取共享锁
      tryReleaseShared(int)尝试释放共享锁
      isHeldExclusively()判断当前线程是否获得了独占锁
    • 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法则会调用使用者重写的方法

  • 模板方法模式

    • 模板方法模式,定义了一个操作中的算法的骨架,并将一些步骤延迟到子类中去实现,它使得子类可以在不改变一个算法结构的情况下,就可以重新定义该算法中的某些特定步骤

    • 模板方法模式可以把子类中不变的行为抽象到父类,从而去除子类中重复的代码,帮助子类摆脱重复的不变行为的纠缠


四、源码方法详解

  • 获取独占锁的流程

    • acquire(int arg)

      public final void acquire(int arg) {
          if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
              selfInterrupt();
      }
      
      • 获取独占锁,忽略中断

      • 若获取成功,线程直接返回;若获取失败,线程会被阻塞,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止

      • 可以用于实现 Lock.lock() 方法

    • tryAcquire(int arg)

      protected boolean tryAcquire(int arg) {
          throw new UnsupportedOperationException();
      }
      
      • 尝试获取独占锁,该方法没有具体实现,交由子类实现

      • 可以用于实现 Lock.tryLock() 方法

    • addWaiter(Node)

      private Node addWaiter(Node mode) {
          /*
           * 根据当前线程和给定模式,创建一个 Node 节点
           * model: SHARED(共享)、EXCLUSIVE(独占)
           */
          Node node = new Node(Thread.currentThread(), mode);
          // 将 pred 节点的引用指向尾部节点
          Node pred = tail;
          // 如果 pred 节点不为 null
          if (pred != null) {
              // 则将 pred 节点设置为 node 节点的前驱节点
              node.prev = pred;
              // 使用 CAS 操作将尾部节点由 pred 节点更新为 node 节点
              if (compareAndSetTail(pred, node)) {
                  // 将 node 节点设置为 pred 节点的后继节点
                  pred.next = node;
                  return node;
              }
          }
          /*
           * 如果 pred 节点为 null,或上述 
           * compareAndSetTail(pred, node) 操作失败,
           * 则调用 enq(final Node node) 方法将 node
           * 节点加入到 CLH 队列尾部
           */
          enq(node);
          return node;
      }
      
      • 根据当前线程和给定模式,创建一个 Node 节点,将其加入到 CLH 队列尾部
    • enq(Node)

      private Node enq(final Node node) {
          for (;;) {
              // 将 t 节点的引用指向尾部节点
              Node t = tail;
              // 如果 t 节点为 null
              if (t == null) {
                  /*
                   * 说明队列为空,则创建一个新的 Node
                   * 节点作为队列的头部节点和尾部节点
                   */
                  if (compareAndSetHead(new Node()))
                      tail = head;
              // 如果 t 节点不为 null
              } else {
                  // 说明队列不为空,则将 t 节点设置为当前节点的前驱节点
                  node.prev = t;
                  // 使用 CAS 操作将尾部节点由 t 节点更新为当前节点
                  if (compareAndSetTail(t, node)) {
                      // 将当前节点设置为 t 节点的后继节点
                      t.next = node;
                      return t;
                  }
              }
          }
      }
      
      • 将当前节点加入到 CLH 队列尾部
    • acquireQueued(final Node node, int arg)

      final boolean acquireQueued(final Node node, int arg) {
          // 标记是否成功获取资源
          boolean failed = true;
          try {
              // 标记等待过程中是否被中断过
              boolean interrupted = false;
              for (;;) {
                  // 获取当前节点的前驱节点 p
                  final Node p = node.predecessor();
                  /*
                   * 如果 p 节点为头部节点,则当前节点会尝试
                   * 获取独占锁,如果获取成功,则进行后续流程
                   */
                  if (p == head && tryAcquire(arg)) {
                      // 设置当前节点为头部节点
                      setHead(node);
                      /*
                       * 此时 CLH 队列的头部节点为当前节点,
                       * 原先的头部节点 p 已处理完资源出队,
                       * 因此将 p 节点的 next 引用设置为 null
                       * 方便 GC 对 P 节点进行回收
                       */
                      p.next = null; // help GC
                      failed = false;
                      return interrupted;
                  }
                  /*
                   * 如果 p 节点不为头部节点,则根据 p 节点
                   * 的状态来判断是否要阻塞当前节点
                   */
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              // 如果未成功获取资源,则取消当前节点获取锁的尝试
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      • 当前节点位于 CLH 队列中等待,直到其他节点释放资源,由当前节点获取并返回
    • shouldParkAfterFailedAcquire(Node pred, Node node)

      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          // 获取前驱节点状态
          int ws = pred.waitStatus;
          if (ws == Node.SIGNAL)
              /*
               * 如果前驱节点状态为 SIGNAL(-1),则表示其在释放锁的时候
               * 会去唤醒后继节点,所以此时后继节点可以阻塞自己,等待被唤醒
               */
              return true;
          if (ws > 0) {
              /*
               * 如果前驱节点状态为 CANCELLED(1),则在队列中向前遍历,
               * 直到找到第一个非 CANCELLED(1) 状态的节点,并将该节点
               * 设置为当前节点的前驱节点
               */
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              pred.next = node;
          } else {
              /*
               * 如果前驱节点状态既不为 SIGNAL(-1) 也不为 CANCELLED(1),
               * 则使用 CAS 操作将前驱节点状态设置为 SIGNAL(-1)
               */
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }
      
      • 根据前驱节点状态来判断是否要阻塞当前节点
    • parkAndCheckInterrupt()

      private final boolean parkAndCheckInterrupt() {
          // 阻塞当前线程
          LockSupport.park(this);
          // 检查当前线程是否被中断
          return Thread.interrupted();
      }
      
      • 阻塞当前线程并检查其是否被中断
    • cancelAcquire(Node node)

      private void cancelAcquire(Node node) {
          // 忽略当前节点不存在的情况
          if (node == null)
              return;
      
          // 将当前节点的线程设置为 null
          node.thread = null;
      
          // 将 pred 节点的引用指向当前节点的前驱节点
          Node pred = node.prev;
          /*
           * 如果前驱节点状态为 CANCELLED(1),则在队列中向前遍历,
           * 直到找到第一个非 CANCELLED(1) 状态的节点,并将该节点
           * 设置为当前节点的前驱节点
           */
          while (pred.waitStatus > 0)
              node.prev = pred = pred.prev;
      
          // 将 predNext 节点的引用指向 pred 节点的后继节点
          Node predNext  = pred.next;
      
          // 将当前节点状态设置为 CANCELLED(1)
          node.waitStatus = Node.CANCELLED;
      
          /*
           * 如果当前节点是尾部节点,且使用 CAS 操作将尾部节点
           * 由当前节点更新为 pred 节点,成功后,再使用 CAS 操作
           * 将 pred 节点的后继节点由 predNext 节点更新为 null
           *
           * 此时就断开了 pred 节点与其所有后继节点的联系,这些后继
           * 节点在引用链上不可达,最终会被 GC 回收掉
           */
          if (node == tail && compareAndSetTail(node, pred)) {
              compareAndSetNext(pred, predNext, null);
          /*
           * 如果当前节点不是尾部节点 (即当前节点还存在着后继节点),
           * 此时要做的事是将 pred 节点和当前节点的非 CANCELLED(1)
           * 状态的后继节点拼接起来
           */
          } else {
              int ws;
              /*
               * 如果 pred 节点不是头部节点,
               * 且 (pred 节点状态为 SIGNAL(-1);或如果 pred 节点状态
               *      不为 SIGNAL(-1),则将其设置为 SIGNAL(-1))
               * 且 pred 节点的线程不为 null
               */
              if (pred != head &&
                  ((ws = pred.waitStatus) == Node.SIGNAL ||
                   (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                  pred.thread != null) {
                  // 将 next 节点的引用当前节点的后继节点
                  Node next = node.next;
                  /*
                   * 如果 next 节点不为 null 且 next 节点状态不为 CANCELLED(1)
                   * 则使用 CAS 操作将 pred 节点的后继节点由 predNext 节点更新为 next 节点
                   */
                  if (next != null && next.waitStatus <= 0)
                      compareAndSetNext(pred, predNext, next);
              /*
               * 如果不满足上诉条件,则在这种情况下,
               * 为了保证队列的活跃性,需要去唤醒一次后继节点
               * 
               * 举例说明:
               *      如果 pred 节点是头部节点,则有可能当前已经
               *      没有线程持有锁了,也就不会有释放锁唤醒后继节点
               *      的操作,而如果不唤醒后继节点,队列就挂掉了
               */
              } else {
                  unparkSuccessor(node);
              }
      
              /*
               * 将当前节点的后继节点设置为其本身,之所以不设置为 null,
               * 是因为为了方便 AQS 中 Condition 部分的 isOnSyncQueue 方法
               *
               * isOnSyncQueue:
               *      用于判断一个原先属于条件队列的节点是否转移到了同步队列上,
               *      因为同步队列中会用到节点的 next 域,如果节点状态为 CANCELLED(1)
               *      且其 next 域也有值的话,则可以说明该节点一定位于同步队列上
               *
               * 在 GC 层面,和设置为 null 具有相同的效果
               */
              node.next = node; // help GC
          }
      }
      
      • 取消当前节点获取锁的尝试
    • unparkSuccessor(Node node)

      private void unparkSuccessor(Node node) {
          // 获取当前节点状态
          int ws = node.waitStatus;
          /*
           * 如果当前节点状态不为 CANCELLED(1) 或默认状态 (0)
           * 则使用 CAS 操作将当前节点状态设置为默认状态 (0)
           */
          if (ws < 0)
              compareAndSetWaitStatus(node, ws, 0);
      
          // 将 s 节点的引用指向当前节点的后继节点
          Node s = node.next;
          // 如果 s 节点为 null 或 s 节点状态为 CANCELLED(1)
          if (s == null || s.waitStatus > 0) {
              // 将 s 节点设置为 null
              s = null;
              /*
               * 在队列中向前遍历,直到找到第一个非 CANCELLED(1) 
               * 状态的节点并将 s 节点的引用指向该节点
               */
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0)
                      s = t;
          }
          // 如果 s 节点不为 null,则将其唤醒
          if (s != null)
              LockSupport.unpark(s.thread);
      }
      
      • 如果当前节点存在后继节点,则对其进行唤醒

  • 释放独占锁的流程

    • 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;
      }
      
      • 释放独占锁

      • 可以用于实现 Lock.unlock() 方法

    • tryRelease(int arg)

      protected boolean tryRelease(int arg) {
          throw new UnsupportedOperationException();
      }
      
      • 尝试释放独占锁,该方法没有具体实现,交由子类实现

  • 获取共享锁的流程

    • acquireShared(int arg)

      public final void acquireShared(int arg) {
          if (tryAcquireShared(arg) < 0)
              doAcquireShared(arg);
      }
      
      • 获取共享锁,忽略中断

      • 若获取成功,线程直接返回;若获取失败,线程会被阻塞,并将其以及状态信息包裹成一个 Node 节点加入到 CLH 队列中,直到其他线程释放锁为止

    • tryAcquireShared(int arg)

      protected int tryAcquireShared(int arg) {
          throw new UnsupportedOperationException();
      }
      
      • 尝试获取共享锁,该方法没有具体实现,交由子类实现
    • doAcquireShared(int arg)

      private void doAcquireShared(int arg) {
          // 创建一个共享节点 node,将其加入到 CLH 队列尾部
          final Node node = addWaiter(Node.SHARED);
          // 标记是否成功获取资源
          boolean failed = true;
          try {
              // 标记等待过程中是否被中断过
              boolean interrupted = false;
              for (;;) {
                  // 获取 node 节点的前驱节点 p
                  final Node p = node.predecessor();
                  /*
                   * 如果 p 节点为头部节点,则 node 节点会尝试
                   * 获取共享锁,如果获取成功,则进行后续流程
                   */
                  if (p == head) {
                      /*
                       * 负数表示失败
                       * 0 表示获取成功,但没有剩余可用资源
                       * 正数表示成功,且有剩余资源
                       */
                      int r = tryAcquireShared(arg);
                      if (r >= 0) {
                          // 将 node 节点设置为头部节点,并唤醒后继节点
                          setHeadAndPropagate(node, r);
                          /*
                           * 此时 CLH 队列的头部节点为 node 节点,
                           * 原先的头部节点 p 已处理完资源出队,
                           * 因此将 p 节点的 next 引用设置为 null
                           * 方便 GC 对 P 节点进行回收
                           */
                          p.next = null; // help GC
                          if (interrupted)
                              selfInterrupt();
                          failed = false;
                          return;
                      }
                  }
                  /*
                   * 如果 p 节点不为头部节点,则根据 p 节点
                   * 的状态来判断是否要阻塞 node 节点
                   */
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              // 如果未成功获取资源,则取消 node 节点获取锁的尝试
              if (failed)
                  cancelAcquire(node);
          }
      }
      
      • 在不可中断模式下获取共享锁
    • setHeadAndPropagate(Node node, int propagate)

      private void setHeadAndPropagate(Node node, int propagate) {
          // 将 h 节点的引用指向旧的头部节点
          Node h = head;
          // 将当前节点设置为新的头部节点
          setHead(node);
          /*
           * 此处 propagate 为 tryAcquireShared(int arg) 方法的返回值,是决定是否传播的依据之一
           * 
           * 如果当前计数大于 0,或旧头部节点为 null,
           * 或旧头部节点状态不为 CANCELLED(1) 或默认状态 (0),
           * 或新头部节点为 null,或新头部节点状态不为 CANCELLED(1) 或默认状态 (0)
           */
          if (propagate > 0 || h == null || h.waitStatus < 0 ||
              (h = head) == null || h.waitStatus < 0) {
              // 将 s 节点的引用指向当前节点的后继节点
              Node s = node.next;
              /*
               * 如果 s 节点为 null 或 s 节点为
               * 共享节点,则对后继节点进行唤醒传播
               */
              if (s == null || s.isShared())
                  doReleaseShared();
          }
      }
      
      • 设置当前节点为头部节点,并根据 tryAcquireShared(int acquires) 方法的返回值以及节点状态来判断是否需要唤醒后继节点
    • doReleaseShared()

      private void doReleaseShared() {
          /*
           * 如果头部节点存在后继节点,且节点状态为 SIGNAL(-1),则唤醒后继节点;
           * 如果头部节点存在后继节点,且节点状态为默认状态 (0),
           * 则为了保证唤醒操作可以正确稳定地传播下去,需要设置头部节点状态为 
           * PROPAGATE(-3),这样的话,当获取锁的线程在执行 setHeadAndPropagate(Node node, int propagate)
           * 方法时可以读取到头部节点的 PROPAGATE(-3) 状态,从而让获取锁的线程去唤醒后继节点
           */
          for (;;) {
              // 将 h 节点的引用指向头部节点
              Node h = head;
              /*
               * 如果 h 节点不为 null 且 h 节点
               * 不是尾部节点 (说明 h 节点存在后继节点)
               */
              if (h != null && h != tail) {
                  // 获取 h 节点状态
                  int ws = h.waitStatus;
                  // 如果 h 节点状态为 SIGNAL(-1)
                  if (ws == Node.SIGNAL) {
                      /* 使用 CAS 操作将 h 节点状态由 SIGNAL(-1)
                       * 更新为默认状态 (0),若操作失败则跳过本次循环
                       */
                      if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                          continue;            // loop to recheck cases
                      // 唤醒后继节点
                      unparkSuccessor(h);
                  }
                  /*
                   * 如果 h 节点状态为默认状态 (0),
                   * 则需要使用 CAS 操作来将 h 节点状态设置为 PROPAGATE(-3),
                   * 用以保证对后继节点进行唤醒传播
                   */
                  else if (ws == 0 &&
                           !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                      continue;                // loop on failed CAS
              }
              /*
               * 如果 h 节点仍然为头部节点,则结束循环 
               * 如果头部节点已改变,则重新进行循环
               */
              if (h == head)                   // loop if head changed
                  break;
          }
      }
      
      • 唤醒头部节点的后继节点或设置头部节点状态为传播状态 (PROPAGATE(-3))

      • 后继节点被唤醒后,会尝试获取共享锁,获取成功之后,又会调用 setHeadAndPropagate() 方法,将唤醒操作传播下去

      • 该方法保证了队列中处于等待状态的节点能够有办法被唤醒


  • 释放共享锁的流程

    • releaseShared(int arg)

      public final boolean releaseShared(int arg) {
          // 尝试释放共享锁
          if (tryReleaseShared(arg)) {
              // 唤醒后继节点
              doReleaseShared();
              return true;
          }
          return false;
      }
      
      • 释放共享锁
    • tryReleaseShared(int arg)

      protected boolean tryReleaseShared(int arg) {
          throw new UnsupportedOperationException();
      }
      
      • 尝试释放共享锁,该方法没有具体实现,交由子类实现

五、获取独占锁流程图

在这里插入图片描述


六、参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值