本篇文章介绍AbstractQueuedSynchronized,内容皆总结摘抄自《Java并发编程的艺术》和《Java并发编程实战》,仅作笔记。
AQS介绍
队列同步器AbstractQueuedSynchronizer是用来构建锁或其他同步组件的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在同步器中提供了getState()、setState()和compareAndSetState()方法来操作同步状态,这三个方法可以保证状态的改变是安全的。同步器并没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既支持独占式的获取同步状态,也支持共享式获取同步状态,这样方便实现不同类型的同步组件。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者锁需关注的领域。包括下面要介绍的重入锁ReentrantLock和读写锁ReentrantReadWriteLock都基于同步器实现。
同步器的设计基于模版方法模式,即使用者需要实现同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模版方法,这些模版方法将会调用使用者重写的方法。
API
同步器提供了以下三个方法来访问或修改同步状态:
//获取同步状态值
protected final int getState();
//设置同步状态的值
protected final void setState(int newState);
//如果当前状态值等于期望值,则将同步状态原子的设置为新值
protected final boolean compareAndSetState(int expect, int update);
除此之外,同步器还有以下常用方法:
//独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将进入同步队列等待
//该方法会调用重写的tryAcquire(int arg)方法
public final void acquire(int arg);
//与acquire(int arg)相同,但该方法响应中断,当前线程未获取到同步状态而进入同步队列
//如果当前线程被中断,则该方法会抛出IntertuptedException异常并返回
public final void acquireInterruptibly(int arg);
//共享的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待
//与独占式获取的主要区别是同一时刻可以有多个线程获取到同步状态
public final void acquireShared(int arg);
//与acquireShared(int arg)相同,该方法响应中断
public final void acquireSharedInterruptibly(int arg);
//返回一个包含可能正在等待以独占模式获取的线程的集合
public final Collection<Thread> getExclusiveQueuedThreads();
//返回队列中第一个(等待时间最长的)线程,如果为null则表示没有线程在排队
public final Thread getFirstQueuedThread();
//返回一个包含可能正在等待获取的线程集合
public final Collection<Thread> getQueuedThreads();
//返回等待获取的线程数
public final int getQueueLength();
//返回包含正在等待以共享模式获取的线程的集合
public final Collection<Thread> getSharedQueuedThreads();
//返回是否有其他线程在争取此同步器
public final boolean hasContended();
//返回是否有线程等待锁的时间比当前线程长
public final boolean hasQueuedPredecessors();
//是否有线程等待获取锁
public final boolean hasQueuedThreads();
//当前同步器是否在独占模式下被线程占用
protected boolean isHeldExclusively();
//指定线程是否在队列中
public final boolean isQueued(Thread thread);
//独占模式释放同步状态,该方法会在释放同步状态之后,将同步队列第一个节点包含的线程唤醒
public final boolean release(int arg);
//共享模式释放指定量的资源
public final boolean releaseShared(int arg);
//尝试独占模式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期
//然后进行CAS设置同步状态
protected boolean tryAcquire(int arg)
//在acquireInterruptily(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有
//获取到同步状态,将会返回false,如果获取到了则返回true
public final boolean tryAcquireNanos(int arg, long nanosTimeout);
//共享式获取同步状态,返回大于等于0的值表示获取成功,反之,获取失败
protected int tryAcquireShared(int arg);
//在acquireSharedInterruptibly(int arg)基础上增加了超时限制
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout);
//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected boolean tryRelease(int arg);
//共享式释放同步状态
protected boolean tryReleaseShared(int arg);
同步器提供的模版方法基本上分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件可以使用同步器提供的模版方法来实现自己的同步语义。
AQS实现分析
同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造为一个节点(AQS内部类Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点Node用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与描述如下表:
属性类型与名称 | 描述 |
---|---|
int waitStatus |
等待状态,包含如下状态: 1. CANCELLED:值为1,由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态不会变化。 2. SIGNAL:值为-1,后继节点的线程处于等待状态,当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使后继节点的线程得以运行。 3. CONDITION:值为-2,节点在等待队列中,节点线程等待在Conditon上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。 4. PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件的被传播下去。 5. INITIAL:值为0,初始状态。 |
Node prev | 前驱节点,当节点加入同步队列时被设置 |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继结点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,即节点类型(独占和共享)和等待队列中的后继节点共用一个字段 |
Thread thread | 获取同步状态的线程 |
节点是构成同步队列的基础,同步器拥有首节点head和尾节点tail,没有成功获取同步状态的线程将会成为节点加入该队列的尾部。同步队列的基本结构如下图:
同步器包含了两个节点类型的引用,一个指向头节点,一个指向尾节点。同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,会唤醒后继节点,而后继节点在获取同步状态成功时将自己设置为首节点。
独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,即由于线程获取同步状态失败后进图同步队列,后续对线程进行中断操作时,线程不会从同步队列中移出。该方法代码如下所示:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
以上代码首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(参数Node.EXCLUSIVE意思是创建的是独占式节点)并通过addWaiter(Node node)方法将该节点加入同步队列的尾部,最后调用acquireQueue(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒则主要依靠前驱节点的出队或阻塞线程被中断来实现。
addWaiter()方法构造节点并加入同步队列,代码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上述代码中的compareAndSetTail(Node expect,Node update)来确保节点能够被线程安全的添加,这个方法类似于这篇文章中介绍的CAS,此处不再赘述。
在enq(final Node node)方法中,同步器通过死循环来保证节点的正确添加,只有通过CAS将节点设置为尾节点后,当前线程才能从该方法返回,否则当前线程便不断地尝试。enq()方法将并发添加节点的请求通过CAS变得“串行化”了。
节点进入同步队列后,就进入自旋的过程,每个节点(线程)都当条件满足,获取到了同步状态,就从这个自旋过程中退出,否则依旧留在这个自旋过程中,并且阻塞节点的线程。自旋过程在acquireQueued()方法中,其代码如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
在此方法的循环中,首先判断当前线程的前驱节点是否是头节点,如果是头节点再去尝试获取同步状态,如果不是头节点则不用尝试去获取同步状态。如果以上两个条件都满足则将当前节点设置为头结点。
独占式同步状态获取流程图如下图:
上图中,判断前驱节点是否为头节点以及获取同步状态是否成功就是获取同步状态的自旋过程。当同步状态获取成功后,当前线程从acquire()方法返回,对于锁这种并发组件而言,代表当前线程获取了锁。
当前线程获取同步状态并执行相应逻辑后,就需要释放同步状态,使得后继节点能够继续获取同步状态。通过调用同步器的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;
}
在unparkSuccessor()方法中使用LockSupport工具来唤醒出于等待状态的线程。
在获取同步状态时,同步器维护了一个同步队列,获取状态失败的线程都会创建一个对应的节点对象加入到队列尾部,并在队列中自旋。停止自旋的条件是前驱节点是头节点且当前节点成功获取到了同步状态。在释放同步状态时,调用tryRelease()方法释放同步状态,然后唤醒头节点的后继节点。
共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,一个程序在对文件进行写操作时,这一时刻对于该文件的写操作都被阻塞,而读操作能同时进行。写操作要求对资源的独占式访问,而读操作可以共享式访问。
同步器调用acquireShared(int arg)方法可以共享的获取同步状态,该方法代码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
该方法同样调用自定义同步器实现的tryAcquireShared(int arg)方法,但tryAcquireShared()方法的返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态;当返回值小于0时,则调用doAcquireShared()方法。此方法代码如下:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此方法流程与独占式获取类似,首先调用addWaiter()方法创建节点并加入同步队列尾部,然后判断其前驱节点是否为头节点。如果是头结点,则尝试共享式获取同步状态,获取tryAcquireShared()方法返回值。由于共享式获取同步状态成功的条件是tryAcquireShared()方法返回值大于等于0,因此当前驱节点是头节点时判断tryAcquireShared()返回值如果大于等于0则表示获取同步状态成功,退出自旋过程。
共享式获取同样需要释放同步状态,调用releaseShared()方法可以释放同步状态。该方法代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在唤醒同步状态后,会唤醒后续处于等待状态的节点。对于能支持多个线程同时访问的并发组件,它和独占式主要区别在于tryReleaseShared()方法必须确保同步状态线程安全的释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。
独占式超时获取同步状态
调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则返回false。
在分析该方法的实现前,先介绍一下响应中断的同步状态获取过程。在Java 5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程仍旧会阻塞在synchronized上,等待着获取锁。在Java 5后,同步器提供了acquireInterruptibly()方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterruptedException。
超时获取同步状态的过程可以视作响应中断获取同步状态过程的增强版,doAcquireNanos()方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout -= now - lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间味道,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时。该方法代码如下:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法在自旋过程中,判断前驱节点为头节点且获取同步状态成功,则从该方法返回,这个过程和独占式同步获取类似,但在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(即判断nanosTimeout是否小于等于0),如果没有超时,则重新计算超时间隔nanosTimeout,使当前线程等待nanasTimeout纳秒。
如果nanosTimeout小于等于spinForTimeoutThreshod(1000纳秒)时,将不会使线程进行超时等待,而是进入快速的自旋过程。因为非常短的超市等待无法做到十分精确。
独占式超时获取同步态状态的流程图如下:
独占式超时获取同步状态与独占式获取同步状态在流程上非常相似,主要区别在于未获取到同步状态时的处理逻辑。独占式获取在未获取到同步状态时,将会使当前线程一直出于等待状态,而独占式超时获取会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。