第十四章——构建自定义的同步工具

如果类库没有提供你需要的功能,那么还可以使用 Java 语言和类库提供的底层机制来构造自己的同步机制,包括内置的条件队列、显式的 Condition 对象以及 AbstractQueuedSynchronizer 框架。

14.1 状态依赖性的管理

在单线程程序中调用一个方法时,如果某个基于状态的前提条件未得到满足(例如 “连接池必须非空”),那么这个条件将永远无法成真。因此,在编写顺序程序中的类时,要使得这些类在它们的前提条件未被满足时就失败。但在并发程序中,基于状态的条件可能会由于其他线程的操作而改变:一个资源池可能在几条指令之前还是空的,但现在却变为非空的,因为另一个线程可能会返回一个元素到资源池。对于并发对象上依赖状态的方法,虽然有时候在前提条件不满足的情况下不会失败,但通常有一种更好的选择,即等待前提条件变为真。

内置的条件队列可以使线程一直阻塞,直到对象进入某个进程可以继续执行的状态,并且当被阻塞的线程可以执行时再唤醒它们。

可阻塞的状态依赖操作的形式如 程序清单 14-1 所示。

acquire lock on object state
while (precondition does not hold) {
    release lock
    wait until precondition might hold
    optionally fail if interrupted or timeout expires
    reacquire lock
}
perform action
    release lock

在生产者——消费者的设计中经常会使用像 ArrayBlockingQueue 这样的有界缓存。在有界缓存提供的 puttake 操作中都包含一个前提条件:不能从空缓存中获取元素,也不能将元素放入已满的缓存中。当前提条件未满足时,依赖状态的操作可以抛出一个异常或返回一个错误状态(使其成为调用者的一个问题),也可以保持阻塞直到对象进入正确的状态。

接下来介绍有界缓存的几种实现,其中将采用不同的方法来处理前提条件失败的问题。在每种实现中都扩展了 程序清单 14-2 中的 BaseBoundedBuffer

// 程序清单 14-2
public abstract class BaseBoundedBuffer {
    private final V[] buf;
    private int tail;
    private int head;
    private int count;
    
    protected BaseBoundedBuffer(int capacity) {
        this.buf = (V[]) new Object[capacity];
    }
    
    protected synchronized final void doPut(V v) {
        buf[tail] = v;
        if (++tail == buf.length) {
            tail = 0;
        }
        ++count;
    }
    
    protected synchronized final V doTake() {
        V v = buf[head];
        buf[head] = null;
        if (++head == buf.length) {
            head = 0;
        }
        --count;
        return v;
    }
    
    public synchronized final boolean isFull() {
        return count == buf.length;
    }
    
    public synchronized final boolean isEmpty() {
        return count == 0;
    }
}
14.1.1 示例:将前提条件的失败传递给调用者
// 程序清单 14-3
public class GrumpyBoundedBuffer extends BaseBoundedBuffer {
    public GrumpyBoundedBuffer(int capacity) {
        super(capacity);
    }
    
    public synchronized void put(V v) throws BufferFullException {
        if (isFull()) {
            throw new BufferFullException();
        }
        doPut(v);
    }
    
    public synchronized V take() throws BufferFullException {
        if (isEmpty()) {
            throw new BufferFullEmptyException();
        }
        return doTake();
    }
}

上面代码是一个简单的有界缓存实现。puttake 方法都进行了同步以确保实现对缓存状态的独占访问,因为这两个方法在访问缓存时都采用 “先检查再运行” 的逻辑策略。

尽管这种方法看起来很简单,但使用起来却并非如此。异常应该用于发生异常条件的情况中。“缓存已满” 并不是有界缓存的异常条件。在实现缓存时得到的简化并不能抵消在使用时存在的复杂性,因为调用者必须做好捕获异常的准备,并且在每次缓存操作时都需要重试①。

① 如果将状态依赖性交给调用者管理,那么将导致一些功能无法实现,例如维持 FIFO 顺序,由于迫使调用者重试,因此失去了 “谁先到达” 的信息。

