上一篇博客主要讲了AQS的结构。
而这片博客主要针对于同步状态的获取与释放。在此之前,先看一下AQS提供的模板方法
AQS提供的模板方法基本分为3类:独占式获取与释放同步状态,共享式获取与释放同步状态和查询同步队列中的等待线程情况。
一、独占式同步状态获取与释放
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法主要完成了同步状态获取、节点的构造、加入同步队列以及在同步队列中自旋的相关操作。
1、tryAcquire:是自定义AQS实现的。该方法保证线程安全的获取同步状态,获取成功返回true,获取失败则返回false
2、addWaiter:如果tryAcquire返回false,也就是获取同步状态失败,这时候则构造同步节点(独占式)EXCLUSIVE,调用该方法将节点加入到同步队列尾部
3、acquireQueued:该方法是的同步队列中的节点以自旋方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞的线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
private Node addWaiter(Node mode) {
//传入当前线程构造同步节点
Node node = new Node(Thread.currentThread(), mode);
// 快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
//使用CAS操作确保节点能够被线程安全添加到队列尾部
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) { // Must initialize
//如果首节点为空,则构造首节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//将节点设置为尾节点后返回
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
从上述代码可以看出,通过使用compareAndSetTail方法来确保节点能够被线程安全添加,在enq方法中,AQS通过死循环来保证节点的正确添加,注意只有通过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;
}
//头结点没有获取到同步状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,当前线程在死循环中尝试获取同步状态,而只有前驱节点是头结点才能获取到同步状态。
这是为什么:
1、头结点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否为头结点
2、为了维护同步队列的FIFO原则。
节点自旋获取同步状态的行为如下图所示:
可以看出,由于非首节点线程的前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱节点是否是首节点,如果是则尝试获取同步状态。
独占式获取同步状态流程,也就是acquire方法调用流程如下图:
二、独占式响应获取同步状态
上面说的acquire方法虽然以独占方式获取同步状态,但是该方法对线程的中断并不响应,当该线程被中断时,该线程仍然在同步队列中等待着获取同步状态,为了响应中断,AQS还提供了acquireInterruptibly方法,当该线程在等待获取同步状态时被中断,会立刻响应中断并抛出中断异常。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
可以看到,程序首先判断线程是否中断,如果中断,则抛出中断异常,否则,尝试获取同步状态,如果获取同步状态成功,则调用doAcquireInterruptibly方法。
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
从上述代码可以看出,doAcquireInterruptibly方法和acquire方法仅有两点不同,一个是doAcquireInterruptibly方法声明抛出中断异常,另一个就是在中断方法处,不是设置线程的中断标志,而是抛出中断异常
三、独占式超时获取同步状态
除了上面两个方法外,AQS还提供了一个超时获取同步状态方法tryAcquireNanos,该方法不但可以响应中断,还可以在指定时间内获取同步状态,获取成功返回true,否则返回false
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
可以看出,tryAcquireNanos方法主要是调用了doAcquireNanos方法
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();
//如果超时,则返回false
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);
}
}
该线程在自旋过程中,当节点的前驱节点为头结点时尝试获取同步状态,如果获取成功,则返回true,但在获取同步状态失败时的处理方式则不同。最开始先计算出线程超时的时间点 deadline = System.nanoTime()+timeout,如果获取同步状态失败,计算剩余时间,接下来判断是否超时(timeout<=0),超时则返回false,否则重新计算超时间隔,然后使当前线程等待timeout纳秒。
如果timeout小于等于spinForTimeoutThreshold(1000纳秒),将不会使该线程进行超时等待,而是进入快速自旋的过程。原因就是在于,非常短的超时等待无法做到十分精确,如果这是在进行超时等待,相反的会让timeout的超时从整体上表现得反而不精确。因此在超时非常短的情景下,AQS会进入无条件的快速自旋。
四、独占式同步状态释放
在线程获取同步状态之后,执行完相应的逻辑代码,就会释放同步状态,其他等待线程就可以竞争获取同步状态。通过调用AQS的release方法可以释放同步状态,该方法释放了同步状态后,会唤醒其后继节点。
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(在下一篇博客会说到)来唤醒处于等待状态的线程。
五、共享式同步状态获取
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
在acquireShared方法中,AQS调用tryAcquireShared方法尝试获取同步状态。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);
}
}
可以看出,在doAcquireShared方法中,如果前驱节点是头结点才尝试获取同步状态,获取成功则退出自旋。并且此方法是不响应中断的,当然AQS也提供了响应中断acquireSharedInterruptibly和超时等待的方法tryAcquireSharedNanos。这里我就不说了。
六、共享式同步状态释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在释放同步状态后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(如Semphore),他和独占式主要区别在于tryReleaseShared方法必须确保同步状态线程安全释放,一般通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程。