上篇 CyclicBarrier多任务协同的利器 我们借助部门TB的例子,一步步分析了 CyclicBarrier 多线程协调的功能。
CyclicBarrier 功能强大的同时,意味着提供了更多的API,并且在使用过程中,可能有一些注意点。
今天就来聊聊 BrokenBarrierException,从名字就能看出,是“屏障被破坏异常”,屏障被破坏时,CyclicBarrier 的期望功能就不能完成,甚至导致程序异常;
BrokenBarrierException 可谓是 CyclicBarrier 的克星。
上篇的例子,我们仅仅使用了 CyclicBarrier 最基本的API
public CyclicBarrier(int parties);
CyclicBarrier(int parties, Runnable barrierAction);
public int await();
实际还有:
int getParties():获取CyclicBarrier打开屏障的线程数量,也成为方数
int getNumberWaiting():获取正在CyclicBarrier上等待的线程数量
int await(timeout,TimeUnit):带限时的阻塞等待
boolean isBroken():获取是否破损标志位broken的值
void reset():使得CyclicBarrier回归初始状态
我们重点介绍一下,能够导致 BrokenBarrierException 的操作,然后给出详细示例:
首先是 await() 和 await(timeout,TimeUnit)带时限的阻塞等待
/**
* Waits until all {@linkplain #getParties parties} have invoked
* {@code await} on this barrier.
*
* If the current thread is not the last to arrive then it is
* disabled for thread scheduling purposes and lies dormant until
* one of the following things happens:
*
* - The last thread arrives; or
*
- Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
- Some other thread {@linkplain Thread#interrupt interrupts}
* one of the other waiting threads; or
*
- Some other thread times out while waiting for barrier; or
*
- Some other thread invokes {@link #reset} on this barrier.
*
*
* If the current thread:
*
* - has its interrupted status set on entry to this method; or
*
- is {@linkplain Thread#interrupt interrupted} while waiting
*
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
*
* If the barrier is {@link #reset} while any thread is waiting,
* or if the barrier {@linkplain #isBroken is broken} when
* {@code await} is invoked, or while any thread is waiting, then
* {@link BrokenBarrierException} is thrown.
*
*
If any thread is {@linkplain Thread#interrupt interrupted} while waiting,
* then all other waiting threads will throw
* {@link BrokenBarrierException} and the barrier is placed in the broken
* state.
*
*
If the current thread is the last thread to arrive, and a
* non-null barrier action was supplied in the constructor, then the
* current thread runs the action before allowing the other threads to
* continue.
* If an exception occurs during the barrier action then that exception
* will be propagated in the current thread and the barrier is placed in
* the broken state.
*
* @return the arrival index of the current thread, where index
* {@code getParties() - 1} indicates the first
* to arrive and zero indicates the last to arrive
* @throws InterruptedException if the current thread was interrupted
* while waiting
* @throws BrokenBarrierException if another thread was
* interrupted or timed out while the current thread was
* waiting, or the barrier was reset, or the barrier was
* broken when {@code await} was called, or the barrier
* action (if present) failed due to an exception
*/
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
await() 源码注释,描述了方法功能:调用该方法的线程进入等待,在CyclicBarrier上进行阻塞等待,直到发生以下情形之一:
- 在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。—— 正常情形
- 当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
- 其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
除了第一种属于正常的情形,其他的都会导致 BrokenBarrierException。
带时限的await() 会抛出 TimeoutException;
public int await(long timeout, TimeUnit unit) throws InterruptedException,
BrokenBarrierException,
TimeoutException
当前线程等待超时,则抛出TimeoutException异常,并停止等待,继续执行。
当前线程抛出 TimeoutException 异常时,其他线程会抛出 BrokenBarrierException 异常。
await() 和 await(timeout,TimeUnit)带时限的阻塞等待,总共会有4种情形,产生 BrokenBarrierException;
下面我们一一来看。
Barrier被破坏的4中情形
为了加深大家对 CyclicBarrier 使用场景的熟悉,我们在复现产生 BrokenBarrierException 的4种情形时,使用运动员比赛的例子:
1.如果有线程已经处于等待状态,调用reset方法会导致已经在等待的线程出现BrokenBarrierException异常。并且由于出现了BrokenBarrierException,将会导致始终无法等待。
比如,五个运动员,其中一个在等待发令枪的过程中错误地接收到裁判传过来的指令,导致这个运动员以为今天比赛取消就离开了赛场。但是其他运动员都领会的裁判正确的指令,剩余的运动员在起跑线上无限地等待下去,并且裁判看到运动员没有到齐,也不会打发令枪。
class MyThread extends Thread {
private CyclicBarrier cyclicBarrier;
private String name;
private int ID;
public MyThread(CyclicBarrier cyclicBarrier, String name,int ID) {
super();
this.cyclicBarrier = cyclicBarrier;
this.name = name;
this.ID=ID;
}
@Override
public void run() {
System.out.println(name + "开始准备");
try {
Thread.sleep(ID*1000); //不同运动员准备时间不一样,方便模拟不同情况
System.out.println(name + "准备完毕!在起跑线等待发令枪");
try {
cyclicBarrier.await();
System.out.println(name + "跑完了路程!");
} catch (BrokenBarrierException e) {
e.printStackTrace();
System.out.println(name+"看不见起跑线了");
}
System.out.println(name+"退场!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("发令枪响了,跑!");
}
});
for (int i = 0; i < 5; i++) {
new MyThread(barrier, "运动员" + i + "号", i).start();
}
Thread.sleep(1000);
barrier.reset();
}
}
当发生 BrokenBarrierException 时,CyclicBarrier的保障被破坏,不能完成原功能;对应比赛场景,相当于运动员退场了。
运行结果:
运动员0号开始准备
运动员2号开始准备
运动员3号开始准备
运动员0号准备完毕!在起跑线等待发令枪
运动员1号开始准备
运动员4号开始准备
运动员1号准备完毕!在起跑线等待发令枪
运动员0号看不见起跑线了
运动员0号退场!
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:31)
运动员2号准备完毕!在起跑线等待发令枪
运动员3号准备完毕!在起跑线等待发令枪
运动员4号准备完毕!在起跑线等待发令枪
从输出可以看到,运动员0号在等待的过程中,主线程调用了reset方法,导致抛出BrokenBarrierException异常。但是其他线程并没有受到影响,它们会一直等待下去,从而一直被阻塞。
此时程序一直没停。
这种场景下,因为有参与者提前离开,导致剩余参与者永久等待。
2.如果在等待的过程中,线程被中断,也会抛出BrokenBarrierException异常,并且这个异常会传播到其他所有的线程。
public class Test {
static Map threads=new HashMap<>();
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("发令枪响了,跑!");
}
});
for (int i = 0; i < 5; i++) {
MyThread t = new MyThread(barrier, "运动员" + i + "号", i);
threads.put(i, t);
t.start();
}
Thread.sleep(3000);
threads.get(1).interrupt();
}
}
运行结果:
运动员0号开始准备
运动员2号开始准备
运动员3号开始准备
运动员1号开始准备
运动员0号准备完毕!在起跑线等待发令枪
运动员4号开始准备
运动员1号准备完毕!在起跑线等待发令枪
运动员2号准备完毕!在起跑线等待发令枪
运动员3号准备完毕!在起跑线等待发令枪
java.lang.InterruptedException
运动员3号看不见起跑线了
运动员3号退场!
运动员2号看不见起跑线了
运动员2号退场!
运动员0号看不见起跑线了
运动员0号退场!
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员4号准备完毕!在起跑线等待发令枪
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员4号看不见起跑线了
运动员4号退场!
从输出可以看到,其中一个线程被中断,那么所有的运动员都退场了。
在实际使用CyclicBarrier,一定要防止这种情况发生。
3.如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,其他线程会抛出BrokenBarrierException,屏障被损坏。
这个就好比运动员都没有问题,而是裁判出问题了。裁判权力比较大,直接告诉所有的运动员,今天不比赛了,你们都回家吧!
public class Test {
static Map threads = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
String str = null;
str.substring(0, 1);// 模拟异常
System.out.println("发令枪响了,跑!");
}
});
for (int i = 0; i < 5; i++) {
MyThread t = new MyThread(barrier, "运动员" + i + "号", i);
threads.put(i, t);
t.start();
}
}
}
运行结果:
运动员0号开始准备
运动员3号开始准备
运动员2号开始准备
运动员1号开始准备
运动员4号开始准备
运动员0号准备完毕!在起跑线等待发令枪
运动员1号准备完毕!在起跑线等待发令枪
运动员2号准备完毕!在起跑线等待发令枪
运动员3号准备完毕!在起跑线等待发令枪
运动员4号准备完毕!在起跑线等待发令枪
Exception in thread "Thread-4" java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员0号看不见起跑线了
运动员0号退场!
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员3号看不见起跑线了
运动员3号退场!
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员1号看不见起跑线了
运动员1号退场!
java.lang.NullPointerException
at thread.Test$1.run(Test.java:15)
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:220)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at thread.MyThread.run(MyThread.java:27)
运动员2号看不见起跑线了
运动员2号退场!
我们在 CyclicBarrier 的构造方法中指定回调函数,并模拟了异常;
可以看到,如果在执行屏障动作的过程中出现异常,那么所有的线程都会抛出BrokenBarrierException异常。
这也提醒我们,使用带回调的CyclicBarrier构造方法时,指定的回调任务一定不要抛出异常,或者实现异常处理。
4.如果超出指定的等待时间,当前线程会抛出 TimeoutException 异常,其他线程会抛出BrokenBarrierException异常。
public class MyThread extends Thread {
private CyclicBarrier cyclicBarrier;
private String name;
private int ID;
public MyThread(CyclicBarrier cyclicBarrier, String name, int ID) {
super();
this.cyclicBarrier = cyclicBarrier;
this.name = name;
this.ID = ID;
}
@Override
public void run() {
System.out.println(name + "开始准备");
try {
Thread.sleep(ID * 1000);
System.out.println(name + "准备完毕!在起跑线等待发令枪");
try {
try {
cyclicBarrier.await(ID * 1000, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
e.printStackTrace();
}
System.out.println(name + "跑完了路程!");
} catch (BrokenBarrierException e) {
e.printStackTrace();
System.out.println(name + "看不见起跑线了");
}
System.out.println(name + "退场!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
运动员0号开始准备
运动员3号开始准备
运动员2号开始准备
运动员1号开始准备
运动员0号准备完毕!在起跑线等待发令枪
运动员4号开始准备
运动员0号跑完了路程!
运动员0号退场!
java.util.concurrent.TimeoutException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
运动员1号准备完毕!在起跑线等待发令枪
运动员2号准备完毕!在起跑线等待发令枪
运动员1号跑完了路程!
运动员1号退场!
运动员2号看不见起跑线了
运动员2号退场!
java.util.concurrent.TimeoutException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
运动员3号准备完毕!在起跑线等待发令枪
java.util.concurrent.BrokenBarrierException
运动员3号看不见起跑线了
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
运动员3号退场!
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
运动员4号准备完毕!在起跑线等待发令枪
java.util.concurrent.BrokenBarrierException
运动员4号看不见起跑线了
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
运动员4号退场!
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
从输出可以看到,如果其中一个参与者抛出TimeoutException,其他参与者会抛出 BrokenBarrierException。
如何处理 BrokenBarrierException ?
可以看到,使用 CyclicBarrier 还需注意许多事项,其中 BrokenBarrierException 被称为是 CyclicBarrier 的克星;
那又如何 处理/预防 BrokenBarrierException 呢?
当然,要预防,还需从 CyclicBarrier 的设计开始考虑,设计者已经帮我们考虑了一些问题,如检查是否被破坏,重置 CyclicBarrier 等。
boolean isBroken():获取是否破损标志位broken的值,此值有以下几种情况:
- CyclicBarrier初始化时,broken=false,表示屏障未破损。
- 如果正在等待的线程被中断,则broken=true,表示屏障破损。
- 如果正在等待的线程超时,则broken=true,表示屏障破损。
- 如果有线程调用CyclicBarrier.reset()方法,则broken=false,表示屏障回到未破损状态。
void reset():使得 CyclicBarrier 回归初始状态,直观来看它做了两件事:
- 如果有正在等待的线程,则会抛出BrokenBarrierException异常,且这些线程停止等待,继续执行。
- 将是否破损标志位broken置为false。
在任务协同阶段,我们可以借助这两个API来做辅助;
当然源码设计者肯定不能从源头将所有问题都解决,剩下的是需要我们根据业务情况,看是需要终止协作:抛异常、还是直接退出。
并且根据触发 BrokenBarrierException 的场景,我们在相关代码实现时,尽量规避。
推荐阅读
-
JUC源码
面试题拷问Java线程池
吃透线程池ThreadPoolExecutor源码
手写简易版Java线程池
FutureTask源码解析
手写FutureTask
AtomicXxxx原子类全解析
ReentrantLock之Condition源码
细聊ReentrantLock之Condition
本文首发于 公众号 架构道与术(ToBeArchitecturer),欢迎关注、学习更多干货~
推荐阅读
-
并发设计模式
并发设计模式总结
并发设计模式 | 生产者-消费者模式,流水线思想提高效率
并发设计模式 | 两阶段终止模式:如何优雅地终止线程?
并发设计模式 | Worker Thread模式:如何避免重复创建线程?
并发设计模式 | Thread-Per-Message每请求每线程
并发设计模式 | Balking模式:"你不需要,就算了"
并发设计模式 | Guarded Suspension模式:等待唤醒机制的规范实现
并发设计模式 | ThreadLocal线程本地存储模式
Copy-on-Write模式:不是延时策略的COW
并发设计 | Immutability模式:解决并发安全问题
-
并发工具
27 | 并发工具类-踩坑热点问题盘点
26 | Fork/Join:单机版的MapReduce
25 | CompletionService:批量执行异步任务
24 | CompletableFuture:Java异步编程
23 | Future:获取线程的执行结果
22 | Executor与线程池:如何创建正确的线程池?
21 | 并发工具:无锁原子类
20 | 并发容器:那些需要我们填的“坑”
19 | CountDownLatch和CyclicBarrier让多线程步调一致
18 | 比读写锁更快的StampedLock
17 | ReadWriteLock:如何快速实现一个完备的缓存?
16 | 用Semaphore实现一个限流器
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
14 | 并发编程之Lock和Condition
-
并发基础
13 | Java并发编程:12篇【干货】打包带走
12 | 并发编程:如何用面向对象思想写好并发程序?
11 | Java线程(下):为什么局部变量是线程安全的?
10 | Java线程(中):创建多少线程才是合适的?
09 | Java线程(上):Java线程的生命周期
08-管程:并发编程的万能钥匙
07-并发编程:安全性、活跃性以及性能问题
06 | 用“等待-通知”机制优化循环等待
05 | 一不小心就死锁了,怎么办?
04 | 互斥锁(下):如何用一把锁保护多个资源?
03 | 互斥锁(上):解决原子性问题
02 | Java内存模型:看Java如何解决可见性和有序性问题
01 | 可见性、原子性和有序性问题:并发编程Bug的源头