// 程序清单 14-4
while (true) {
    try {
        V item = buffer.take();
        // 对于 item 执行一些操作
        break;
    } catch (BufferEmptyException e) {
        Thread.sleep(SLEEP_GRANULARITY);
    }
}

上面的休眠并不是实现重试的唯一方式。调用者还可以直接重新调用 take 方法,这种方法被称为忙等待或自旋等待。如果缓存的状态在很长一段时间内都不会发生变化,那么使用这种方法就会消耗大量的 CPU 时间。但是,调用者也可以进入休眠状态来避免消耗过多的 CPU 时间,但如果缓存的状态在刚调用完 sleep 就立即发生变化,那么将不必要地休眠一段时间。

14.1.2 示例:通过轮询与休眠来实现简单的阻塞
// 程序清单 14-5
public class SleepyBoundedBuffer extends BaseBoundedBuffer {
    public SleepyBoundedBuffer(int capacity) {
        super(capacity);
    }
    
    public void put(V v) throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isFull()) {
                    doPut(v);
                    return;
                }
            }
            Thread.sleep(SLEEP_GRANULARITY);
        }
    }
    
    public V take() throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isEmpty()) {
                    return doTake();
                }
            }
            Thread.sleep(SLEEP_GRANULARITY);
        }
    }
}

SleepyBoundedBuffer 的实现远比之前的实现复杂。但是要选择合适的休眠时间间隔,就需要在响应性与 CPU 使用率之间进行权衡。同时,SleepyBoundedBuffer 对调用者提出了一个新的需求:处理 InterruptedException

14.1.3 条件队列
// 程序清单 14-6
public class BoundedBuffer extends BaseBoundedBuffer {
    public BoundedBuffer(int capacity) {
        super(capacity);
    }

    // 这里一定要用 synchronized 来获取内置锁。如果在调用 wait() 时,该线程没有持有内置锁,那么将出错
    public synchronized void put(V v) throws InterruptedException {
        while (isFull()) {
            wait();
        }
        doPut(v);
        notifyAll();
    }

    public synchronized V take() throws InterruptedException {
        while (isEmpty()) {
            wait();
        }
        V v = doTake();
        notifyAll();
        return v;
    }
}

“条件队列” 这个名字来源于:它使得一组线程(称之为等待线程集合)能够通过某种方法来等待特定的条件变成真。传统队列的元素时一个个数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。

正如每个 Java 对象都可以作为一个锁,每个对象同样可以作为一个条件队列,并且 Object 中的 waitnotifynotifyAll 方法构成了内部条件队列的 API对象的内置锁与其内部条件队列是相关联的,要调用对象 X 中条件队列的任何一个方法,必须持有对象 X 上的锁。

Object.wait 会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并修改对象的状态。

14.2 使用条件队列

通常我们应该尽量基于 LinkedBlockingQueueLatchSemaphoreFutureTask 等类来构造程序,避免使用条件队列。

14.2.1 条件谓词

条件谓词是使某个操作成为状态依赖操作的前提条件。在有界缓存中,只有当缓存不为空时,take 方法才能执行,否则必须等待。对 take 方法来说,它的条件谓词就是 “缓存不为空”,take 方法在执行之前必须首先测试该条件谓词。

14.2.2 过早唤醒

虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但 wait 方法的返回并不一定意味着线程正在等待的条件谓词已经变成真了。
当执行控制重新进入调用 wait 的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用 notifyAll 时,条件谓词可能已经变成真,但在重新获取锁时将再次变为假。在线程被唤醒到 wait 重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的标志。或者,条件谓词从调用 wait 起根本就没有变成真(因为 notifynotifyAll 都可能会唤醒任意一个 wait)。

基于所有这些原因,每当线程从 wait 中唤醒时,都必须再次测试条件谓词,如果条件谓词不为真,那么就继续等待(或者失败)。由于线程在条件谓词不为真的情况下也可以反复地醒来,因此必须在一个循环中调用 wait,并在每次迭代中都测试条件谓词。程序清单 14-7 给出了条件等待的标准形式。

