JUC--AQS源码分析(二)同步状态的获取与释放
1概述
上一遍文章JUC--AQS源码分析(一)CLH同步队列我们了解了CLH同步队列的结构,以及同步队列的入列和出列。那么通过这篇文章我们将了解到同步状态的获取和释放。当然这里针对同步状态的获取和释放就需要区分共享模式和独占模式。通过查看AQS的源码我们可以发现AQS中使用了模板方法模式(针对模板方法模式我们可以查看Java设计模式--模板方法模式)。
AQS提供了大量的模板方法,主要分成三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程的情况。子类通过实现AQS中的基本方法就可以来实现自己的同步语义。下面我们分成两个部分来学习AQS的源码。
2 独占式
同一时刻仅仅只有一个线程持有同步状态就是独占式。
2.1 独占式同步状态的获取
acquire(int arg)方法为AQS提供的模板方法,该方法不响应中断,也就是因为没有获取到同步状态而加入到CLH队列的线程,当线程中断的时候并不能够从CLH队列中移除。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从上面的源码我们可以看出,acquire()方法是一个final方法,从而更加证实了这是一个模板方法。这个方法里面的执行步骤如下:
(1)tryAcquire,尝试获取同步状态以独占模式。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
这个方法是一个基本方法,因此具体的实现需要子类来完成。
(2)addWaiter,添加线程进入CLH队列。
private Node addWaiter(Node mode) {
//构建对应模式的节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
//使用CAS设置该节点为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
针对这个方法的详细介绍我们可以参考JUC--AQS源码分析(一)CLH同步队列 。
(3)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; // help GC
failed = false;
return interrupted;
}
//如果线程能够被阻塞就阻塞线程并且返回线程的中断状态。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果获取同步状态失败,则取消获取
if (failed)
cancelAcquire(node);
}
}
针对上面的acquireQueued方法的源码我们可以看见,方法调用了shouldParkAfterFailedAcquire,parkAndCheckInterrupt和cancelAcquire。这几个方法的详解如下:
(3.1)shouldParkAfterFailedAcquire
//检测线程是否应该被阻塞或者设置前继节点的状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前继节点状态为SIGNAL,从而当前节点可以被安全的阻塞
return true;
if (ws > 0) {
//前级节点的状态为CANCEL(>0),表明已经被删除,所以跳过前继节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//等待状态是小于0的,因此我们需要一个通知来保证后继节点能够被唤醒。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
我们可以看见上面这个方法所要达到一个目的就是当线程被阻塞之后能够被唤醒,这个时候大家可能有一个疑虑,为什么前继节点的waitStatus为Node.SIGNAL就能保证后继节点能够被唤醒,这个就需要等会我们学习释放同步状态方法release的时候了解。
(3.2)parkAndCheckInterrupt
//阻塞线程并返回线程的中断状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个方法比较简单,就是阻塞线程,返回中断状态。
(3.2)cancelAcquire
//删除一个正在尝试获取同步状态的节点
private void cancelAcquire(Node node) {
//如果节点不存在,直接返回
if (node == null)
return;
//置空节点所有线程
node.thread = null;
//跳过已经删除的前继节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取前继节点的后继节点
Node predNext = pred.next;
//改变当前节点的状态为CANCELLED
node.waitStatus = Node.CANCELLED;
//如果node是尾节点,直接移除
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
//将前继节点和当前节点的后继节点关联起来,并且设置前继节点的等待状态为SIGNAL。否则直接唤醒后继节点。
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
上面是针对acquireQueued方法的学习,回到acquire的源码,我们可以看见如果没有获取到同步状态,并且最终线程的中断状态为true,则会调用selfInterrupt方法进行线程中断。
(4)selfInterrupt,中断线程
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
针对上面的执行流程,我们可以简单概括为下图:
2.2 独占式同步状态的获取--相应中断
和“独占式同步状态的获取”方法不同的是,这个方法能够相应县城中断。当检测到线程已经中断的时候直接抛出异常。而“独占式同步状态的获取”会一直执行下去,直到获取获取到同步状态,才根据线程中断的情况来进行中断处理。
“独占式同步状态的获取--相应中断”的对应方法源码如下:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//检测到线程已经中断,则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
通过上面的方法我们可以看出acquireInterruptibly与acquire方法的区别仅仅在于开始的时候检测了线程的状态,并进行了相应的中断处理。
那么自旋等待doAcquireInterruptibly方法和doAcquire方法相比又有什么独特之处呢?
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少了中断状态,而是直接针对中断进行抛出异常。
2.3 独占式同步状态的获取--响应超时设置
从标题命名我们可以猜出tryAcquireNanos方法可以给同步状态的获取设定一个时间,如果在时间内获取到了同步状态则返回true,如果没有获取到就返回false。下面我们来看看tryAcquireNanos方法的具体源码。
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 {
//如果超时时间小于等于0,直接返回false
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 <= 0)直接返回false
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大于1000的时候是要进行休眠的,而且修改的时间和nanosTimeout相等,这一操作的作用是什么希望读者能够告知。
总结起来doAcquireNanos方法就比doAcquireInterruptibly多了一个判断当前时间是否已经超过了超时时间的逻辑。
2.4 独占式同步状态的释放
上面我们学习了三种独占式同步状态的获取,下面我们来学习一下独占式同步状态的释放。
/**
* 模板方法
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//头节点存在,并且头节点有等待状态,则唤醒头节点的后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
从上面我们可以看出,这个同步状态的释放release方法是一个模板方法,里面的tryRelease方法是一个基本方法,是需要子类来实现的。而针对唤醒后继节点的方法unparkSuccessor,我们在下一篇文章详细讲解。
3 共享式
共享式与独占式的区别在于,共享式在同一时刻可以有多个线程获取同步状态,而独占式在同一时刻只允许有一个线程获取同步状态。列入我们常见的读写锁中读操作就相当于是一个共享式,同一时刻可以有多个线程获取同步状态进行读操作,而写操作就相当于是独占式的,同一时刻就仅仅允许一个线程获取同步状态进行写操作。
针对共享式,我们简单学习下同步状态的获取和同步状态的释放就行了,其余的方法和独占式大同小异。
3.1 共享式同步状态获取
AQS提供了acquireShared方法用于共享式获取同步状态,具体的源码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
和独占式相同,上面的方法依然是一个模板方法,基本方法tryAcquireShared需要子类去实现,当然,这个获取同步状态成功的标志就是大于0,否则就会进行自旋等待去获取同步状态。
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);
}
}
3.2 共享式同步状态释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
同样的这个方法也是一个模板方法。针对doReleaseShared方法的学习如下:
private void doReleaseShared() {
for (;;) {
//获取头节点
Node h = head;
//如果头节点不是尾节点,并且不为null
if (h != null && h != tail) {
int ws = h.waitStatus;
//等待状态为通知后继节点
if (ws == Node.SIGNAL) {
//设置SIGNAL 为 0,直到成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒后继节点
unparkSuccessor(h);
}
//等待状态为0,直到设置头节点为共享模式的可运行状态成功为止
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
上面就是针对AQS同步状态获取与释放的学习,接下来我们将继续学习AQS阻塞和唤醒线程。