CountDownLatch与CyclicBarrier对比
在并发编程时总会遇到一种这样的场景:等待一系列任务做完后,才能开始做某个任务。当遇到这种场景时,两个类cross our mind:CountDownLatch和CyclicBarrier。下面从使用方法和内部实现原理分别对这两个类做出介绍。
使用方法
CountDownLatch
任务
class MyThread extends Thread{
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
// 任务完成 state - 1
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
latch.countDown();
}
}
}
在完成每一个任务后,latch中的int数字做减一操作。
测试
public class CountDownLatchTest {
@Test
public void main() {
// 初始化值为3
CountDownLatch latch = new CountDownLatch(3);
// 启动3个任务
for (int i = 3; i > 0; i --) {
new MyThread(latch).start();
}
try {
// 等待三个任务完成
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + latch.getCount());
System.out.println("finished");
}
}
主线程中启动了三个子线程,然后调用了latch.await()方法。
输出结果
Thread-1
Thread-0
Thread-2
count = 0
finished
从输出结果可以看出,主线程在等待三个子线程完成任务之后才结束的。
CyclicBarrier
先完成的任务
public class NormalTask implements Runnable {
CyclicBarrier barrier;
NormalTask(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(100);
barrier.await();
System.out.println(System.currentTimeMillis() + " first step finished");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
每个任务完成需要100ms。
后完成的任务
public class FinalTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " second step finished");
}
}
后完成的任务需要10ms。
主线程
主线程启动了两个先执行的线程,将后完成的线程作为参数传入CyclicBarrier。
@Test
public void testInterruptException() throws InterruptedException {
// 主线程作为参数传入,主线程需要等待子线程完成
CyclicBarrier barrier = new CyclicBarrier(2,new FinalTask());
new Thread(new NormalTask(barrier)).start();
new Thread(new NormalTask(barrier)).start();
Thread.sleep(300);
}
运行结果
1543326017854 first step finished
1543326017854 first step finished
1543326017870 second step finished
从运行结构可以看出,先启动的任务几乎同时完成,而后完成的任务结束时间比前两个线程完成时间晚16ms,其中6ms是启动线程所花费的。主线程中sleep 300ms 是为了等待所有的线程都执行完成。也可以使用join实现相同的效果。在这里解释一下为什么不能像CountDownLatch一样用主线程作为等待线程。我刚开始也是这样做的,发现主线程一下就跑完了,根本不停。查看了源码才发现,CyclicBarrier没有park主线程。具体逻辑相见下文的原理分析。
相同点
两个类都可以实现一个任务等待其他几个任务完成后再执行。
不同点
- 任务中调用的接口不同:CountDownLatch在任务完成后调用的是countDown函数。在等待线程中调用了await方法。而CyclicBarrier只在先开始的任务中调用了await方法。后运行任务中没有涉及到任何和CyclicBarrier的信息。
- CountDownLatch 在完成所有的操作后不可重用了,但是CyclicBarrier可以在完成任务或者有线程抛出异常后调用reset方法继续使用,这应该是这个类叫做循环屏障的原因。
原理
两个类都是在初始化时,传入一个整形数字,表示需要等待几个任务完成后才能开始执行等待的任务。但是其底层实现的原理完全不同。下面对两个类的实现原理做具体介绍。
CountDownLatch
CountDownLatch park 的是主线程,是主线程和所有的子线程在竞争同一把锁。但是初始化时,他把锁默认给了子线程(将AQS中的state 置为需要等待的子线程的个数)。
Sync(int count) {
setState(count);
}
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
而主线程在调用await方法时,先检测state是否为0,如果=0 就不用park了,这时说明子线程都已完成了。如果!= 0。则park。
每个子线程在执行完任务后,将state使用cas的方式减1,并尝试取唤醒(unpark)主线程。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 通过cas将state减1 如果state = 0 则调用doReleaseShared唤醒AQS队列中的主线程
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;
}
}
上面完整的介绍了CountDownLatch的工作原理。
CyclicBarrier
为了与CountDwonLatch 对比,也为了方便描述问题,我们将先执行的任务叫做子线程,将后执行的任务叫做主线程。
CyclicBarrier 在初始化时将int值不但赋值给了state,其内部也留了一个备份,这就是CyclicBarrier可以调用reset重新使用的一个原因。而且其内部是在可重入锁ReentrantLock和Condition的基础上实现的,在其代码内部几乎看不到CAS代码,看到更多的是重入锁的lock和unlock以及Condition的await和singal。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
其中的parties就是子线程个数的备份,而barrierAction可有可无。
在子任务完成后就会调用await方法:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
其核心逻辑在dowait方法中。dowait的核心逻辑是,先上锁,而后检查异常,如果有线程抛出过异常则当前线程也抛出异常。
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
....
如果没有线程抛出异常,将count减一,并检查count是否为0 如果不为0 将当前的线程放入Condition的等待队列。如果等于0 则唤醒之前的所有线程。
int index = --count;
if (index == 0) { // 如果等于0说明所有的任务都已完成,唤醒所有Condition中的线程。
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await(); // 放入Condition队列中
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
至此,CyclicBarrier的原理页介绍完成了。
不同点
- CountDownLatch 在AQS队列中park的是主线程,而CyclicBarrier在AQS中park的是所有的子线程。
- CountDownLatch 是放到AQS队列中,而CyclicBarrier是将子线程放到Condition队列中。
- CountDownLatch 唤醒的是主线程,而CyclicBarrier 是通过singleAll函数,将所有的子线程移动到AQS队列中,然后再开始执行。
总结
通过以上分析可以得出,CountDownLatch 更适合一个任务等待一些任务执行完成后再执行,而CyclicBarrier更适合保证一批任务同时结束。