Latch在英文中是门闩的意思,这个词用得非常贴切。CountDownLatch是一个倒计时。先让所有线程await。然后有线程调用countDown()方法之后,才能运行。当然CountDownLatch可以设置一个值N,只有调用N次后,await的线程才可以执行。
举个例子,所有线程完成之后,才可以执行下一步。等于说有两个用途:
一、N=1时,只有开关开启时,其他所有线程才能继续运行。
二、N>1时,只有倒计时结束时,等待的线程才能执行。
开关用法
这是的N=1的使用场景,以下是例子:
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行中");
}).start();
}
System.out.println("现在开始");
latch.countDown();
}
执行结果:
倒计时用法
这是N>1的场景,以下是代码实例:
final CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
System.out.println(getName()+"运行中");
countDownLatch.countDown();
}
}.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有线程执行完毕");
结果:
缺陷
CountDownLatch只能使用一次,这是它的缺陷。把上面的例子改写一下,让CountDownLatch重新await,但是会发现根本没效果。以下是我的测试代码:
package com.trend;
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) {
final CountDownLatch countDownLatch = new CountDownLatch(4);
startThreads(countDownLatch);
waitAll(countDownLatch);
System.out.println("所有线程执行完毕");
startThreads(countDownLatch);
waitAll(countDownLatch);
System.out.println("所有线程执行完毕");
}
private static void waitAll(CountDownLatch countDownLatch) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void startThreads(CountDownLatch countDownLatch) {
for (int i = 0; i < 4; i++) {
new Thread() {
@Override
public void run() {
System.out.println(getName()+"运行中");
countDownLatch.countDown();
}
}.start();
}
}
}
打印结果如下:
Thread-1运行中
Thread-2运行中
Thread-0运行中
Thread-3运行中
所有线程执行完毕
Thread-4运行中
所有线程执行完毕
Thread-5运行中
Thread-6运行中
Thread-7运行中
可见await没有任何效果。
原理
现在面试可恶心了哈,原理什么的,总是问一大堆,所幸CountDownLatch的原理比较简单了。
首先第一个问题,为什么会wait,wait在什么地方?源码是这样的:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
展开代码是这样:
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
等待的原因在于doAcquireSharedNanos中:
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
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);
}
}
这个for (;;)无限循环造成了线程的等待,这种策略也称为自旋。它一直在自旋等待,所以是没有切换线程状态的,我们把上述倒计时用法的代码,在countDown处打个断点就知道了:
这时候断住了,但是再看看主线程的状态:
主线程的状态是RUNNING,而不是WAITING,这就是自旋的证明。自旋的好处是免去了线程切换状态的开销,但是缺点是如果自旋次数太多,就很浪费CPU资源。