CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一
组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会
开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数 量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。示例 代码如代码清单8-3所示。
代码清单8-3 CyclicBarrierTest.java
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
c.await();
} catch (Exception e) {
}
System.out.println(1);
}
}).start();
try {
c.await();
} catch (Exception e) {
}
System.out.println(2);
}
}
因为主线程和子线程的调度是由CPU决定的,两个线程都有可能先执行,所以会产生两种
输出,第一种可能输出如下。
第一种输出
1
2
第二种输出
2
1
如果把new CyclicBarrier(2)修改成new CyclicBarrier(3),则主线程和子线程会永远等待, 因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个
线程都不会继续执行。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景,如代码 清单8-4所示。
代码清单8-4 CyclicBarrierTest2.java
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest2 {
static CyclicBarrier c = new CyclicBarrier(2, new A());
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
c.await();
Thread.sleep(1000);
} catch (Exception e) {
}
System.out.println(1);
}
}).start();
try {
c.await();
} catch (Exception e) {
}
System.out.println(2);
}
static class A implements Runnable {
@Override
public void run() {
System.out.println(3);
}
}
}
因为CyclicBarrier设置了拦截线程的数量是2,所以必须等代码中的第一个线程和线程A 都执行完之后,才会继续执行主线程,然后输出2,所以代码执行后的输出如下。
3
2
1
CyclicBarrier的应用场景
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保 存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户 的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日 均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流 水,如代码清单8-5所示。
代码清单8-5 BankWaterService.java
package demo2;
import java.util.Map.Entry;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class BankWaterService implements Runnable {
/** * 创建4个屏障,处理完之后执行当前类的run方法 */
private CyclicBarrier c = new CyclicBarrier(4, this);
/** * 假设只有4个sheet,所以只启动4个线程 */
private Executor executor = Executors.newFixedThreadPool(4);
/** * 保存每个sheet计算出的银流结果 */
private ConcurrentHashMap<String, Integer> sheetBankWaterCount = new ConcurrentHashMap<String, Integer>();
private void count() {
for (int i = 0; i < 4; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
// 计算当前sheet的银流数据,计算代码省略
sheetBankWaterCount.put(Thread.currentThread().getName(), 1);
// 银流计算完成,插入一个屏障
try {
c.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void run() {
int result = 0;
// 汇总每个sheet计算出的结果
for (Entry<String, Integer> sheet : sheetBankWaterCount.entrySet()) {
result += sheet.getValue();
}
// 将结果输出
sheetBankWaterCount.put("result", result);
System.out.println(result);
}
public static void main(String[] args) {
BankWaterService bankWaterCount = new BankWaterService();
bankWaterCount.count();
}
}
使用线程池创建4个线程,分别计算每个sheet里的数据,每个sheet计算结果是1,再由 BankWaterService线程汇总4个sheet计算出的结果,输出结果如下。
4
CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重 置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数
器,并让线程重新执行一次。
CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier 阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。代码清单8-5执行完之后会 返回true,其中isBroken的使用代码如代码清单8-6所示。
代码清单8-6 CyclicBarrierTest3.java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest3 {
static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
c.await();
} catch (Exception e) {
System.out.println("-------------");
e.printStackTrace();
System.out.println("-------------");
System.out.println(c.isBroken()+"========");
}
}
});
thread.start();
thread.interrupt();
try {
c.await();
} catch (Exception e) {
e.printStackTrace();
System.out.println(c.isBroken());
}
}
}
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。
举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
2. 怎么使用 CyclicBarrier
2.1 构造方法
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
解析:
- parties 是参与线程的个数
- 第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
解析:
- 线程调用 await() 表示自己已经到达栅栏
- BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
需求
一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务
代码实现
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest3 {
static class TaskThread extends Thread {
CyclicBarrier barrier;
public TaskThread(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(getName() + " 到达栅栏 A");
barrier.await();
System.out.println(getName() + " 冲破栅栏 A");
Thread.sleep(2000);
System.out.println(getName() + " 到达栅栏 B");
barrier.await();
System.out.println(getName() + " 冲破栅栏 B");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
int threadNum = 5;
CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 完成最后任务");
}
});
for (int i = 0; i < threadNum; i++) {
new TaskThread(barrier).start();
}
}
}
Thread-0 到达栅栏 A
Thread-2 到达栅栏 A
Thread-1 到达栅栏 A
Thread-3 到达栅栏 A
Thread-4 到达栅栏 A
Thread-4 完成最后任务
Thread-4 冲破栅栏 A
Thread-0 冲破栅栏 A
Thread-2 冲破栅栏 A
Thread-1 冲破栅栏 A
Thread-3 冲破栅栏 A
Thread-1 到达栅栏 B
Thread-3 到达栅栏 B
Thread-4 到达栅栏 B
Thread-0 到达栅栏 B
Thread-2 到达栅栏 B
Thread-2 完成最后任务
Thread-2 冲破栅栏 B
Thread-4 冲破栅栏 B
Thread-0 冲破栅栏 B
Thread-3 冲破栅栏 B
Thread-1 冲破栅栏 B
从打印结果可以看出,所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。
CyclicBarrier 使用场景
可以用于多线程计算数据,最后合并计算结果的场景。
CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
在之前的介绍 CountDownLatch 的文章中,CountDown 可以实现多个线程协调,在所有指定线程完成后,主线程才执行任务。
但是,CountDownLatch 有个缺陷,这点 JDK 的文档中也说了:他只能使用一次。在有些场合,似乎有些浪费,需要不停的创建 CountDownLatch 实例,JDK 在 CountDownLatch 的文档中向我们介绍了 CyclicBarrier——循环栅栏
在 CyclicBarrier 中,有一个 “代” 的概念,因为 CyclicBarrier 是可以复用的,那么每次所有的线程通过了栅栏,就表示一代过去了,就像我们的新年一样。当所有人跨过了元旦,日历就更新了。
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
如果使用 CyclicBarrier 就知道了,CyclicBarrier 支持在所有线程通过栅栏的时候,执行一个线程的任务。
parties 属性就是线程的数量,这个数量用来控制什么时候释放打开栅栏,让所有线
程通过。
好了,CyclicBarrier 的最重要的方法就是 await 方法,当执行了这样一个方法,就像是树立了一个栅栏,将线程挡住了,只有所有的线程都到了这个栅栏上,栅栏才会打开。
看看这个方法的实现。
await 方法实现
代码加注释如下:
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()) {
// 将损坏状态设置为 true
// 并通知其他阻塞在此栅栏上的线程
breakBarrier();
throw new InterruptedException();
}
// 获取下标
int index = --count;
// 如果是 0 ,说明到头了
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 执行栅栏任务
if (command != null)
command.run();
ranAction = true;
// 更新一代,将 count 重置,将 generation 重置.
// 唤醒之前等待的线程
nextGeneration();
// 结束
return 0;
} finally {
// 如果执行栅栏任务的时候失败了,就将栅栏失效
if (!ranAction)
breakBarrier();
}
}
for (;;) {
try {
// 如果没有时间限制,则直接等待,直到被唤醒
if (!timed)
trip.await();
// 如果有时间限制,则等待指定时间
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// g == generation >> 当前代
// ! g.broken >>> 没有损坏
if (g == generation && ! g.broken) {
// 让栅栏失效
breakBarrier();
throw ie;
} else {
// 上面条件不满足,说明这个线程不是这代的.
// 就不会影响当前这代栅栏执行逻辑.所以,就打个标记就好了
Thread.currentThread().interrupt();
}
}
// 当有任何一个线程中断了,会调用 breakBarrier 方法.
// 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
if (g.broken)
throw new BrokenBarrierException();
// g != generation >>> 正常换代了
// 一切正常,返回当前线程所在栅栏的下标
// 如果 g == generation,说明还没有换代,那为什么会醒了?
// 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
// 正是因为这个原因,才需要 generation 来保证正确。
if (g != generation)
return index;
// 如果有时间限制,且时间小于等于0,销毁栅栏,并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
代码虽然长,但整体逻辑还是很简单的。总结一下该方法吧。
-
首先,每个 CyclicBarrier 都有一个 Lock,想执行 await 方法,就必须获得这把锁。所以,CyclicBarrier 在并发情况下的性能是不高的。
-
一些线程中断的判断,注意,CyclicBarrier 中,只有有一个线程中断了,其余的线程也会抛出中断异常。并且,这个 CyclicBarrier 就不能再次使用了。
-
每次线程调用一次 await 方法,表示这个线程到了栅栏这里了,那么就将计数器减一。如果计数器到 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。最后,将代更新,计数器重置,并唤醒所有之前等待在栅栏上的线程。
-
如果不是最后一个线程到达栅栏了,就使用 Condition 的 await 方法阻塞线程。如果等待过程中,线程中断了,就抛出异常。这里,注意一下,如果中断的线程的使用 CyclicBarrier 不是这代的,比如,在最后一次线程执行 signalAll 后,并且更新了这个“代”对象。在这个区间,这个线程被中断了,那么,JDK 认为任务已经完成了,就不必在乎中断了,只需要打个标记。所以,catch 里的 else 判断用于极少情况下出现的判断——任务完成,“代” 更新了,突然出现了中断。这个时候,CyclicBarrier 是不在乎的。因为任务已经完成了。
-
当有一个线程中断了,也会唤醒其他线程,那么就需要判断 broken 状态。
-
如果这个线程被其他的 CyclicBarrier 唤醒了,那么 g 肯定等于 generation,这个事件就不能 return 了,而是继续循环阻塞。反之,如果是当前 CyclicBarrier 唤醒的,就返回线程在 CyclicBarrier 的下标。完成了一次冲过栅栏的过程。
总结
从 await 方法看,CyclicBarrier 还是比较简单的,JDK 的思路就是:设置一个计数器,线程每调用一次计数器,就减一,并使用 Condition 阻塞线程。当计数器是0的时候,就唤醒所有线程,并尝试执行构造函数中的任务。由于 CyclicBarrier 是可重复执行的,所以,就需要重置计数器。
CyclicBarrier 还有一个重要的点,就是 generation 的概念,由于每一个线程可以使用多个 CyclicBarrier,每个 CyclicBarrier 又都可以唤醒线程,那么就需要用代来控制,如果代不匹配,就需要重新休眠。同时,这个代还记录了线程的中断状态,如果任何线程中断了,那么所有的线程都会抛出中断异常,并且 CyclicBarrier 不再可用了。
总而言之,CyclicBarrier 是依靠一个计数器实现的,内部有一个 count 变量,每次调用都会减一。当一次完整的栅栏活动结束后,计数器重置,这样,就可以重复利用了。
而他和 CountDownLatch 的区别在于,CountDownLatch 只能使用一次就 over 了,CyclicBarrier 能使用多次,可以说功能类似,CyclicBarrier 更强大一点。并且 CyclicBarrier 携带了一个在栅栏处可以执行的任务。更加灵活。
下面来一张图,说说 CyclicBarrier 的流程。和 CountDownLatch 类似:
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierDemo {
public static void main(String[] args) throws Exception{
final CyclicBarrier cyclicBarrier = new CyclicBarrier(2); //当凑够2个线程金进行触发
for (int i = 0; i < 3 ; i++) {
final int second = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("["+Thread.currentThread().getName()+"]-等待开始");
try {
TimeUnit.SECONDS.sleep(second);
// cyclicBarrier.await(); //等待处理
// 如果不想一直等待则可以设置超时时间,则超过了等待时间之后将会出现"TimeoutException"。
cyclicBarrier.await(6,TimeUnit.SECONDS); //等待处理
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("["+Thread.currentThread().getName()+"]-等待结束");
}
},"娱乐者-"+i).start();
}
}
}
CyclicBarrier还有一个特点是可以进行重置处理
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierResetDemo {
public static void main(String[] args) throws Exception {
final CyclicBarrier cb = new CyclicBarrier(2); // 当凑够2个线程就进行触发
for (int i = 0; i < 3; i++) {
final int second = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "]-等待开始");
try {
if (second == 2) {
cb.reset(); // 重置
System.out.println("[重置处理****]" + Thread.currentThread().getName());
} else {
TimeUnit.SECONDS.sleep(second);
cb.await(6, TimeUnit.SECONDS);// 等待处理
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("[" + Thread.currentThread().getName() + "]-等待结束");
}
}, "娱乐者-" + i).start();
}
}
}