在我参阅的众多的书籍当中,都没有看到对这个类名的翻译,可能是觉得没有一个更好的中文单词从字面上描述这个类名,又或者有合适的但是正确不能表达它要表达的含义。
JDK API的书写者既然用这个类名,想其他大多数类名一样,肯定是希望达到望文知义的效果。现在我们试图从这个命名出发来描述它要表达的含义。
CountDownLatch = count down + latch
count down: 计数减小
latch: 门闩——指门关上后,插在门内使门推不开的滑动插销。
几乎所有的文章都把我这里的"计数减小"描述成"倒计时"。不过因为我们的常识,“倒计时”跟时间有关系,我们所看到的"倒计时"是自动地,可能就会觉得这个CountDownLatch类也有这种自动倒计时的功能,有这样自觉地话对我们理解和使用这个类就有影响,所以这里我不把它写成倒计时。
那么从字面上翻译就是——计数减小门闩。
这个看上去很生硬,的确是这样。
那么我们可不可以这样望文生义一下:这个CountDownLatch提供了两个功能,一个就是计数减小功能,另一个就是门闩功能。
计数减小功能:对于这个功能,相信大多数人都是很清楚如何实现的。比如说倒计时60秒,那么第一步首先规定有60秒,第二步开始不断地减秒数。
门闩功能:门闩的作用是关住门,阻止别人再进来。关注门是要做的动作,阻止别人再进来是目的。
有的人觉得还应该有一个打开门闩的功能,我们暂且猜测,这是个智能门闩,计数变为0之后门闩自动打开。
通过研究和使用CountDownLatch类,我们发现它提供的功能与我们猜测相符。
计数总数设置
CountDownLatch通过提供可接受倒计时总数作为参数的构造方法实现倒计时总数设置。这个计数总数通常都是具有某种业务含义的数字。
public CountDownLatch(int count)
计数减数
下面的方法提供计数减数的功能,每次减1。
public void countDown()
上门闩
public void await()
现在我们通过源码来逐个分析上面的功能。
计数总数设置
上面提到了是通过CountDownLatch的构造方法设置的,这里因为涉及到AQS的知识,所以我们不深入源码,只要知道这个类拥有一个表示计数的属性。
private volatile int state;
计数减数
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// 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,计数总数变为减1后的值。
上门闩
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
这个方法会检查当前的计数是否为0,如果计数不为0那么就表示上门闩成功,await()后的代码被门闩挡住,无法继续执行,当前的线程会被挂起,直到计数为0,线程被唤醒继续执行。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
打开门闩
上面我们提到"打开门闩"这个功能,而且我们猜测是自动的,看来确实是这样,因为CountDownLatch这个类没有为我们提供打开门闩这个方法。
现在我们来研究一下,什么时候会打开门闩。
在讲到"上门闩"这个功能的时候,我们提到了上门闩后线程就被挂起了,等待被唤醒。那么就看一下什么时候唤醒。
我们知道计数的减少是通过countDown这个方法来控制的,它对计数这个值是很敏感的,因为每调一次它会获取这个计数进行减1,也就是说当计数变为0的时候是它触发的,那么这个时候它很适合唤醒上门闩的线程。研究代码发现,的确如此,下面是从代码的角度对这段逻辑的分析。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// 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;
}
}
tryReleaseShared的代码中我们可以看到nextc表示计数减1后的值,如果计数减1后为0则方法会返回true。这个返回结果为ture之后,releaseShared的下面这段方法就会执行:
doReleaseShared();
这段代码我们不深究(因为涉及到AQS的知识),我们只要知道它的功能就是唤醒上门闩的那个线程。
现在我们可以这样总结了:也就是说不断调用countDown方法,等到计数总数变成0之后 ,上门闩的那个线程就被唤醒了,门闩就被打开了,就可以继续执行门闩后面的代码。
现在通过下面的场景来看看CountDownLatch的使用。
学生春游场景
场景描述:现在你们班要去春游,准备做大巴车去,只有等所有的同学都上车之后,司机才会开车出发。
老师拿了个包含50个同学名字的名单,同学来一个就划掉一个,当所有的同学都被划掉后,说明所有的同学都到了,这时候就可以出发了。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
public class SpringOuting {
public static void main(String[] args) throws Exception {
CountDownLatch cd = new CountDownLatch(50);// 学生名单
// 司机
new Thread(new Runnable() {
@Override
public void run() {
try {
cd.await();// 等待所有的学生从名单中被划掉
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("司机启动车出发....");
}
}, "司机").start();
// 学生们
Set<Thread> hashSet = new HashSet<>();
for (int i=1; i<=50; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "上车了...");
cd.countDown();// 从名单中划掉
}
}, "同学" + i);
hashSet.add(t);
}
Iterator<Thread> it = hashSet.iterator();
while (it.hasNext()) {
Thread t = it.next();
t.start();
Thread.sleep(1000);
}
}
}
这里说个题外话,是关于sleep的,写这个类的时候我想模拟同学一个一个上车的效果,当时并不是在代码最后一行这里加上sleep语句的,而是在springOuting.getOn()这个的上面加上sleep,测试发现没有效果。经过分析得到了原因:迭代这里启动线程,50个线程可以说是一瞬间启动了,虽然CPU每个时刻只有一个线程占用(假设单核),但是它切换线程足够得快,使得这50个线程几乎同一时间执行到sleep代码,这样50个线程几乎同时都休眠了,然后几乎同一时间休眠结束,所以就把sleep加到表示同学的线程的内部是不起作用的。
总结
最后通过JDK API的描述来说明这个类的功能:
CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。
应用场景
此为《Java并发编程的艺术》提到的一个场景:我们需要解析一个Excel里多个sheet的数据,此时可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。在这个需求中,要实现主线程等待所有线程完成sheet的解析操作。
实现原理
具体原理参考:Java并发编程 - 共享锁
简要说明:线程执行await判断内部令牌数是否为0,如果不为0,当前线程就会被放入同步队列中;其他线程执行完自己的后调用countDown释放1个令牌,当最后一个调用的线程通过countDown把令牌数降至为0后,执行同步队列中的线程唤醒操作唤醒线程(多个线程await,由于唤醒的传播性,则都会被唤醒)。
网友评论