在上一篇文章中我们介绍了同步辅助类CountDownLatch,在Java concurrent包下还有另一个同步辅助类CyclicBarrier与CountDownLatch非常类似,它也允许多个线程在某个点进行同步,但CyclicBarrier类更加强大。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干自己线程await() 方法后面的干活(如果await()后面有可执行的代码),同时会去执行Runnable barrierAction,且barrierAction的执行时间优先于所有线程后续await() 方法后面干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
操作方法
- 提供两种构造函数带Runnable的 CyclicBarrier(int parties, Runnable barrierAction)表示线程都同步后执行barrierAction。和不带Runnable。
- await() 方法每被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此 CyclicBarrier 上面阻塞的线程开始运行。
然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
1
2
|
public
int
await()
throws
InterruptedException, BrokenBarrierException { };
public
int
await( long timeout, TimeUnit unit)
throws
InterruptedException,BrokenBarrierException,TimeoutException { };
|
第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
与CountDownLatch的不同之处
CountDownLatch对象的作用只能使用一次,当计算减为0之后就不能再使用了。但CyclicBarrier对象可以重复使用。在计算减为0后,如果再次调用await() 方法,计数就又会变成 N-1,新一轮重新开始,也可以通过reset()重置,这便是 Cyclic 的含义所在。
CyclicBarrier 的构造函数还可以接受一个 Runnable,会在所有得线程都到达同步点后执行某些操作。
- CyclicBarrier.await() 方法会抛出一个独有的 BrokenBarrierException。这个异常发生在当某个线程在等待本 CyclicBarrier 时被中断或超时或被重置时,其它同样在这个 CyclicBarrier 上等待的线程便会受到 BrokenBarrierException。意思就是说,同志们,别等了,有个小伙伴已经挂了,咱们如果继续等有可能会一直等下去,所有各回各家吧。
使用实例
该例子在CountDownLatch的例子上进行改进,使用CyclicBarrier类代替结束倒计锁,当CyclicBarrier对象的计算为0时,比赛结束,启动一个线程执行统计:
package MyThread;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
public class Match {
// 模拟了100米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。
public static void main(String[] args) throws InterruptedException {
HashMap<String,String> result=new HashMap<>();
// 开始的倒数锁
final CountDownLatch begin=new CountDownLatch(1) ;
// 结束的倒数锁 使用CyclicBarrier,在所有线程结束后启动统计线程
final CyclicBarrier end=new CyclicBarrier(10, new Statistics(result));
// 十名选手
for (int index = 0; index < 10; index++) {
new Thread(new player(begin,end,result),"player"+index).start();
}
System.out.println("Game Start");
// begin减一,开始游戏
begin.countDown();
// 等待end变为0,即所有选手到达终点
}
}
class player implements Runnable{
// 开始的倒数锁
private final CountDownLatch begin ;
// 结束的倒数锁
private final CyclicBarrier end;
//成绩记录
HashMap<String,String> result;
player(CountDownLatch begin,CyclicBarrier end,HashMap<String,String> result){
this.begin=begin;
this.end=end;
this.result=result;
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
// 如果当前计数为零,则此方法立即返回。
// 等待
begin.await();
Thread.sleep((long) (Math.random() * 10000));
result.put(Thread.currentThread().getName(), new Date().toString());
System.out.println(Thread.currentThread().getName() + " arrived");
end.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 每个选手到达终点时,end就减一
}
}
}
class Statistics implements Runnable{
private HashMap<String,String> result;
Statistics(HashMap<String,String> result){
this.result=result;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Game Over\nbegin statistics:");
Iterator it=result.keySet().iterator();
while(it.hasNext()){
String key=(String)it.next();
String value=result.get(key);
System.out.println(key+":"+value);
}
}
}
结果
Game Start
player3 arrived
player4 arrived
player1 arrived
player6 arrived
player2 arrived
player0 arrived
player8 arrived
player7 arrived
player9 arrived
player5 arrived
Game Over
begin statistics:
player9:Mon Jun 06 21:15:26 CST 2016
player0:Mon Jun 06 21:15:22 CST 2016
player1:Mon Jun 06 21:15:20 CST 2016
player2:Mon Jun 06 21:15:22 CST 2016
player3:Mon Jun 06 21:15:17 CST 2016
player4:Mon Jun 06 21:15:18 CST 2016
player5:Mon Jun 06 21:15:27 CST 2016
player6:Mon Jun 06 21:15:21 CST 2016
player7:Mon Jun 06 21:15:25 CST 2016
player8:Mon Jun 06 21:15:22 CST 2016
下面举几个例子就明白了:
假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
Test {
public
static
void
main(String[] args) {
int
N =
4
;
CyclicBarrier barrier =
new
CyclicBarrier(N);
for
(
int
i=
0
;i<N;i++)
new
Writer(barrier).start();
}
static
class
Writer
extends
Thread{
private
CyclicBarrier cyclicBarrier;
public
Writer(CyclicBarrier cyclicBarrier) {
this
.cyclicBarrier = cyclicBarrier;
}
@Override
public
void
run() {
System.out.println(
"线程"
+Thread.currentThread().getName()+
"正在写入数据..."
);
try
{
Thread.sleep(
5000
);
//以睡眠来模拟写入数据操作
System.out.println(
"线程"
+Thread.currentThread().getName()+
"写入数据完毕,等待其他线程写入完毕"
);
cyclicBarrier.await();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println(
"所有线程写入完毕,继续处理其他任务..."
);
}
}
}
|
执行结果:
1
2
3
4
5
6
7
8
9
10
1
|
线程Thread-
0
正在写入数据...
线程Thread-
3
正在写入数据...
线程Thread-
2
正在写入数据...
线程Thread-
1
正在写入数据...
线程Thread-
2
写入数据完毕,等待其他线程写入完毕
线程Thread-
0
写入数据完毕,等待其他线程写入完毕
线程Thread-
3
写入数据完毕,等待其他线程写入完毕
线程Thread-
1
写入数据完毕,等待其他线程写入完毕
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
|
从上面输出结果可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。
当所有线程线程写入操作完毕之后,所有线程就继续进行后续的操作了。
如果说想在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
class
Test {
public
static
void
main(String[] args) {
int
N =
4
;
CyclicBarrier barrier =
new
CyclicBarrier(N,
new
Runnable() {
@Override
public
void
run() {
System.out.println(
"当前线程"
+Thread.currentThread().getName());
}
});
for
(
int
i=
0
;i<N;i++)
new
Writer(barrier).start();
}
static
class
Writer
extends
Thread{
private
CyclicBarrier cyclicBarrier;
public
Writer(CyclicBarrier cyclicBarrier) {
this
.cyclicBarrier = cyclicBarrier;
}
@Override
public
void
run() {
System.out.println(
"线程"
+Thread.currentThread().getName()+
"正在写入数据..."
);
try
{
Thread.sleep(
5000
);
//以睡眠来模拟写入数据操作
System.out.println(
"线程"
+Thread.currentThread().getName()+
"写入数据完毕,等待其他线程写入完毕"
);
cyclicBarrier.await();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println(
"所有线程写入完毕,继续处理其他任务..."
);
}
}
}
|
运行结果:
1
2
3
4
5
6
7
8
9
10
11
|
线程Thread-
0
正在写入数据...
线程Thread-
1
正在写入数据...
线程Thread-
2
正在写入数据...
线程Thread-
3
正在写入数据...
线程Thread-
0
写入数据完毕,等待其他线程写入完毕
线程Thread-
1
写入数据完毕,等待其他线程写入完毕
线程Thread-
2
写入数据完毕,等待其他线程写入完毕
线程Thread-
3
写入数据完毕,等待其他线程写入完毕
当前线程Thread-
3
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
|
从结果可以看出,当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。
下面看一下为await指定时间的效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
3
8
39
40
41
42
43
44
45
|
public
class
Test {
public
static
void
main(String[] args) {
int
N =
4
;
CyclicBarrier barrier =
new
CyclicBarrier(N);
for
(
int
i=
0
;i<N;i++) {
if
(i<N-
1
)
new
Writer(barrier).start();
else
{
try
{
Thread.sleep(
5000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
new
Writer(barrier).start();
}
}
}
static
class
Writer
extends
Thread{
private
CyclicBarrier cyclicBarrier;
public
Writer(CyclicBarrier cyclicBarrier) {
this
.cyclicBarrier = cyclicBarrier;
}
@Override
public
void
run() {
System.out.println(
"线程"
+Thread.currentThread().getName()+
"正在写入数据..."
);
try
{
Thread.sleep(
5000
);
//以睡眠来模拟写入数据操作
System.out.println(
"线程"
+Thread.currentThread().getName()+
"写入数据完毕,等待其他线程写入完毕"
);
try
{
cyclicBarrier.await(
2000
, TimeUnit.MILLISECONDS);
}
catch
(TimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"所有线程写入完毕,继续处理其他任务..."
);
}
}
}
|
执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
7
2
|
线程Thread-
0
正在写入数据...
线程Thread-
2
正在写入数据...
线程Thread-
1
正在写入数据...
线程Thread-
2
写入数据完毕,等待其他线程写入完毕
线程Thread-
0
写入数据完毕,等待其他线程写入完毕
线程Thread-
1
写入数据完毕,等待其他线程写入完毕
线程Thread-
3
正在写入数据...
java.util.concurrent.TimeoutException
Thread-
1
所有线程写入完毕,继续处理其他任务...
Thread-
0
所有线程写入完毕,继续处理其他任务...
at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
at java.util.concurrent.CyclicBarrier.await(Unknown Source)
at com.cxh.test1.Test$Writer.run(Test.java:
58
)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
at java.util.concurrent.CyclicBarrier.await(Unknown Source)
at com.cxh.test1.Test$Writer.run(Test.java:
58
)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
at java.util.concurrent.CyclicBarrier.await(Unknown Source)
at com.cxh.test1.Test$Writer.run(Test.java:
58
)
Thread-
2
所有线程写入完毕,继续处理其他任务...
java.util.concurrent.BrokenBarrierException
线程Thread-
3
写入数据完毕,等待其他线程写入完毕
at java.util.concurrent.CyclicBarrier.dowait(Unknown Source)
at java.util.concurrent.CyclicBarrier.await(Unknown Source)
at com.cxh.test1.Test$Writer.run(Test.java:
58
)
Thread-
3
所有线程写入完毕,继续处理其他任务...
|
上面的代码在main方法的for循环中,故意让最后一个线程启动延迟,因为在前面三个线程都达到barrier之后,等待了指定的时间发现第四个线程还没有达到barrier,就抛出异常并继续执行后面的任务。
另外CyclicBarrier是可以重用的,看下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
3
44
|
public
class
Test {
public
static
void
main(String[] args) {
int
N =
4
;
CyclicBarrier barrier =
new
CyclicBarrier(N);
for
(
int
i=
0
;i<N;i++) {
new
Writer(barrier).start();
}
try
{
Thread.sleep(
25000
);
}
catch
(InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"CyclicBarrier重用"
);
for
(
int
i=
0
;i<N;i++) {
new
Writer(barrier).start();
}
}
static
class
Writer
extends
Thread{
private
CyclicBarrier cyclicBarrier;
public
Writer(CyclicBarrier cyclicBarrier) {
this
.cyclicBarrier = cyclicBarrier;
}
@Override
public
void
run() {
System.out.println(
"线程"
+Thread.currentThread().getName()+
"正在写入数据..."
);
try
{
Thread.sleep(
5000
);
//以睡眠来模拟写入数据操作
System.out.println(
"线程"
+Thread.currentThread().getName()+
"写入数据完毕,等待其他线程写入完毕"
);
cyclicBarrier.await();
}
catch
(InterruptedException e) {
e.printStackTrace();
}
catch
(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"所有线程写入完毕,继续处理其他任务..."
);
}
}
}
|
执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1
5
|
线程Thread-
0
正在写入数据...
线程Thread-
1
正在写入数据...
线程Thread-
3
正在写入数据...
线程Thread-
2
正在写入数据...
线程Thread-
1
写入数据完毕,等待其他线程写入完毕
线程Thread-
3
写入数据完毕,等待其他线程写入完毕
线程Thread-
2
写入数据完毕,等待其他线程写入完毕
线程Thread-
0
写入数据完毕,等待其他线程写入完毕
Thread-
0
所有线程写入完毕,继续处理其他任务...
Thread-
3
所有线程写入完毕,继续处理其他任务...
Thread-
1
所有线程写入完毕,继续处理其他任务...
Thread-
2
所有线程写入完毕,继续处理其他任务...
CyclicBarrier重用
线程Thread-
4
正在写入数据...
线程Thread-
5
正在写入数据...
线程Thread-
6
正在写入数据...
线程Thread-
7
正在写入数据...
线程Thread-
7
写入数据完毕,等待其他线程写入完毕
线程Thread-
5
写入数据完毕,等待其他线程写入完毕
线程Thread-
6
写入数据完毕,等待其他线程写入完毕
线程Thread-
4
写入数据完毕,等待其他线程写入完毕
Thread-
4
所有线程写入完毕,继续处理其他任务...
Thread-
5
所有线程写入完毕,继续处理其他任务...
Thread-
6
所有线程写入完毕,继续处理其他任务...
Thread-
7
所有线程写入完毕,继续处理其他任务...
|
从执行结果可以看出,在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。