因业务中在用多线程并行执行代码块中会用到CountDownLatch来控制多线程之间任务是否完成的通知,最近突然想去看一下CountDownLatch在await及唤醒是如何实现的,便开始了阅读源码及查阅资料,然后打开了一个新大门。发现它是基于AbstractQueuedSynchronizer(下文简称AQS)框架实现的。
所以我们先了解AQS是干什么的。它提供的功能可以概括为两点:获取资源,如果获取失败加入等待队列并且休眠该线程;释放资源,同时检查是否符合唤醒等待队列中线程的条件,如符合就唤醒线程继续执行。
ReentrantLock,CountDownLatch,ReentrantReadWriteLock等都是基于这个类做的。
它提供了几个方法让子类进行复写以实现各自的功能:
tryAcquire(int arg)//尝试获取独占资源
tryRelease(int arg)//尝试释放独占资源
isHeldExclusively()//判断当前线程是否获取独占资源
tryAcquireShared(int arg)//尝试获取共享资源
tryReleaseShared(int arg)//尝试释放共享资源
这5个方法可以分为两类独占类接口(前3个)和共享类接口(后两个),因为一个子类中一般只需要(也应该如此,ReentrantReadWriteLock同时需要独占和共享,但也是分成两个类来实现的)实现其中一类方法簇,所以作者并没有把他们写成抽象方法,这样对AQS的子类更友好。
这里对AQS独占和共享概念解释一下,这是对AQS的资源(也就是state字段)的描述。即在满足可以获取资源的条件后,在队列中的等待的线程是唤醒一个(一个线程独占)还是说等待的线程都可以唤醒(共享)。
对于AQS的state字段,也就是线程抢夺的资源,不同的子类有不同的定义,标题中提到的CountDownLatch和ReentrantLock正好是对state有不同的概念,看到对这两个类的分析,大家就自然清楚了。AQS内部实现了对资源的获取,释放逻辑,让子类实现的主要是尝试获取和释放资源的场景逻辑。
下面对AQS内部逻辑进行分析,不过其子类根本不用关注这些逻辑,这些方法主要实现了:获取资源,获取失败后加入队列并且让线程阻塞,释放资源满足线程监视的条件后,从等待队列中剔除并唤醒相应的线程。,所以在看CountDownLatch,ReentrantLock代码时可以不细看这个方法的实现,先了解每个方法的作用,搞清楚子类的逻辑后,可以在慢慢研究AQS内部控制。
//AQS源码
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
获取资源的代码,首先调用tryAcquireShared 这个方法来判断是否可以获取到锁(这个方法交友子类实现其判断是否可以获取的逻辑),如果可以获取任何代码都不执行,如果不可以获取就进入if执行doAcquireShared(arg)
/**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//1
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {//2
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())//3
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
因为不能获取到资源,所有线程要加入到等待队列中并且线程进入阻塞状态。这里要说一下AQS的等待队列是由一个双向链表实现的,节点Node有前后节点,当前线程,等待状态值这几个字段组成。所以1处就是在链表中加入一个共享模式的节点。这个方法我就不写了,里面用了CAS+自旋的无锁的方式 确保在多线程下可以正确插入。
在2处判断当前节点是不是当前队列中第一个节点(head头节点就是一个空节点,head指向的下一个节点,才是等待队列中的第一个节点线程),如果是第一个,在次检测一下是否可以获取到锁,如果可以获取锁了,该线程就直接从队列里剔除,继续执行了(可能有人问之前不是尝试获取过一次吗,这里干嘛还在尝试,我认为是为了减少线程无价值的状态变更吧),如果依旧不能获取资源,进行3处,shouldParkAfterFailedAcquire这个方法是判断该线程该不该进行休眠
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {//2
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
可以看到通过LockSupport.park把线程置成阻塞状态。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {//1
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
可以看到1处是一个死循环,如果当前节点释放后,会一直便利直到队列为空或者进入不可唤醒状态
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
释放共享节点AQS从队列中剔除并且uppark该线程。
CountDownLatch的核心就是实现AQS的Sync内部类
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {//1
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {//2
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
可以看到1,2处实现了tryAcquireShared和tryReleaseShared方法,结合上面AQS源码分析应该很容易明白了。
public CountDownLatch(int count) {//CDL类构造方法
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void countDown() {
sync.releaseShared(1);
}
可以看到await方法调用的AQS的方法,该方法 if (tryAcquireShared(arg) < 0) 执行尝试调用的方法,这个由Sync实现,其实现就判断当前state是否等于0,等于说明coutnDown调用初始化的次数,不用阻塞。countDown也就会对state-1在Sync使用CAS进行了处理。
可重入锁提供了公平与非公平锁两种模式,他们的唯一区别就是在抢占锁的顺序,所以在内部实现的公平与非公平也是在lock()方法有所区别,公平模式下,有一个FIFO的队列进行排序。
这里以非公平锁为列
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
这里state为0表示当前没有线程持有该锁,>0表示有线程持有该锁,因为是可重入,所以当前线程lock一次,state就会+1,如下代码1处所示
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可重入锁使用的是AQS的独占模式,在细节上有些不同,AQS也是有两套方法,整个逻辑是一致的,不同点
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队列的第一个节点。在共享方法里是一个循环。