Java并发编程-同步辅助类之CyclicBarrier

在上一篇文章中我们介绍了同步辅助类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无法进行重复使用。


猜你喜欢

转载自blog.csdn.net/luckykapok918/article/details/80974999