// 程序清单 14-7
void stateDependentMethod() throws InterruptedException {
    synchronized(lock) {
        while (!conditionPredicate())
            lock.wait();
        // 现在对象处于合适的状态
    }
}
14.2.3 丢失的信号

丢失的信号是指:线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词。现在,线程将等待一个已经发生过的事件。

14.2.4 通知

到目前为止,我们介绍了条件等待的前一半内容:等待。另一半内容是通知。

每当在等待一个条件时,一定要确保在条件谓词变为真时通过某种方式发出通知。

在条件队列 API 中有两个发出通知的方法,即 notifynotifyAll。无论调用哪一个,都必须持有与条件队列对象相关联的锁。在调用 notify 时,JVM 会从这个条件队列上等待的多个线程中选择一个来唤醒,而调用 notifyAll 则会唤醒所有在这个条件队列上等待的线程。由于在调用 notifynotifyAll 时必须持有条件队列对象的锁,而如果这些等待中线程此时不能重新获得锁,那么无法从 wait 返回,因此发出通知的线程应该尽快地释放锁,从而确保正在等待的线程尽可能快地解除阻塞。

我们应该尽可能的使用 notifyAll 而不是 notify 来唤醒阻塞的线程。因为 notify 会从多个阻塞的线程中选择一个来唤醒,因此完全有可能会唤醒一个不想唤醒的线程,那么应该被唤醒的线程就会错误这个信号。

只有同时满足以下两个条件时,才能用单一的 notify 而不是 notifyAll
所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从 wait 返回后将执行相同的操作。
单进单出。在条件变量上的每次通知,最多只能唤醒一个线程来执行。

BoundedBufferputtake 方法中采用的通知机制时保守的:每当一个对象放入缓存或者从缓存中移走一个对象时,就执行一次通知。我们可以对其进行优化:首先,仅当缓存从空变为非空,或者从满转为非满时,才需要释放一个线程。并且,仅当 puttake 影响到这些状态转换时,才发出通知。这也被称为 “条件通知(Conditional Notification)”。虽然 “条件通知” 可以提升性能,但却很难正确的实现(而且还会使子类的实现变得复杂),因此在使用时应该谨慎。

// 程序清单 14-8
public synchronized void put(V v) throws InterruptedException {
    while (isFull()) {
        wait();
    }
    boolean wasEmpty = isEmpty();
    doPut(v);
    if (wasEmpty) {
        notifyAll();
    }
}
14.2.5 示例:阀门类

在第 5 章的 TestHarness 中使用的 “开始阀门闭锁” 在初始化时指定的参数为 1,从而创建了一个二元闭锁:它只有两种状态,即初始状态和结束状态。闭锁能阻止线程通过开始阀门,并直到阀门被打开,此时所有的线程都可以通过该阀门。虽然闭锁机制通常都能满足需求,但在某些情况下存在一个缺陷:按照这种方式构造的阀门在打开后无法重新关闭。

通过使用条件等待,我们可以开发一个可重新关闭的 ThreadGate 类:

// 程序清单 14-9
public class ThreadGate {
    private boolean isOpen;
    private int generation;
    
    public synchronized void close() {
        isOpen = false;
    }
    
    public synchronized void open() {
        ++generation;
        isOpen = true;
        notifyAll();
    }
    
    public synchronized void await() throws InterruptedException {
        int arrivalGeneration = generation;
        while (!isOpen && arrivalGeneration == generation) {
            wait();
        }
    }
}

await 中使用的条件谓词比测试 isOpen 复杂得多。这种条件谓词是必需的,因为如果当阀门打开时有 N 个线程正在等待它,那么这些线程都应该被允许执行。然而,如果阀门在打开后又非常快速地关闭了,并且 await 方法只检查 isOpen,那么所有线程都可能无法释放。因此,在 ThreadGate 中使用了一个更复杂的条件谓词:每次阀门打开时,递增一个 “Generation” 计数器,这样即使阀门马上关闭,但是在关闭之前就进入 await 的线程依然能够通过阀门(因为它们的计数器还是上一次的)。

