目录
一.简介
在 JUC学习(二):线程同步之CountDownLatch 中,介绍了CDL,它可以实现线程同步,但是也有一个不足:无法重用。一旦计数器清零,CDL对象就无法再使用。如果一个任务中,需要多次进行线程同步,那么就要设置多个CDL,不但浪费资源,而且对象多了也容易搞混,不利于维护。CyclicBarrier就能够实现重用。不同的是,CDL是一组线程等待另一组线程,CyclicBarrier是一组线程间相互等待。
下面是一段示例代码:
import java.util.Random;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierTest {
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> System.out.println("线程们都准备好了!"));
public static void testMethod(){
while(true){
try{
Thread.sleep(new Random().nextInt(10)*1000);
System.out.println(Thread.currentThread().getName()+"准备好了!");
barrier.await();
}catch (Exception e) {
}
}
}
public static void main(String args[]) {
Thread[] threads = new Thread[5];
for(int i=0;i<5;i++){
threads[i] = new Thread(()->testMethod(),"Thread-"+i);
}
for(int i=0;i<5;i++){
threads[i].start();
}
}
}
运行结果类似于:
Thread-0准备好了!
Thread-4准备好了!
Thread-3准备好了!
Thread-1准备好了!
Thread-2准备好了!
线程们都准备好了!
Thread-4准备好了!
Thread-2准备好了!
Thread-1准备好了!
Thread-3准备好了!
Thread-0准备好了!
线程们都准备好了!
Thread-0准备好了!
Thread-1准备好了!
Thread-2准备好了!
Thread-3准备好了!
Thread-4准备好了!
线程们都准备好了!
可以无限运行下去。可以看到,barrier可以重复使用。
二.构造方法
和CDL不同,CyclicBarrier构造方法可以额外接受一个Runnable对象,作为线程全部就绪后进行的操作:
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
三.await方法
await方法实际调用了dowait方法,下面分段解说该方法:
首先,使用ReentrantLock加锁,并进行一些状态检验:
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();
}
...
} finally {
lock.unlock();
}
Generation是一个静态内部类,只有一个成员变量broken。它是用来标记每一代barrier的状态的。
接下来是判断barrier是否已经被打破的代码:
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
每有一个线程执行await,都会将计数器值减1,假如计数器清零,就会执行预设的任务(如果有的话),并开始下一轮同步。
如果barrier还没有被打破,就先自旋等待:
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
可以看到,退出等待的方法有四个:1)超时(如果设置了时限的话);2)线程中断;3)barrier被打破(即count为0);4)当前世代的barrier已结束(实际上就是执行了reset方法)
reset的一个例子是,假如CyclicBarrier的count值大于线程总数,可以用该方法手动结束同步状态:
public class CyclicBarrierTest {
private static CyclicBarrier barrier = new CyclicBarrier(6, () -> System.out.println("线程们都准备好了!"));
public static void testMethod(){
...
}
public static void main(String args[]) throws InterruptedException {
Thread[] threads = new Thread[5];
for(int i=0;i<5;i++){
threads[i] = new Thread(()->testMethod(),"Thread-"+i);
}
for(int i=0;i<5;i++){
threads[i].start();
}
Thread.sleep(10000);
barrier.reset();
}
}
testMethod方法的代码与示例代码相同。上面的代码中,将barrier的计数器值改为6,并设置了main线程sleep十秒钟后执行barrier的reset方法,结果如下:
Thread-0准备好了!
Thread-4准备好了!
Thread-3准备好了!
Thread-1准备好了!
Thread-2准备好了!
//上面是reset执行之前的输出
Thread-1准备好了!
Thread-2准备好了!
Thread-4准备好了!
Thread-0准备好了!
Thread-3准备好了!
为了方便观察效果,我加了一行“注释”,实际输出没有这行。可以看到,reset之后,第一轮同步中断,没有执行我们预设的任务;紧接着开始了第二轮同步,不过由于线程数不足以打破barrier,使得第二轮同步永远地阻塞了下去。