AQS详解

2019年03月01日

文档

依赖FIFO(先进先出)队列提供了一个阻塞锁和同步器(semaphores,events等)相关的实现框架。这个类使用一个单独的原子整形值(int)代表状态,为大多数种类的同步器设计了一个有用的基础。子类必须定义protected方法来变更这个状态,并且就索取或释放锁定义状态的意义。这个类中的其他方法会完成队列和阻塞机制。子类可以包含其他的状态域,但是只有 getState(), setState(), compareAndSetState() 获取状态并原子性的操作状态,才能跟踪到同步机制。

子类应定义为用于实现其封闭类的同步属性 的非公共内部帮助器类。 AbstractQueuedSynchronizer 类没有实现任何同步机制接口。相反,它定义了例如 acquireInterrupt 的方法,具体锁和相关同步器可以根据需要调用这些方法来实现它们的公共方法。

这个类提供了 排他锁和共享锁两种模式。当使用排他锁模式获取了锁后,其他线程尝试获取锁都会失败。共享模式则多个线程同时获取锁时可能会成功。此类不会理解这些差异,除非在机械意义上,即当共享模式获取成功时,下一个等待线程(如果存在)还必须确定它是否也可以获取,获取不同模式的所有线程都是贡献同一个FIFO队列的。通常,子类的实现智慧支持其中一种模式。但是也有同时支持两种模式的类(例如 ReadWriteLock)。子类如果只支持排他锁或者共享锁的其中一种模式的话,不用去定义其他不使用的方法。

这个类定义了一个实现 Condition 接口的嵌套类 ConditionObject,支持排他锁的子类实现可以通过 isHeldExclusively() 来报告同步器是否由当前线程独占锁的, 方法 release() 使用 getState() 获取的值完全释放这个对象,acquire使用传入的参数报错状态值,最终将其恢复到前一次获取到的状态。 AbstractQueuedSynchronizer 不能有方法创建condition,如果不能满足这个约束,就不要使用它。ConditionObject的行为依赖于同步器实现的语意。

这个类提供了对内部队列检查,操作和监控的方法,条件对象也一样提供了这些方法。可以根据需要,使用AbstractQueuedSynchronizer将它们导出到类中,以实现同步机制。

这个类的序列化存储只有用于维护状态的原子整数,所以反序列化对象会包含一个空的线程队列。通常子类如果要序列化能力会定义readObject方法在反序列化时进行初始化。

使用

这个类用来作为同步器的基础,可以适当的重新定义下面的方法,使用 getState,setState,compareAndSetState方法来检查或者修改同步器的状态。

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

这些方法默认实现是抛出 UnsupportedOperationExcetion。这些方法的实现必须是内部线程安全的,并且应尽量简短和非阻塞的。这个类只定义了这些支持的方法,其他方法都是final的,因为它们不能独立变化。

在使用独占同步器时你也可以通过继承自 AbstractOwnableSynchronizer 的方法来保持对线程拥有者的跟踪。这些方法是被鼓励使用的,这使得监视和诊断工具能够帮助用户确定哪些线程持有锁。

尽管这个类是基于内部的FIFO队列的,但是它不会自动强制执行FIFO策略。独占锁核心实现应类似如下形式:

    Acquire:
         while (!tryAcquire(arg)) {
            <em>enqueue thread if it is not already queued</em>;
            <em>possibly block current thread</em>;
         }

    Release:
         if (tryRelease(arg))
            <em>unblock the first queued thread</em>;

(共享模式类似,但可能涉及级联信号)

由于在进入队列之前要执行acquire检查。另外一个新的请求线程可能会抢在其他线程阻塞和入队列之前抢到锁。但是,如果需要,可以在tryAcquiretryAcquireShared 中通过执行一个或多个检查方法来避免抢占,提供一个 公平 的FIFO请求顺序。通常,大多数同步器可以在 hasQueuedPredecessors(设计用来支撑公平同步器)方法返回true时,在tryAcquire中返回false,当也可能有其他变化的情况。

在默认的抢占模式策略下(也称为greedy、renouncement和convoy-avoidancew),吞吐量和扩展性通常都是最高的。虽然这样无法保证公平或者无饥饿,但是早期的线程会在后面的线程之前和刚进来的线程进行公正的竞争。另外,在通常情况下,获取锁不是循环的,它们在阻塞前执行多次 tryAcquire 之间会穿插其他计算。当独占同步只被短暂地保持时,这就提供了旋转的大部分好处,而当它不被保持时,则不需要承担大部分责任。如果需要的话,您可以通过前面的调用来增强这一点,以获取具有“快速路径”检查的方法,可能会预检查hasscontendedhasqueuedthreads,仅当同步器可能不被争用时才这样做。

这个类通过依赖于整数的状态,获取和释放的参数,和一个内部的FIFO队列为同步器提供了一个高效并可扩展的基础。当这个类不能满足你的需求时,你可以使用更底层的 java.util.concurrent.atomic包中的类,自定义的 java.util.Queue类,以及 LockSupport阻塞支持 来构建你自己的同步器。