14.3 显式的 Condition 对象

在某些情况下,当内置锁过于灵活时,可以使用显式锁。正如 Lock 是一种广义的内置锁,Condition 也是一种广义的内置条件队列。

public interface Condition {
    void await() throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    long awaitNanos(long nanoTimeout) throws InterruptedException;
    void awaitUninterruptibly();
    boolean awaitUntil(Date deadline) throws InterruptedException;
    
    void signal();
    void signalAll();
}

内置条件队列存在一些缺陷。每个内置锁都只能有一个相关联的条件队列,因而在像 BoundedBuffer 这种类中,多个线程可能在同一个条件队列上等待不同的条件谓词。这使得无法满足在使用 notifyAll 时所有等待线程为同一类型的需求。如果想编写一个带有多个条件谓词的并发对象,或者想获得除了条件队列可见性之外的更多控制权,就可以使用显式的 LockCondition 而不是内置锁和条件队列。

一个 Condition 和一个 Lock 关联在一起,就像一个条件队列和一个内置锁相关联一样。要创建一个 Condition,可以在相关联的 Lock 上调用 Lock.newCondition 方法。正如 Lock 比内置加锁提供了更为丰富的功能,Condition 同样比内置条件队列提供了更丰富的功能:在每个锁上可存在多个等待、条件等待可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。

与内置条件队列不同的是,对于每个 Lock,可以有任意数量的 Condition 对象。

特别注意:在 Condition 对象中,与 waitnotifynotifyAll 方法对应的分别是 awaitsignalsignalAll。但是,ConditionObject 进行了扩展,因而它也包含 waitnotify 方法。一定要确保使用正确的版本——awaitsignal

下面的 程序清单 14-11 给出了有界缓存的另一种实现,即使用两个 Condition,分别为 notFullnotEmpty,用于表示 “非满” 与 “非空” 两个条件谓词。

// 程序清单 14-11
public class ConditionBoundedBuffer {
    protected final Lock lock = new ReentrantLock();
    // 条件谓词:notFull (count < items.length)
    private final Condition notFull = lock.newCondition();
    // 条件谓词:notEmpty (count > 0)
    private final Condition notEmpty = lock.newCondition();
    private final T[] items = (T[]) new Object[BUFFER_SIZE];
    private int tail, head, count;
    
    // 阻塞并直到:notFull
    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();
            }
            items[tail] = x;
            if (++tail == items.length) {
                tail = 0;
            }
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    
    // 阻塞并直到:notEmpty
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            T x = items[head];
            items[head] = null;
            if (++head == items.length) {
                head = 0;
            }
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

ConditionBoundedBuffer 的行为和 BoundedBuffer 相同,但它对条件队列的使用方式更容易理解——在分析使用多个 Condition 的类时,比分析一个使用单一内部队列加多个条件谓词的类简单得多。

在使用显式的 Condition 和内置条件队列之间进行选择时,与在 ReentrantLocksynchronized 之间进行选择是一样的:如果需要一些高级功能,例如使用公平的队列操作或者在每个锁上对应多个等待线程集,那么应该优先使用 Condition 而不是内置条件队列。

14.4 Synchronizer 剖析

ReentrantLockSemaphore 这两个接口之间存在许多共同点。这两个类都可以用做一个“阀门”,即每次只允许一定数量的线程通过,并当线程到达阀门时,可以通过(在调用 lockacquire 时成功返回),也可以等待(在调用 lockacquire 时阻塞),还可以取消(在调用 tryLocktryAcquire 时返回 “假”,表示在指定的时间内锁是不可用的或者无法获得许可)。而且,这两个接口都支持可中断的,不可中断的以及限时的获取操作,并且也都支持等待线程执行公平或非公平的队列操作。

列出这种共性后,你或许会认为 Semaphore 是基于 ReentrantLock 实现的,或者认为 ReentrantLock 实际上是带有一个许可的 Semaphore。下面我们将使用锁来实现技术信号量:

