文章目录
1.CyclicBarrier概述
和CountDownLatch
类似,CyclicBarrier
也可以看做是一个阻塞程序运行的计数器,我们可以定义一个计数器值,然后需要阻塞的线程可以通过await()
方法来等待阻塞,直到调用await()
方法的线程达到了我们定义的计数器值后,唤醒所有正在等待的线程。
1.1.CyclicBarrier和CountDownLatch的区别
比较维度 | CountDownLatch | CyclicBarrier |
---|---|---|
使用方式 | 定义了等待和计数器减1的方法,两个方法分开使用,更适合于一个线程等待多个线程的场景。 | 定义了一个等待方法,这个方法组合了阻塞和计数器减1的功能,更适用于多个线程互相等待的场景。 |
是否复用 | 不可以复用,使用一次之后需要重新创建CountDownLatch 实例。 | 可以复用,CyclicBarrier 在创建时保存了一个计数器的常量,与一个计数器的变量,每次递减的是这个变量,在使用完后会将常量重新赋值给计数器变量,实现复用。 |
实现方式 | 使用AQS共享锁,调用await() 的线程会在共享锁队列中阻塞,直到其它线程调用countDown() 将计数器减为0时唤醒。 | 使用ReentrantLock 和Condition 实现,线程调用await() 方法会在临界区中对计数器做减操作,如果没有减到0,则将自己挂起在Condition 队列中。最后一个进入的线程如果将计数器减到0,就调用signalAll() 方法唤醒所有挂起的线程。 |
注:在网上查阅了部分资料都提到了CountDownLatch
是做的减计数,而CyclicBarrier
做的是加计数,不知道这个说法的依据来自哪里。从JDK8的源码中看,CyclicBarrier
依然是做的减计数。
1.2.CyclicBarrier的使用
打开CyclicBarrier
的使用API,可以看到它有两个构造方法,其中一个比较好理解,就是传入一个计数器值,用来配置可以阻塞多少个线程的。另外一个除了计数器值外,还可以传入一个Runnable
的实例对象,这个对象可以在计数器值减到0后,发起一次调用。
例如:下面代码就会在计数器减到0后,打印出"回环屏障退出"。
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("回环屏障退出"));
至于CyclicBarrier
阻塞线程的使用方法也非常简单,线程调用一个await()
方法就会将计数器减1,然后把自己阻塞。
下面的代码中,main
线程就会和新创建的4个子线程互相等待,直到计数器减到0。
public static void test(CyclicBarrier cyclicBarrier) throws BrokenBarrierException, InterruptedException {
for (int i = 0; i < 4; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":开始等待");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + ":结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "线程" + i).start();
}
System.out.println(Thread.currentThread().getName() + ":开始等待");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + ":结束等待");
}
调用test()
方法的打印结果如下:
线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程3:结束等待
线程1:结束等待
线程0:结束等待
main:结束等待
线程2:结束等待
可以看到,只有在5个线程都进入等待之后,才会退出回环屏障。
除此之外,CyclicBarrier
是可以复用的,我们可以尝试一下直接调用两次,如下:
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("回环屏障退出"));
System.out.println("-----等一次-----");
test(cyclicBarrier);
System.out.println("-----等二次-----");
test(cyclicBarrier);
}
打印结果可以证明CyclicBarrier
确实是可以复用的。
-----等一次-----
线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程1:结束等待
线程3:结束等待
main:结束等待
线程0:结束等待
-----等二次-----
线程2:结束等待
线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程3:结束等待
线程1:结束等待
线程0:结束等待
main:结束等待
线程2:结束等待
2.CyclicBarrier实现原理
CyclicBarrier
的阻塞和唤醒是通过ReentrantLock
和Condition
来实现,如果想详细的看一下两者的实现原理,可以跳转到《(十四)Java可重入互斥锁实现——ReentrantLock详解》和《(十五)Condition的使用及其阻塞唤醒原理》,本篇只对CyclicBarrier
做分析。
2.1.CyclicBarrier的实例化
这里直接看构造方法,第一种只传入计数器数值。
public CyclicBarrier(int parties) {
this(parties, null);
}
可以看到它调用的是一个重载的构造方法,代码如下:
public CyclicBarrier(int parties, Runnable barrierAction) {
// 显然,计数器值不能小于1
if (parties <= 0) throw new IllegalArgumentException();
// parties是一个常量,用在复用回环屏障时重置计数器值。
this.parties = parties;
// 当前的计数器值,线程的阻塞就是依据count值来判断的,每进入一个线程递减1
this.count = parties;
// 定义一个Runnable对象,当计数器归零时,会调用其run()方法
this.barrierCommand = barrierAction;
}
有了实例化对象之后,就可以执行它的阻塞方法的了。
2.2.CyclicBarrier阻塞的实现
CyclicBarrier
阻塞的实现本质是在调用Condition
中的await()
方法,这个方法可以选择是否传入等待时间,所以在CyclicBarrier
中的await()
方法,也可以选择是否传入等待时间,如下两个方法。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
上面代码中调用的dowait()
就是回环屏障的核心实现代码了,这个方法比较长,所以下面的源码中省略一部分代码,只保留最重要的部分。
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 回环屏障使用完毕或重置后,都会生成一个新的generation,这个对象可以用来让线程退出回环屏障
final Generation g = generation;
// 每个进入的线程,都使计数器减1,当计数器归零后进入下面的if判断
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 如果实例化时传入了Runnable对象,则在这里调用它的run()方法
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 里面做了唤醒所有等待线程的操作,线程是在下面的自旋中挂起的
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
for (;;) {
// 此处省略的线程被interrupt的try catch
// 根据是否传入等待时间来判断调用哪一个方法
if (!timed) {
// condition的await()方法,这里会暂时释放锁
trip.await();
} else if (nanos > 0L) {
nanos = trip.awaitNanos(nanos);
}
// 计数器归零后,线程退出自旋
if (g != generation) {
return index;
}
}
} finally {
lock.unlock();
}
}
上面代码中的trip
就是一个Condition对象,是CyclicBarrier
的一个成员变量,如下:
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
总结一下doWait()
方法,其实做的事情还是比较简单的。
- 线程进入
doWait()
, 先抢占到锁lock锁对象,并执行计数器递减1的操作。 - 递减后的计数器值不为0,则将自己挂起在
Condition
队列中。 - 递减后的计数器值为0,则调用
signalAll()
唤醒所有在条件队列中的线程,并创建新的generation
对象,让线程可以退出回环屏障。
线程被唤醒后,是通过下面的判断来退出自旋的:
if (g != generation) {
return index;
}
关键点就在于这个generation
对象是否已经被修改了,这个对象的修改是发生在nextGeneration()
中的。
private void nextGeneration() {
// 唤醒所有等待的线程
trip.signalAll();
// 重置计数器
count = parties;
// 创建新的generation对象,可以使上一轮的线程退出屏障,同时可以发起新一轮的屏障操作
generation = new Generation();
}
2.3.小结
CyclicBarrier
就是通过ReentrantLock
和Condition
来实现阻塞和唤醒。- 通过在自己的成员变量中定义一个保存初始计数器值的常量,来重置当前的计数器值,并配合
generation
对象来重复使用屏障。