AQS的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法会调用使用者重写的方法。AQS提供的模板方法基本上分为三类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况,自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。下面我们主要分析共享式同步状态的获取与释放操作。
共享式
共享式与独占式的最主要区别在于同一时刻能否有多个线程同时获取到同步状态,以文件读写为例,如果一个程序在对文件进行读操作,那么对于这个文件的写操作都被禁止,而读操作能够同时运行。写操作要求对文件独占式访问,而读操作可以使共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如下图所示:
如图所示,左半部分,是共享式访问资源时,其他共享式地访问均被允许,而独占式访问被阻塞;右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
共享式同步状态获取
通过调用同步器的acquireShared(int)方法可以共享式地获取同步状态,该方法代码如下:
/**
* 共享模式下的同步状态获取操作,忽略中断
* 首先调用至少一次的tryAcquireShared(int)方法,若同步状态获取成功,则直接返回
* 否则,线程入队,然后是阻塞和非阻塞状态之间的连续变换,直到调用tryAcquireShared(int)方法成功为止
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
若调用tryAcquireShared(int)方法获取同步状态失败,则调用doAcquireShared(int)方法:
// 共享非中断模式下获取同步状态
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 线程中断标志
boolean interrupted = false;
for (;;) {
// 获取node节点的前驱节点
final Node p = node.predecessor();
// 如果node的前驱节点是头结点
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);
}
}
doAcquireShared(int)方法与独占式的acquireQueued(Node, int)方法类似,共享式获取同步状态依靠tryAcquireShared(int arg)方法完成,该方法返回值类型为int,当返回值 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。
它和独占式的区别主要是获取同步状态成功之后的行为,共享式获取同步状态成功之后会调用setHeadAndPropagate(Node, int)方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
如果当前的同步状态(或资源数)大于0,则判断后继节点是否是共享模式,如果是共享模式,那么就会调用doReleaseShared()方法直接对后继节点进行唤醒操作,也就是说每次获取同步状态的线程都有可能通过setHeadAndPropagate(Node, int)方法来唤醒其他线程,进而修改同步状态,从而激发多个线程并发的运行。
acquireShared(int)方法是不响应中断的,但与独占式类似,同步器也提供了共享式响应中断、超时获取同步状态的方法,分别是:
acquireSharedInterruptibly(int)方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireSharedNanos(int, long)方法:
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
这两个方法与独占式相应中断、超时获取同步状态的方法类似,不再过多解释。
共享式同步状态释放
与独占式一样,共享式获取同步状态并执行了相应的逻辑之后,也需要释放同步状态,通过调用releaseShared(int)方法可以完成同步状态的释放,源码如下:
// 如果tryReleaseShared(int)方法返回true,则唤醒一个或多个线程
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在释放同步状态后,将会唤醒后续处于等待状态的节点,它和独占式的主要区别在于该方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS操作来保证的,因为释放同步状态的操作可能会同时来自多个线程。
相关博客
AbstractQueuedSynchronizer同步队列详解
AbstractQueuedSynchronizer独占式同步状态获取与释放
参考资料
方腾飞:《Java并发编程的艺术》