下文将从实现角度分析AQS是如何完成线程同步,主要包括:同步队列、独占式同步状态获取与释放、共享式同步状态获取与释放、超时获取同步状态等AQS的核心数据结构模板方法。
同步队列
AQS依赖同步队列(一个FIFO双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成一个节点(Node)并且将其加入到同步队列中,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的Node节点用来保存获取同步状态失败的线程引用。等待状态以及前驱和后继节点。
static final class Node {
/** 表示节点正处在共享模式下等待的标记 **/
static final Node SHARED = new Node();
/**表示节点正在以独占模式等待的标记*/
static final Node EXCLUSIVE = null;
/**waitStatus值,表示线程已取消 */
static final int CANCELLED = 1;
/** waitStatus值,表示后继线程需要取消挂起 */
static final int SIGNAL = -1;
/** waitStatus值,表示线程正在等待条件 */
static final int CONDITION = -2;
/**waitStatus值指示下一个acquireShared应无条件传播*/
static final int PROPAGATE = -3;
/**
* 状态字段,仅接受值:
*
* SIGNAL:值为-1 ,后继节点的线程处于等待状态,
* 而当前节点的线程如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。
*
* CANCELLED:值为1,由于在同步队列中等待的
* 线程等待超时或者被中断,需要从同步队列中取消等待,
* 节点进入该状态将不会变化
*
* CONDITION: 值为-2,节点在等待队列中,
* 节点线程等待在Condition上,当其他线程
* 对Condition调用了singal方法后,该节点
* 将会从等待队列中转移到同步队列中,加入到
* 对同步状态的获取中
*
* PROPAGATE: 值为-3,表示下一次共享模式同步
* 状态获取将会无条件地传播下去
*
* INITIAL: 初始状态值为0
*/
volatile int waitStatus;
/**
* 链接到前驱节点,当前节点/线程依赖它来检查waitStatus。
* 在入同步队列时被设置,并且仅在移除同步队列时才归零
* (为了GC的目的)。 此外,在取消前驱节点时,我们在找到
* 未取消的一个时进行短路,这将始终存在,因为头节点从未
* 被取消:节点仅作为成功获取的结果而变为头。
* 被取消的线程永远不会成功获取,并且线程只取消自身,
* 而不是任何其他节点。
*/
volatile Node prev;
/**
* 链接到后续节点,当前节点/线程释放时释放。
* 在入同步队列期间分配,在绕过取消的前驱节
* 点时调整,并在出同步队列时取消(为了GC的目的)。
* enq操作不会分配前驱节点的next字段,直到附加之后,
* 因此看到一个为null的next字段不一定意味着该节点在
* 队列的末尾。 但是,如果next字段显示为null,我们
* 可以从尾部扫描prev,仔细检查。 被取消的节点的next字段
* 被设置为指向节点本身而不是null,以使isOnSyncQueue更
* 方便操作。调用isOnSyncQueue时,如果节点(始终
* 是放置在条件队列上的节点)正等待在同步队列上重新获取,则返回true。
**/
volatile Node next;
/**
* 将此节点入列的线程。在构造方法里初始化,使用后清零。
* 链接到下一个节点等待条件,或特殊值SHARED。
* 因为条件队列只有在保持在独占模式时才被访问,
* 所以我们只需要一个简单的链接队列来保存节点,
* 同时等待条件。 然后将它们转移到队列中以重新获取。
* 并且因为条件只能是排它的,我们通过使用特殊的
* 值来指示共享模式来保存一个字段。
*/
Node nextWaiter;
/**
*如果节点在共享模式下等待,则返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回上一个节点,如果为null,
* 则抛出NullPointerException。当前驱节点不为null时使用。
**/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/**
*用于建立初始化head节点或SHARED标记
**/
Node() {
}
/**
*由addWaiter使用
**/
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
/**
* 供Condition使用
*/
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
Node是构成同步队列的基础,AQS拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会放入到队列的尾部
同步队列的基本结构:
同步器中包含了两个节点类型的引用,一个指向头节点(head),一个指向尾节点(tail),没有获取到锁的线程,加入到队列的过程必须保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法CompareAndSetTail(Node expect,Node update),它需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才能正式与之前的尾节点建立关联。
同步器将节点加入到同步队列的过程如图所示:
同步队列遵循FIFO,首节点是获取锁成功的节点,首节点的线程在释放锁时,将会唤醒后继节点,而后继节点将会在获取到锁时,将自己设置位首节点,过程如下所示:
设置首节点是由成功获取锁的线程来完成的,由于只有一个线程能够成功获取锁,因此设置首节点不需要CAS操作。
AQS同步状态获取与释放
1. 独占式
2. 共享式
3. 独占式超时获取
独占式同步状态的获取与释放
通过调用AQS的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)方法将节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//快速尝试在尾部添加节点
Node pred = tail;
if (pred != null) {
node.prev = pred;//如果尾部节点存在,那么新加入到队列尾部节点的前驱节点为pred
if (compareAndSetTail(pred, node)) {//通过CAS操作设置尾部节点
pred.next = node;//pred的后续节点为先加入的尾部节点
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 说明队列为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)方法来确保节点能够被线程安全的添加到同步队列的尾部。在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在死循环中,只有通过CAS将节点设置为尾节点后,当前线程才能从该方法返回,否则,当前线程不断得尝试设置。enq(final Node node)方法将并发添加节点的请求通过CAS变得串行化了。
节点进入到同步队列后,进入了一个自旋的过程,每个节点都在自省的观察,当条件满足,获取到了同步状态时,就可以从这个自旋中退出,否则继续自旋并且阻塞节点的线程
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; // help GC
failed = false;
return interrupted;
}
//检查和更新未能获取的节点的状态。
//如果线程应该阻塞,返回true。 这是主要信号
//控制所有获取循环。 需要pred == node.prev
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()
interrupted = true;
}
} finally {
if (failed)//如果需要中断,则把节点从队列中移除
cancelAcquire(node);
}
}
从以上代码可以知道,只有当线程的前驱节点是头节点才能继续获取同步状态,原因如下:
1. 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
2. 维护同步队列的FIFO原则。
独占式同步状态获取流程如下:
当前线程获取同步状态并且执行完了对应的逻辑后,需要释放同步状态,使得后续节点继续获取同步状态,通过调用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;
}
总结一下:在获取同步状态时,AQS维护这一个同步队列,获取状态失败的线程,会先创建一个独占式节点,并且加入到同步队列的尾部,同时在同步队列中进行自旋;移除队列的条件是前驱节点为头结点并且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
共享式获取同步状态
共享式获取与独占式获取的最主要区别在于同一时刻能否有多个线程同时获取到同步状态。通过调用acquireShared(int arg)方法可以共享式得获取同步状态。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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);
}
}
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,其返回值为int类型,当返回值大于0时,表示能够获取同步状态。因此,在共享式获取的自旋过程中,成功获取同步状态并且退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。共享式释放同步状态状态是通过调用releaseShared(int arg)方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法与独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态线程安全释放。一般是通过循环和CAS来保证的。因为释放同步状态的操作会同时来自多个线程。
通过截图我们可以知道,CountDownLatch、ReentrantReadWriteLock、Semaphore等都是共享式获取同步状态的。
独占式超时获取同步状态
通过调用同步器的doAcquireNanos(int arg,long nanosTimeOut)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
doAcquireNanos(int arg,long nanosTimeOut)方法针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早同时,nanosTimeout计算公式为:nanosTimeout -=now-lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,反之,则表示超时。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime();
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;
}
if (nanosTimeout <= 0)//超时
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)//spinForTimeoutThreshold=1000L
LockSupport.parkNanos(this, nanosTimeout);//
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法在自旋时,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从方法返回,这个过程和独占式获取过程相似,但是在获取失败时,有所不同。如果当前线程获取同步状态失败时,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后是当前线程等待nanosTimeout纳秒,当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker,long nanos)方法返回。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。因为,非常短的超时等待无法做到十分精确,如果这时在进行超时等待,相反会让nanosTimeout的超时从整体上表现的反而 不精确。因此,在超市分长短的场景下,同步器会进入无条件的快速自旋。
独占式超时获取同步状态的流程