// 程序清单 14-12
public class SemaphoreOnLock {
    private final Lock lock = new ReentrantLock();
    private final Condition permitsAvailable = lock.newCondition();
    private int permits;
    
    SemaphoreOnLock(int initialPermits) {
        lock.lock();
        try {
            permits = initialPermits;
        } finally {
            lock.unlock();
        }
    }
    
    // 阻塞并直到:permitsAvailable
    public void acquire() throws InterruptedException {
        lock.lock();
        try {
            while (permits <= 0) {
                permitsAvailable.await();
            }
            --permits;
        } finally {
            lock.unlock();
        }
    }
    
    public void release() {
        lock.lock();
        try {
            ++permits;
            permitsAvailable.signal();
        } finally {
            lock.unlock();
        }
    }
}

事实上,ReentrantLockSemaphore 在实现时都是用了一个共同的基类,即 AbstractQueuedSynchronized(AQS),这个类也是其他许多同步类的基类。AQS 是一个用于构建锁和同步器的框架,许多同步器都可以通过 AQS 很容易并且高效地构造出来。不仅 ReentrantLockSemaphore 是基于 AQS 构建的,还包括 CountDownLatchReentrantReadWriteLockSynchronousQueueFutureTask

AQS 解决了在实现同步器时涉及的大量细节问题,例如等待线程采用 FIFO 队列操作顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程时应该通过还是需要等待。

基于 AQS 来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。在 SemaphoreOnLock 中,获取许可的操作可能在两个时刻阻塞——当锁保护信号量状态时(acquire 方法中的 lock.lock()),以及当许可不可用时(acquire 方法中的 permitsAvailable.await())。在基于 AQS 构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。

14.5 AbstractQueuedSynchronizer

大多数开发者都不会直接使用 AQS,标准同步器类的集合能够满足绝大多数情况的需求。

在基于 AQS 构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。当使用锁或信号量时,“获取”操作的含义就很直观,即获取的是锁或者许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。在使用 CountDownLatch 时,“获取”操作意味着“等待并直到闭锁到达结束状态”,而在使用 FutureTask 时,则意味着“等待并直到任务已经完成”。“释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。

如果一个类想成为状态依赖的类,那么它必须拥有一些状态。AQS 负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过 getStatesetState 以及 compareAndSetStateprotected 类型方法来进行操作。这个整数可以用于表示任意状态。例如,ReentrantLock 用它来表示所有者线程已经重复获取该锁的次数,Semaphore 用它来表示剩余的许可数量,FutureTask 用它来表示任务的状态(尚未开始、正在运行、已完成以及已取消)。在同步类中还可以自行管理一些额外的状态变量,例如,ReentrantLock 保存了锁的当前所有者的信息,这样就能区分某个获取操作是重入的还是竞争的。

// 程序清单 14-13
boolean acquire() throws InterruptedException {
    while (当前状态不允许获取操作) {
        if (需要阻塞获取请求) {
            如果当前线程不在队列中,则将其插入队列
            阻塞当前线程
        } else {
            返回失败
        }
    }
    可能更新同步器的状态
    如果线程位于队列中,则将其移除队列
    返回成功
}

void release() {
    更新同步器的状态
    if (新的状态允许某个被阻塞的线程获取成功) {
        解除队列中一个或多个线程的阻塞状态
    }
}

根据同步器的不同,获取操作可以是一种独占操作(例如 ReentrantLock),也可以是一个非独占操作(例如 SemaphoreCountDownLatch)。一个获取操作包含了两个部分。首先,同步器判断当前状态是否允许获得操作,如果是,则允许线程执行,否则获取操作将阻塞或失败。这种判断时由同步器的语义决定的,例如,对于锁来说,如果它没有被某个线程持有,那么就能被成功地获取,而对于闭锁来说,如果它处于结束状态,那么也能被成功地获取。

