上一篇我们通过ReentrantLock分析了如何AQS独占模式的实现原理。这一篇我们将根据CountDownLatch探索共享模式的实现原理,如果你认真看完上一篇,那该篇的内容将更容易理解。主要分析以下几个问题?
一、思考
- 问题一:CountDownLatch提供了怎样的功能?其实现原理是什么?AQS独占模式是怎样实现的?
- 问题二:共享锁和独占锁的区别是什么?
二、源码分析
因为CountDownLatch是借助AQS实现的,所以其必然有一个内部类继承AQS并实现tryAcquireShared(获取共享锁)和tryReleaseShared(释放共享锁)方法,这是使用AQS的唯一方式。我们去论证这一点:
在介绍CountDownLatch实现之前,我们先了解一下CountDownLatch的一般使用方式:
CountDownLatch latch=new CountDownLatch(n);
//A线程中等待
Thread a=new Thread(()->{
latch.await();
doSomeThing();
});
....可能有多个线程同时调用latch.await();
//其他线程countDown
Thread b=new Thread(()->{
doSomeThing();
latch.countDown();
});
Thread c=new Thread(()->{
doSomeThing();
latch.await();
});
......
Thread n=new Thread(()->{
doSomeThing();
latch.await();
});
接下来我们将从CountDownLathc的定义,await方法,countDown方法取剖析其实现原理,以及AQS共享模式的实现原理。
2.1CountDownLatch的定义
Method:CountDownLatch(int)
//CountDownLatch初始化
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
//详情请看下文Sync构造函数
this.sync = new Sync(count);
}
Method:Sync(int)
//在CountDownLatch初始化时调用AQS中setState方法将AQS中state值初始化为count
Sync(int count) {
setState(count);
}
2.2CountDownLatch等待(这里只分析不限时等待,后面会专门出一篇关于AQS中限时等待的博文)
Method:await()
//使该线程等待直到以下两种情况之一发生:
//1.其他线程调用countDown方法使count变为0
//2.其他线程调用Thread.interrupt方法中断该线程
public void await() throws InterruptedException {
//具体请看下文acquireSharedInterruptibly(int)方法
sync.acquireSharedInterruptibly(1);
}
Method:AbstractQueuedSynchronizer.acquireSharedInterruptibly(int)
//通过共享模式获取,如果线程被中断则停止。
//如果当前线程被中断,将抛出InterruptedException。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//因为支持可中断,所以先检查线程是否中断
if (Thread.interrupted())
throw new InterruptedException();
//先调用子类的tryAcquireShared尝试以共享模式获取,如果获取失败(该方法返回值小于0),则进
//阻塞获取
//详情请看下文tryAcquireShared和doAcquireSharedInterruptibly方法
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
Method:Sync.tryAcquireShared(int)
//尝试获取,如果stage为0,则获取成功,否则获取失败
//注意state被初始化为n,这里的语义就是当n没有减为0时,认为该线程是不可获取成功的
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
Method:doAcquireSharedInterruptibly(arg)
//以共享可中断模式进行获取
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//addWaiter方法请看【AQS分析第三篇】
//创建新的代表当前线程的Node入队
//这里与独占模式的区别在于Node.nextWaiter=Node.SHARED
final Node node = addWaiter(Node.SHARED);
//最终是否获取失败
boolean failed = true;
try {
//自旋,当被唤醒之后,再次检查之前被阻塞的原因现在是否满足
for (;;) {
final Node p = node.predecessor();
//head为当前获取成功的线程,会释放其后继线程(这里后继线程是自己,所以尝试获取)
if (p == head) {
int r = tryAcquireShared(arg);
//这里r>==0,说明state为0(已有n个线程调用了countDown方法)
if (r >= 0) {
//如果获取成功,将当前Node设置为队头节点
//这里与独占模式不同的是,独占模式只是调用setHead(请看上一篇)设
//置了队头,请看下文setHeadAndPropagate分析其除了设置队头还需要干什么
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//下面这段阻塞前处理和线程阻塞的代码同【AQS分析第三篇】,这里不再赘述
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//这里同上一篇ReentrantLock分析一样,如果tryAcquireShared方法异常,则取消等待
if (failed)
cancelAcquire(node);
}
}
Method:AbstactQueuedSynchronizer.setHeadAndPropagate(Node,int)
//在分析该方法之前我们先总结一下:
//到目前为止,当前线程尝试获取成功(tryAcquireShared方法返回值大于等于0,即state值为0),那么此时
//我们肯定要做的是设置当前线程为队头线程(这一点同独占锁,因为无论什么模式head代表获取成功的线程)
//那除了设置队头还需要做什么。
//因为可能有多个线程
//会调用latch.await方法进行等待,等待的条件是state不为0。当能走到这个方法,说明tryAcquiredShared方法返回值大于等于0,说明此时state为0,所
//以我们需要将所有在await方法上阻塞的线程全部唤醒。所以当前被唤醒的线程是有义务去唤醒其后继的所有
//线程的(传播机制)。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//下面列举了很多需要传播的情况,这里我们只看propagate>0的情况,该参数表明了是否需要传播
//大于0表示需要,在CountDownLatch中,当state为0时,tryAcquireShared返回的是1,即需要传播。
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//如果当前节点的后继节点是共享模式,唤醒当前节点的后继节点(传播),在这里我们第一次
//使用了nextWaiter属性,请看下文isShared方法(判断需不需要传播唤醒后继节点)
Node s = node.next;
if (s == null || s.isShared())
//需要唤醒则调用doReleaseShared依次唤醒后继节点
//请看下文doReleaseShared方法
doReleaseShared();
}
}
Method:isShared()
//返回true则代表该节点是需要被传播唤醒的
final boolean isShared() {
return nextWaiter == SHARED;
}
Method:doReleaseShared()
//该方法在CountDownLatch的实现中会被调用多次:
//1.当某个线程调用latch.countDown之后,state变为0,调用该方法唤醒head后继线程
//2.当某个阻塞线程从park返回之后,发现tryAcquireShared方法返回true,会调用setHeadAndPropagate
//,这个方法中会调用该方法唤醒head后继线程
//该方法将确保释放操作在队列中传播,即使有其他acquire/release操作在执行中。
private void doReleaseShared() {
//无限传播
for (;;) {
Node h = head;
//该if分支不满足情况:
//h==null或者h==tail:这代表队列中无Node或者只有一个Node节点(当前线程),则无须继
//续传播。
//当队列中阻塞线程个数>1时,则需要尝试唤醒head后继节点
if (h != null && h != tail) {
int ws = h.waitStatus;
//表明需要唤醒后继节点
if (ws == Node.SIGNAL) {
//CAS判断是否有其他操作已经唤醒head后继节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//如果确实有其他操作已经唤醒head后继节点,则再次循环传播释放
continue; // loop to recheck cases
//唤醒head后继节点
unparkSuccessor(h);
}
//ws==0表明无阻塞线程被添加到head之后
//CAS设置ws==PROPAGATE:让下一个调用acquireShared的线程可以被无条件传播,因为
//这里释放了锁,但是并无唤醒任何线程,所以下一个线程当然可以无条件传播
//CAS失败,可能已经有其它线程加入head之后,循环传播唤醒
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//当h==head,说明被唤醒的线程再次尝试获取依然失败。或者无后继节点需要唤醒,则退出循
//环。
if (h == head) // loop if head changed
break;
}
}
2.3CountDownLatch.countDown
Method:CountDownLatch.countDown()
//将count值减1,当count=0时唤醒所有在latch.latch上等待的线程
public void countDown() {
//具体请看下文AbstractQueuedSynchronizer.releaseShared(int)方法
sync.releaseShared(1);
}
Method:AbstractQueuedSynchronizer.releaseShared(int)
public final boolean releaseShared(int arg) {
//尝试以共享模式进行释放,当返回true时(count为0),唤醒素有才latch.await上等待的线程,
//请看下文tryReleaseShared方法。
if (tryReleaseShared(arg)) {
//以传播方式唤醒所有阻塞线程,请看上文对该方法的解释
doReleaseShared();
return true;
}
return false;
}
Method:Sync.tryReleaseShared()
//该方法会线程安全的对count(state)进行减一操作,当count为0之后
//返回值:true代表需要唤醒队列中再latch.await上等待的线程
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
//当state已经为0,说明已经执行过唤醒操作返回false
if (c == 0)
return false;
int nextc = c-1;
//CAS加失败重试
if (compareAndSetState(c, nextc))
//如果当前线程将count-1之后,count为0,则唤醒所有等待线程
return nextc == 0;
}
}
至此我们已经看完CountDownLatch的源码实现。接下来我们先总结一下:
CountDownLatch初始化时,会将state设置为n,所有在CountDownLatch上调用await方法的线程都将等待直到有n个线程调用countDown方法将state值减为0。当state值为0时,AQS会通过传播机制依次唤醒这些在队列中阻塞的线程。但是这个唤醒操作,不会等待当前线程执行完成再去操作,只要当前线程获取成功,就先唤醒后继节点,然后继续执行自己的方法。
三、开篇解答
- 问题一:CountDownLatch提供了怎样的功能?其实现原理是什么?
答:CountDownLatch可以让一个或一组线程等待其他一些线程进行完某些操作后执行。原理上借助AQS共享模式进行实现,详情请看上文源码分析。
- 问题二:共享模式和独占模式的区别是什么?
答:我们先分析一下CountDownLatch中共享模式体现在哪里,当调用CountDownLatch.countDown()方法时,只是CAS加失败重试的机制,这里不存在对资源的获取操作,所以不会体现共享模式。当调用CountDownLatch.await方法时,如果state不为0,那么线程将阻塞,其实这里和独占的ReentrantLock认为state不为0类似,也并体现不出共享性。但是当state为0时,CountDownLatch中所有在await方法上阻塞的线程都将获取成功,而ReentrantLock中只有一个线程可以在lock上获取成功,这里变体现了独占和共享模式的区别。总结一下如下:
独占模式:当state为0时,下一个获取成功的线程将独占state状态,其他在等待这个条件(state=0)的线程无法获取成功。
共享模式:当state为0时,所有在等待这个条件(state=0)的线程都将获取成功。