使用样例

下面是一个 不可重入的 互斥独占锁类的实现,它使用0表示未锁的状态,1表示加锁的状态。虽然不可重入锁不强行要求记录当前锁的持有线程,这个类依然记录了,这样使它更容易监控。它也支持条件对象,并暴露了一个检测方法:

      class Mutex implements Lock, java.io.Serializable {

          // 内部辅助类
          private static class Sync extends AbstractQueuedSynchronizer {
            // 返回是否是加锁的状态
            protected boolean isHeldExclusively() {
              return getState() == 1;
            }

            // 如果状态是0,则获取锁
            public boolean tryAcquire(int acquires) {
              assert acquires == 1; // Otherwise unused
              if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
              }
              return false;
            }

            // 通过设置状态为0,释放锁
            protected boolean tryRelease(int releases) {
              assert releases == 1; // Otherwise unused
              if (getState() == 0) throw new IllegalMonitorStateException();
              setExclusiveOwnerThread(null);
              setState(0);
              return true;
            }

            // 支持条件对象
            Condition newCondition() { return new ConditionObject(); }

            // 反序列化属性设置
            private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
              s.defaultReadObject();
              setState(0); // reset to unlocked state
            }
          }

            // 同步对象已经做好了最困难的部分,我们只需要做一层转发即可
              private final Sync sync = new Sync();

              public void lock()                { sync.acquire(1); }
              public boolean tryLock()          { return sync.tryAcquire(1); }
              public void unlock()              { sync.release(1); }
              public Condition newCondition()   { return sync.newCondition(); }
              public boolean isLocked()         { return sync.isHeldExclusively(); }
              public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
              public void lockInterruptibly() throws InterruptedException {
                sync.acquireInterruptibly(1);
              }
              public boolean tryLock(long timeout, TimeUnit unit)
                  throws InterruptedException {
                return sync.tryAcquireNanos(1, unit.toNanos(timeout));
              }
            }}

下面是一个类似于 java.util.concurrent.CountDownLatch 的门闩类。只是请求一个信号(signal)去继续执行。由于门闩是非独占的,所以使用shared获取和释放锁。

        class BooleanLatch {

            private static class Sync extends AbstractQueuedSynchronizer {
              boolean isSignalled() { return getState() != 0; }

              protected int tryAcquireShared(int ignore) {
                return isSignalled() ? 1 : -1;
              }

              protected boolean tryReleaseShared(int ignore) {
                setState(1);
                return true;
              }
            }

            private final Sync sync = new Sync();
            public boolean isSignalled() { return sync.isSignalled(); }
            public void signal()         { sync.releaseShared(1); }
            public void await() throws InterruptedException {
              sync.acquireSharedInterruptibly(1);
            }
          }}

等待队列类

这个等待队列是 CLH (Craig,Landin,and Hagersten) 锁队列的一个变体。CLH锁一个常用的循环锁。我使用它来阻塞同步器,但是使用相同的基本策略来保存关于其节点前一个线程的一些控制信息。每个节点的status域用来跟踪线程是否应该被阻塞。当节点的前一个节点释放的时候,当前节点会得到通知。此外,队列的每个节点都充当一个保存单个等待线程的特定通知样式监视器。status属性不会控制线程锁授权等操作。如果线程第一次进入队列,它会尝试获取锁。但是第一次并不保证会成功,只会给它一个竞争的机会。所以当前释放的竞争者线程可能会重新进入等待。

进入CLH锁队列,需要原子性操作将其作为一个新的队尾,出队列时,只需要设置head域。

*      +------+  prev +-----+       +-----+
* head |      | <---- |     | <---- |     |  tail
*      +------+       +-----+       +-----+

CLH队列的插入操作只要求在 队尾(tail) 的一个原子性操作,所以从未入队列到入队列有一个明显的原子分界点。类似的,执行出队列只需要更新队头(head)。但是,在节点上它会花费多一点工作来区分谁是成功的,部分是由于超时和中断引发的取消。

prev链(在原本的CLH锁中没有使用)是主要用来处理取消的。如果一个节点取消了,它的下一个节点需要重新设置prev为一个非取消状态的前任节点。如果想了解旋转锁的类似机制,可以查看 Scott 和 Scherer 的论文:http://www.cs.rochester.edu/u/scott/synchronization/

在实现阻塞机制时,我们也会使用 next 链。每个节点中会保存线程id,所以前任节点可以通过next链决定哪个线程需要被唤醒。继任节点需要避免和新的队列节点设置next域的竞争。当节点的后续节点看起来为空时,通过从原子更新的“tail”向后检查来解决这个问题。(或者,换句话来说,next链是一种优化,所以我们通常不必反向扫描)

取消机制给基础算法引入了一些保守性。因为我们必须轮询取消其他节点,所以我们可能会错过在我们前面或者后面取消的节点。这个的解决方案是,在取消时一致唤醒后置继承节点,允许它们稳定在一个新的前置节点,除非我们可以定义一个未取消的节点来负这个责任。