其次就是更新同步器的状态,获取同步器的某个线程可能会对其他线程能否也获取该同步器造成影响。例如,当获取一个锁后,锁的状态将从 “未被持有” 变为 “已被持有”,而从 Semaphore 中获取一个许可后,将把剩余许可的数量减 1。然而,当一个线程获取闭锁时,并不会影响其他线程能否获取它(因为闭锁能否获取,不取决于请求线程,而是取决于控制它的线程是否 countDown 完),因此获取闭锁的操作不会改变闭锁的状态。

如果某个同步器支持独占的获取操作,那么需要实现一些保护方法,包括 tryAcquiretryReleaseisHeldExclusively 等,而对于支持共享获取的同步器,则应该实现 tryAcquireSharedtryReleaseShared 等方法。AQS 中的 acquireacquireSharedreleasereleaseShared 等方法都将调用这些方法在子类中带有前缀 try 的版本来判断某个操作是否能执行。在同步器的子类中,可以根据其获取操作和释放操作的语义,使用 getStatesetState 以及 compareAndSetState 来检查和更新状态,并通过返回的状态值来告知基类 “获取” 和 “释放” 同步器的操作是否成功。例如,如果 tryAcquireShared 返回一个负值,那么表示获取操作失败,返回零值表示同步器通过独占方式被获取,返回正值则表示同步器通过非独占方式被获取。对于 tryReleasetryReleaseShared 方法来说,如果释放操作使得所有在获取同步器时被阻塞的线程恢复执行,那么这两个方法应该返回 true

一个简单的闭锁
// 程序清单 14-14
public class OneShotLatch {
    private final Sync sync = new Sync();
    
    public void signal() {
        sync.releaseShared(0);
    }
    
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(0);
    }
    
    private class Sync extends AbstractQueuedSynchronizer {
        // 将被 sync.acquireSharedInterruptibly() 调用
        @Override
        protected int tryAcquireShared(int ignored) {
            // 如果闭锁是开的(state == 1),那么这个操作将成功,否则将失败
            return (getState() == 1) ? 1 : -1;
        }

        // 将被 sync.releaseShared() 调用
        @Override
        protected boolean tryReleaseShared(int ignored) {
            setState(1);    // 现在打开闭锁
            return true;    // 现在其他的线程可以获取该闭锁
        }
    }
}

OneShotLatch 中,AQS 状态用来表示闭锁状态——关闭(0)或者打开(1)。await 方法调用 AQSacquireSharedInterruptibly,然后接着调用 OneShotLatch 中的 tryAcquireShared 方法。在 tryAcquireShared 的实现中必须返回一个值来表示该获取操作能否执行。如果之前已经打开了闭锁,那么 tryAcquireShared 将返回成功并允许线程通过,否则就会返回一个表示获取操作失败的值。acquireSharedInterruptibly 方法在处理失败的方式,是把这个线程放入等待线程队列中。类似地,signal 将调用 releaseShared,接下来又会调用 tryReleaseShared。在 tryReleaseShared 中将无条件地把闭锁的状态设置为打开,(通过返回值)表示该同步器处于完全被释放的状态。因而 AQS 让所有等待中的线程都尝试重新请求该同步器,并且由于 tryAcquireShared 将返回成功,因此现在的请求操作将成功。

OneShotLatch 是一个功能全面的、可用的、性能较好的同步器,并且仅使用了大约 20 多行代码就实现了。当然,它缺少了一些有用的特性,例如限时的请求操作以及检查闭锁的状态,但这些功能实现起来同样很容易。

OneShotLatch 也可以通过扩展 AQS 来实现,而不是将一些功能委托给 AQS(代码中将功能委托给了 sync)。但这样做将破坏 OneShotLatch 接口(只有两个方法)的简洁性,并且调用者可以很容易地错误使用 AQS 的其他方法。java.util.concurrent 中的所有同步器类都没有直接扩展 AQS,而是将它们的响应功能委托给私有的 AQS 子类来实现。

14.6 java.util.concurrent 同步器类中的 AQS

