AbstractQueuedSynchronizer
提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。该类被设计为大多数依赖于单个原子int
值来表示状态的同步器的有用基础。子类必须定义改变该状态的受保护方法,以及这些方法定义了该状态在获取或释放该对象方面的含义。考虑到这些,该类中的其他方法执行所有队列和阻塞机制。子类可以维护其他状态字段,但是只有使用方法getState
、setState
和compareAndSetState
对原子更新的int
值进行同步跟踪。
子类应该被定义为非公共的内部帮助类,用于实现其封闭类的同步属性。类AbstractQueuedSynchronizer
不实现任何同步接口。相反,它定义了诸如acquireInterruptibly
这样的方法,具体锁和相关同步器可以根据需要调用这些方法来实现它们的公共方法。
这个类支持默认的模式和模式。在独占模式下获取时,其他线程试图获取的操作无法成功。多个线程获取的共享模式可能(但不需要)成功。这个类不“明白”除了机械意义上的差异之外,这些差异还包括:当共享模式获取成功时,下一个等待的线程(如果存在的话)也必须确定它是否能够获取。在不同模式下等待的线程共享同一个FIFO队列。通常,实现子类只支持其中一种模式,但这两种模式都可以发挥作用,例如在ReadWriteLock
中。只支持独占模式或仅支持共享模式的子类不需要定义支持未使用模式的方法。
这个类定义了一个嵌套的ConditionObject
类,可以用作Condition
由子类实现支持独占模式的方法isHeldExclusively
报告同步是否只对当前线程持有,release
方法调用与getState
获取当前的值和acquire
方法完全释放这个对象,鉴于这个保存的状态值,最终将该对象恢复到以前获取的状态。AbstractQueuedSynchronizer
没有方法会创建这样的条件,所以如果不能满足这个约束,就不要使用它。ConditionObject
的行为当然依赖于它的同步器实现的语义。
该类为内部队列提供了检查、检测和监视方法,跟Condition
对象的方法很相似。可以使用AbstractQueuedSynchronizer
将这些同步机制按照需要导出到类中。
该类的序列化只存储底层原子整数维护状态,因此反序列化的对象具有空线程队列。需要序列化的典型子类将定义一个readObject
方法,该方法在反序列化时将其恢复到已知的初始状态。
使用
如果要使用这个类作为同步器的基础,可以使用getState
、setState
和/或compareAndSetState
检查和/或修改同步状态,根据需要重新定义以下方法:
--tryAcquire
--tryRelease
--tryAcquireShared
--tryReleaseShared
--isHeldExclusively
默认情况下,每个方法都会UnsupportedOperationException
。这些方法的实现必须是内部线程安全的,并且通常应该是短的,而不是阻塞的。定义这些方法是使用这个类的only支持的方法。所有其他方法都声明为final
,因为它们不能独立更改。
您还可能发现从AbstractOwnableSynchronizer
继承的方法对于跟踪拥有独占同步器的线程非常有用。我们鼓励您使用它们——这使得监视和诊断工具能够帮助用户确定哪些线程持有锁。
即使这个类基于内部FIFO队列,它也不会自动执行FIFO获取策略。独占同步的核心形式为:
Acquire:
while (!tryAcquire(arg)) {
//enqueue thread if it is not already queued;
//possibly block current thread;
}
Release:
if (tryRelease(arg))
//unblock the first queued thread;
(共享模式类似,但可能涉及级联信号。)
因为签入获取是在排队之前调用的,所以一个新的获取线程可能会“插入”在其他被阻塞和排队的线程之前。但是,如果需要,您可以通过内部调用一个或多个检查方法来定义tryAcquire
和/或tryAcquireShared
来禁止插入,从而提供一个"公平" FIFO获取顺序。特别是,如果hasQueuedPredecessors
(一个专门为公平同步器设计的方法)返回true
,那么大多数公平同步器可以定义tryAcquire
来返回false
。其他变化是可能的。
对于默认的barging(也称为"贪心")策略,吞吐量和可伸缩性通常是最高的。虽然不能保证这是公平的或无中断的,但允许较早排队的线程在较晚排队的线程之前重新竞争,而且每次重新竞争都有公平的机会面对传入成功的线程。而且,在获得的同时不<转动>。通常情况下,在阻塞之前,它们可以执行tryAcquire
的多次调用,其间穿插着其他计算。当独占同步只被短暂地持有时,这就提供了自旋的大部分好处,而当它不被持有时,就没有了大部分的不利因素。如果需要,您可以通过前面的调用来增强这种能力,通过“fast-path”检查来获取方法,可能会预先检查hasContended
和/或hasQueuedThreads
,只在同步器可能不存在竞争的情况下才能这样做。
这个类为同步提供了一个高效且可伸缩的基础,部分原因是它将同步器的使用范围专门化为可以依赖于int
状态、获取和释放参数以及内部FIFO等待队列的同步器。当这还不够时,您可以使用java.util.concurrent.atomic
类、自定义的java.util.Queue
类和LockSupport
阻塞支持,从较低的级别构建同步器。
使用举例
这是一个不可重入互斥锁类,它使用0表示未锁状态,1表示已锁状态。虽然不可重入锁并不严格要求记录当前的持有线程,但是这个类还是这样做了,以便更容易地监视使用情况。它还支持conditions
,并公开了一种检测方法:
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
Condition newCondition() { return new ConditionObject(); }
// 反序列化
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
// sync对象完成所有的工作。我们只需要用它转发功能就行。
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);
}
}
AbstractQueuedSynchronizer.Node
等待队列节点类。
等待队列是CLH
(Craig、Landin和Hagersten)锁队列的变体。CLH
锁通常用于自旋锁。相反,我们使用它们来阻塞同步器,但使用相同的基本策略,即在其节点的前身中保存关于线程的一些控制信息。每个节点中的status
字段跟踪线程是否应该阻塞。一个节点在其前驱节点释放时发出信号。否则,队列的每个节点都充当持有单个等待线程的特定通知样式的监视器。状态字段不控制线程是否被授予锁等。如果线程是队列中的第一个线程,它可以尝试获取。但第一个也并不能保证成功,它只给你竞争的权利。因此,当前发布的竞争者线程可能需要重新等待。
要入队一个CLH锁,您需要将其原子拼接为新尾部。 要出列,您只需设置head字段。
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
插入CLH队列只需要对“尾部”进行单个原子操作,因此存在从未排队到排队的简单原子点划分。 同样,出列只涉及更新“头部”。 但是,节点需要更多的工作来确定他们的后驱是谁,部分是为了处理由于超时和中断而可能的取消。
处理取消主要需要“prev”链接(未在原始CLH锁中使用)。 如果节点被取消,则其后驱(通常)重新链接到未取消的前驱。 有关自旋锁的类似机制的解释,请参阅Scott和Scherer的论文
http://www.cs.rochester.edu/u/scott/synchronization/
我们还使用next
链接来实现阻塞机制。 每个节点保存了自己所在线程ID,因此前驱通过遍历下一个链接来通知下一个节点以确定它是哪个线程。 后驱的确定必须避免使用新排队节点的竞争来设置其前驱的next
字段。 必要时,当节点的后驱看起来为空时,通过从原子更新的tail
向后检查来解决这个问题。 (或者,换句话说,下一个链接是一个优化,因此我们通常不需要向后扫描。)
消除为基本算法引入了一些保守性。 由于我们必须轮询消除其他节点,我们可能会忽略已消除的节点是在我们前面还是在我们后面。 这一点通过消除时始终取消停车的继承人来处理,允许他们稳定在新的前驱,除非我们能够确定一位将承担此责任的未经解除的前驱。
CLH队列需要一个虚拟标头节点才能开始。 但是我们不会在构造函数里创建它们,因为如果没有调用就会浪费资源。 相反,节点在第一次调用时被构造,并且设置头尾指针。
Conditions
等待的线程使用相同的节点,但使用不同链接。Conditions
只需要链接简单(非并发)链接队列中的节点,因为它们仅在完全持有时才被访问。 等待时,将节点插入条件队列。 根据信号,节点被转移到主队列。 状态字段的特殊值用于标记节点所在的队列。
感谢Dave Dice,Mark Moir,Victor Luchangco,Bill Scherer和Michael Scott以及JSR-166专家组成员对本课程设计的有益想法,讨论和批评。