CyclicBarrier(回环屏障)
CyclicBarrier阻塞一组线程,直至某个状态之后再全部同时执行,并且所有线程都被释放。和之前说的CountdownLatch功能相反,CountdownLatch阻塞主线程,等所有子线程完结了再继续下去。
上节介绍的CountDownLatch在解决多个线程同步方面相对于调用线程的join方法已经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownIatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,可以让一组线程全部达到一个状态后再全部同时执行。
这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程就会冲破屏障,继续向下运行。
示例:
public class CycleBarrierTest1 {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+"task1 merge result");
}
});
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"task1-1");
System.out.println(Thread.currentThread()+"enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"enter out barrier");
} catch (Exception e) {
e.printStackTrace();
}
}
});
service.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"task1-2");
System.out.println(Thread.currentThread()+"enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"enter out barrier");
} catch (Exception e) {
e.printStackTrace();
}
}
});
service.shutdown();
}
}
运行的结果如下
如上代码创建了一个CyclicBarrier对象,第一个参数为计数器初始值,第二个参数Runable是当计数器值为0时需要执行的任务。在main函数里面首先创建了一个大小为2的线程池,然后添加两个子任务到线程池,每个子任务在执行完自己的逻辑后会调用await()方法。一开始计数器值为2,当第一个线程调用await方法时,计数器值会递减为1。由于此时计数器值不为0,所以当前线程就到了屏障点而被阻塞。然后第二个线程调用await时,会进入屏障,计数器值也会递减,现在计数器值为0,这时就会去执行CyclicBarrier构造函数中的任务,执行完毕后退出屏障点,并且唤醒被阻塞的第二个线程,这时候第一个线程也会退出屏障点继续向下运行。上面的例子说明了多个线程之间是相互等待的,假如计数器值为N,那么随后调用await方法的N-1个线程都会因为到达屏障点而被阻塞,当第N个线程调用await后,计数器值为0了,这时候第N个线程才会发出通知唤醒前面的N-1个线程。也就是当全部线程都到达屏障点时才能一块继续向下执行。
下面再举个例子来说明CyclicBarrier的可复用性。假设一个任务由阶段1、阶段2和阶段3组成,每个线程要串行地执行阶段1、阶段2和阶段3,当多个线程执行该任务时,必须要保证所有线程的阶段1全部完成后才能进入阶段2执行,当所有线程的阶段2全部完成后才能进入阶段3执行。
示例:
public class CycleBarrierTest2 {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"step-1");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"step-2");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"step-3");
} catch (Exception e) {
e.printStackTrace();
}
}
});
service.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread()+"step-1");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"step-2");
cyclicBarrier.await();
System.out.println(Thread.currentThread()+"step-3");
} catch (Exception e) {
e.printStackTrace();
}
}
});
service.shutdown();
}
}
在如上代码中,每个子线程在执行完阶段1后都调用了await方法,等到所有线程都到达屏障点后才会-块往下执行,这就保证了所有线程都完成了阶段1后才会开始执行阶段2。然后在阶段2后面调用了await方法,这保证了所有线程都完成了阶段2后,才能开始阶段3的执行。这个功能使用单个CountDownLatch是无法完成的。
这里要注意,程序在执行的时候,只有达到了计数器的值等于0,才会执行,否则会挂起,比如计数器的值为2,但只调用了一个await()方法(计数器减一),此时的计数器为1,如果计数器不为0,那么程序将会阻塞挂起等待。将上面示例中的cyclicBarrier.await();注释一个,那么程序将挂起等待。
用cyclicBarrier模拟起跑
public class CyclicBarrierTest {
// TODO 发令枪数量设置
private static final int N = 10;
// 创建一个回环屏障
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(N, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "枪响了,开跑");
}
});
public static void main(String rgs[]) throws InterruptedException {
//发令枪测试
ExecutorService service = Executors.newCachedThreadPool();
System.out.println("运动员上跑道准备。。。。");
for (int i = 0; i < N; ++i) // create and start threads
//任务
service.execute(new WorkerRunnable(cyclicBarrier, i));
service.shutdown();
}
}
class WorkerRunnable implements Runnable {
private final CyclicBarrier cb;
private final int i;
WorkerRunnable(CyclicBarrier cb, int i) {
this.cb = cb;
this.i = i;
}
public void run() {
try {
//阻塞等待,各就位等待号令枪响开始跑
System.out.println(i + "---> 准备就绪,等待开跑");
cb.await();
//业务代码
doWork(i);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
// TODO 其他业务测试在此方法内修改代码即可
void doWork(int i) {
System.out.println(i + "---> 起跑逻辑代码");
}
}