java.util.concurrent 中的许多可阻塞类,例如 ReentrantLockSemaphoreReentrantReadWriteLockCountDownLatchSynchronousQueueFutureTask 等,都是基于 AQS 构建的。下面我们快速地浏览下每个类是如何使用 AQS 的。

14.6.1 ReentrantLock

ReentrantLock 只支持独占方式的获取操作,因此它实现了 tryAcquiretryReleaseisHeldExclusively。如上所述,ReentrantLock 将获取操作委托给了继承自 AQSsync。我们来看看这个 Sync 内部类的 tryAcquire 实现:

// 程序清单 14-15
// 为了好理解,我们简化了一些代码
protected boolean tryAcquire(int ignored) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, 1)) {  // 原子地修改,防止另外的线程占有该锁
            owner = current;  // 保存占有该锁的线程
            return true;
        }
    } else if (current == owner) {
        setState(c+1);  // 将该线程的重入次数 + 1
        return true;
    }
    return false;
}
14.6.2 Semaphore 与 CountDownLatch

SemaphoreAQS 的同步状态用于保存当前可用许可的数量。Semaphore 同样是通过继承自 AQS 的内部类 Sync 来维护状态。

// 程序清单 14-16
protected int tryAcquireShared(int acquires) {
    while(true) {
        int available = getState();  // 计算剩余许可的数量
        int remaining = available - acquires;
        if (remaining < 0 /* 如果没有足够的许可,那么将返回失败 */
            || compareAndSetState(available, remaining)  /* 如果有足够的许可,那么以原子方式降低许可计数 */) {
            return remaining;
        }
    }
}

protected boolean tryReleaseShared(int releases) {
    while(true) {
        int p = getState();
        if (compareAndSetState(p, p + releases)  /* 以原子方式增加许可计数 */) {
            return true;
        }
    }
}

CountDownLatch 使用 AQS 的方式与 Semaphore 很相似:在同步状态中保存的是当前的计数值。countDown 方法调用 release,从而导致计数值递减,并且当计数值为零时,解除所有等待线程的阻塞。await 调用 acquire,当计数器为零时,acquire 将立即返回,否则将阻塞。

14.6.3 FutureTask

初看上去,FutureTask 甚至不像一个同步器,但 Future.get 的语义非常类似于闭锁的语义——如果发生了某个事件(由 FutureTask 表示的任务执行完成或被取消),那么线程就可以恢复执行,否则这些线程将停留在队列中并直到该事件发生。

FutureTask 中,AQS 同步状态被用来保存任务的状态,例如,正在运行、已完成或已取消。FutureTask 还维护一些额外的状态变量,用来保存计算结果或者抛出的异常。此外,它还维护了一个引用,指向正在执行计算任务的线程(如果它当前处于运行状态),因而如果任务取消,该线程就会中断。

但是,现在最新的 FutureTask 已经不再用 AQS 来维护状态:

FutureTask 的注释.png

14.6.4 ReentrantReadWriteLock

ReadWriteLock 接口表示存在两个锁:一个读取锁和一个写入锁,但在基于 AQS 实现的 ReentrantReadWriteLock 中,单个 AQS 子类将同时管理读取加锁和写入加锁。ReentrantReadWriteLock 使用了一个 16 位的状态来表示写入锁的计数,并且使用了另一个 16 位的状态来表示读取锁的计数。在读取锁上的操作将使用共享的获取方法与释放方法,在写入锁上的操作将使用独占的获取方法与释放方法。

AQS 在内部维护了一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。在 ReentrantReadWriteLock 中,当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会得到这个锁。如果位于队列头部的线程执行读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。

这是大部分非公平版本的实现机制。不过在实际中,其实很少需要这么严格的排序策略。如果需要更严格的排序,那么公平版本的 ReentrantReadWriteLock 将保证,并且能够确保读取线程和写入线程不会发生饥饿问题。事实上,所有锁的默认实现都是非公平版本,公平版本需要调用者显式地指定。

你可能感兴趣的:(第十四章——构建自定义的同步工具)