rainie-love

  CyclicBarrier是JDK 1.5 concurrent包出现的一个用于解决多条线程阻塞,当达到一定条件时一起放行的一个类。我们先来看这样一个简单的需求。

  现在我有一个写入数据的类,继承Runable接口:

public class WriteDateThread implements Runnable {

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "开始写入数据...");
        
        System.out.println(Thread.currentThread().getName() + "写入数据完毕!");
    }
}

  代码很简单,只有两个输出语句,一个是开始写入数据,然后打印写入完毕。

  然后创建这样一个Main函数的类:

public class CyclicBarrierMain {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            WriteDateThread thread = new WriteDateThread();
            Thread t = new Thread(thread);
            t.start();
        }
    }
}

  同样,也是很简单的代码,我现在通过main函数来启动这样5个WriteDateThread线程,想必大家应该清楚线程之间的执行顺序在于谁抢到了CPU执行权,于是执行结果如下:

  

  我们可看到,每条线程的开始执行和执行完毕和其他线程都是交错的,这也符合了多线程的执行规律。我们来想象一个实际中的需求,现在有5个玩家同时进入一场游戏对决当中,系统需要读取玩家的数据,当读取完成后,允许玩家进入游戏中。按理来说,当系统读取完某玩家的数据后,该玩家就拥有入场权限了,如果我们不加任何干预的话,会发生什么情况呢?正如上图中执行结果那样,系统读取0,2号玩家的数据,然后2号玩家读取完毕,于是2号玩家先进场,然后再读取4,1号玩家的数据...以此类推。我想大家肯定玩过LOL或者农药这类的多人竞技游戏,实际情况是这样的吗?不是!实际情况是,系统加载完某玩家的数据后,会让该玩家等待其他玩家的数据加载,当所有玩家的数据都加载完毕后,大家同时进入游戏(这里不考虑掉线等意外情况)。

  好了,有了这样一个实际案例,我说出现在的需求大家就很容易理解了,现在的需求是要求所有线程在执行完写入数据后,不能在向下执行,而是等待所有其他线程执行完写入数据,然后大家再一起执行“写入数据完毕...”,我们当然可以用java.util.concurrent包下CountDownLatch计数器来自行控制,但是这里我希望用更智能化一些的CyclicBarrier(循环栅栏)来完成这个需求:

  代码如下:

 1 public class WriteDateThread implements Runnable {
 2 
 3     private CyclicBarrier barrier;
 4     
 5     public WriteDateThread(CyclicBarrier barrier) {
 6         this.barrier = barrier;
 7     }
 8 
 9     @Override
10     public void run() {
11         System.out.println(Thread.currentThread().getName() + "开始写入数据...");
12         try {
13             barrier.await(); //通过调用await方法在此处设置栅栏,使得线程执行到此进入阻塞状态等待其他线程执行完成。
14         } catch (InterruptedException | BrokenBarrierException e) {
15             e.printStackTrace();
16         }
17         System.out.println(Thread.currentThread().getName() + "写入数据完毕!");
18     }
19 }
 1 public class CyclicBarrierMain {
 2     public static void main(String[] args) {
 3         CyclicBarrier barrier = new CyclicBarrier(5);
 4         for (int i = 0; i < 5; i++) {
 5             WriteDateThread thread = new WriteDateThread(barrier);
 6             Thread t = new Thread(thread);
 7             t.start();
 8         }
 9     }
10 }

  我们通过调用CyclicBarrier的await方法,使线程执行到此的时候,进入到阻塞状态,并且等待其他线程都执行到此后,再统一放行,让它们同时向下继续执行(放行后具体谁先执行,还是要看谁先抢到CPU执行权)

  运行结果:

  

  如此,便得到了我们需求中想要的结果了。

  CyclicBarrier的用法到此就介绍完毕,下面我着重介绍一下,CyclicBarrier最常用的一个构造函数,也是我第一次使用时非常令我困惑的一个点,就是我上面代码CyclicBarrierMain中第3行CyclicBarrier barrier = new CyclicBarrier(int parties);这个构造器。

  我们现在有5条线程,因此我传入5这毫无疑问,但是这个5究竟是什么意思呢?如果传入的值大于5或者小于5又会是什么结果呢?大家不妨可以自行试验一下,只要我们传入的不是5,程序都会一直处于阻塞状态,停不下来。

  我翻看过别人别这个值的解释,比较抽象,原文如下:

  参数parties指定线程数量,当指定的线程值都到达栅栏点时,栅栏打开,线程恢复。需要注意的是,当指定的线程数量大于启动的线程数量,比如修改上例中的代码,只启动4个线程,那么所有的线程将执行到await处然后一直处于等待状态。第二种情况是指定的线程数量小于启动的线程,上例代码,启动6个线程,那么当第5个线程到达栅栏点时,那么这5个线程就会恢复继续执行,而第6个线程将一直处于阻塞状态。

  我将通过一个小故事来说明这个问题。

  故事是这样的,有这样一场骑马比赛,有5名骑手,他们各自拥有自己的赛道,并且每条赛道上在开赛之前就已经预设好了一个栅栏。另外还有一名栅栏管理员和一名指挥官,指挥官只负责向管理员发号施令,他只会告诉管理员一个数字n。这个数字的意思是,管理员要打开的栅栏的数量,前提是他必须看到有n个骑手都到达栅栏前,才会将这n个骑手面前的栅栏打开。于是管理会记着这个数字n在栅栏旁守着,当有1名、2名...骑手到达栅栏时,管理员不予理会,直到有第n名骑手到达栅栏处时,管理员统一打开栅栏放行,骑手们得以同时继续骑行,而管理员则完成了任务,进屋休息去了。

  如果指挥官告诉管理员的数字是5,那么一切和平的进行,不会发生任何问题。但是如果指挥官,告诉管理员的数字不是5,比如说4或者6。这样问题就产生了,对于这两种情况,我们分别来分析一下:

  • 指挥官给出的数字大于骑手数量,这里我们假设是6

  我前边说过,每条赛道上在开赛之前都预设好了栅栏,那么此时管理从指挥官那里得到的数字是6,因此管理员要执行的任务就是看着栅栏,直到第1名骑手到达,第2名骑手到达,第3名...直到管理员看见第6名骑手到达时才进行统一放行,但是问题是,总共的参赛选手只有5名啊!这下好了,管理员是个死心眼,他不看见第6名骑手他是绝对不会进行放行的,那么大家可以想象一下,5名骑手被卡在栅栏前不能继续后面的比赛,而管理员在那里痴痴的等着第6名骑手的到来,就这样一直到天荒地老...

  • 指挥官给出的数字小于骑手数量,这里我们假设是4

  还像刚才一样,管理拿到数字4后便在栅栏旁等着,直到看见骑手的相继到来,第1名,第2名,第3名,第4名!管理员看见第4名骑手到来后,立即打开他们各自赛道前的栅栏,于是这4名骑手继续后面的比赛,管理员也完成了自己的任务,进屋睡觉去了...等等!还有第5名骑手呢!第5名骑手到达栅栏前惊奇的发现,其他赛道都被放行了,但是他的赛道上还有栅栏,他不得不等下等着管理员放行,但是郁闷的是,他并不知道管理员再也不会回来了,于是他就这样孤独的一直等待着...

  

  好了,故事讲完了,现在我们通过这个故事映射到代码当中,骑手其实就是我们开的线程,他们各自赛道上赛前设置的栅栏,其实就是我们在程序执行前调用的CyclicBarrier的await方法,程序执行到这个栅栏处会被阻塞住。指挥官其实就是我们自己(写程序的人),管理员就是CyclicBarrier内部实现唤醒线程的解决方案,我们(指挥官)给管理员发号的施令(一个数字)就是我们通过CyclicBarrier(int parties)传入的数值。

  如果我们传入的是6,因为我们只有5条线程,第6条线程永远不会到来,因此管理员也就永远不会放行栅栏,所有线程将会阻塞在栅栏出等待那不存在的第6条线程,我们更改代码中传入的数值为6,得到的运行结果:

  

  可以看到,5条线程全部阻塞到了调用await方法的地方,并且程序一直处于阻塞状态。

  

  如果我们传入的是4,管理员在看到第4条线程的到来时,就会放行他们4个面前的栅栏,于是这4条线程继续执行,此时管理员完成任务回家睡觉了,第5条线程到来时,会一直卡在栅栏前。我们更改代码中传入的数值为4,得到的运行结果:

  

  

  通过上述的通俗的讲解,相信大家不会在觉得CyclicBarrier的(int parties)构造方法那么晦涩难懂了,由于本人所知有限,故难保证文中有什么错误之处,欢迎大牛不惜吝啬下方留言指正。

 

相关文章: