Java并发编程 - CyclicBarrier

在之前写过的关于CountDownLatch的这篇文章中,我们通过使用"学生春游场景"这个示例来讲述了CountDownLatch的使用,在这个示例中我们这样处理:老师拿了个包含50个同学名字的名单,同学来一个就划掉一个,当所有的同学都被划掉后,说明所有的同学都到了,这时候就可以出发了。"划掉"也体现了"count down"的含义。

继续拿这个案例说来说,不过我们现在关注的不是"到齐-出发"这个先来后到的问题,而关注的是"学生上车"这个动作,我们现在的规定是:学生来了,不能立刻上车,只有当所有的学生都到齐之后,才能上车。这里,我们可以采用"签到"的方式来记录学生的到来数,签到满50个后,学生上车。

"签到"是有加计数的含义,这和上面说的"划掉"正好是相反的概念。

这里说"加计数"只是个人觉得从直观上比较好的理解方式。我们后面讲的CycleBarrier与CountDownLatch一样内部也有一个计数器,调用一次await,就将计数减1,这和CountDownLatch计数处理的原理是一样的。只是CycleBarrier的本意是等所有的线程都到了再做处理,所以我觉得把调用一次await逻辑理解成为加上一次,直到加到满足我们的总数,这样能更好的理解,因为await并不像countDown那样具有很直观的逻辑含义。

CountDownLatch的使用场景中被等待的学生线程是可以执行完自己的逻辑的,而我们的等待线程就是要等被等待线程所有都执行完,这也是CountDownLatch的语义所在。但是现在的要求是被等待线程到了某个点后就应该停止,等待所有的线程都到达某个点。这时候使用CountDownLatch就没法满足我们的要求了。

Java提供了CyclicBarrier类,这个类可以满足我们的需求,虽然我们现在还没具体讲这个类,不过可以先通过使用示例认识下它。

学生春游场景

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class SpringOuting {


    public static void main(String[] args) throws Exception {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(50);// 签到名单

        Set<Thread> hashSet = new HashSet<>();
        for (int i=1; i<=50; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "签到...");
                        cyclicBarrier.await();// 签到并在这里等起
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println(Thread.currentThread().getName() + "上车了###");
                }
            }, "同学" + i);

           hashSet.add(t);
        }

        Iterator<Thread> it = hashSet.iterator();
        while (it.hasNext()) {
            Thread t = it.next();
            t.start();
            Thread.sleep(200);
        }
    }

}

下面的代码我们创建的了一个CyclicBarrier对象,规定了要签到的人数:

CyclicBarrier cyclicBarrier = new CyclicBarrier(50);

当学生线程执行,到了下面这里:

 cyclicBarrier.await();// 签到并等起

await()之后就挂起了。

运行代码后我们可以发现,等到最后一个学生线程执行await方法后,其他等待的学生线程都被唤醒了,然后就各自执行await后的代码。

通过CyclicBarrier我们成功得实现了"等学生都到了才能上车"这个需求。

老师发出命令后才上车

现在又有一个要求,在所有的学生到来之后,要先等老师大喊一声:同学们上车吧。之后学生才陆续上车。

CyclicBarrier也提供了这种支持,对上面的代码进行改造:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class SpringOuting {

    public static void main(String[] args) throws Exception {
        
        CyclicBarrier cyclicBarrier = new CyclicBarrier(50, new Runnable(){

            @Override
            public void run() {
                System.out.println("======同学们上车吧======");
            }
            
        });

        Set<Thread> hashSet = new HashSet<>();
        for (int i=1; i<=50; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "签到...");
                        cyclicBarrier.await();// 签到并子并在这里等起
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    
                    System.out.println(Thread.currentThread().getName() + "上车了###");
                }
            }, "同学" + i);

           hashSet.add(t);
        }

        Iterator<Thread> it = hashSet.iterator();
        while (it.hasNext()) {
            Thread t = it.next();
            t.start();
            Thread.sleep(200);
        }
        
    }

}

通过可以传递两个参数的构造方法创建了一个CyclicBarrier对象:

CyclicBarrier cyclicBarrier = new CyclicBarrier(50, new Runnable(){
    @Override
    public void run() {
        System.out.println("======同学们上车吧======");
    }           
});

第二个参数是Runnable接口的实现,看下API中的说明:

public CyclicBarrier(int parties, Runnable barrierAction)

Creates a new CyclicBarrier that will trip when the given number of parties (threads) are waiting upon it, and which will execute the given barrier action when the barrier is tripped, performed by the last thread entering the barrier.

创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)都进入等待状态后启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

某个时刻因为车没到,老师就通知已经签到的同学先回去

先来看代码:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class SpringOuting {

    public static void main(String[] args) throws Exception {
        
        CyclicBarrier cyclicBarrier = new CyclicBarrier(50, new Runnable(){

            @Override
            public void run() {
                System.out.println("======同学们上车吧======");
            }
            
        });

        Set<Thread> hashSet = new HashSet<>();
        for (int i=1; i<=50; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "签到...");
                        cyclicBarrier.await();// 签到并子并在这里等起
                    } catch (InterruptedException | BrokenBarrierException e) {
                        System.out.println(Thread.currentThread().getName() + "返回!!!");
                        return;
                    }
                    
                    System.out.println(Thread.currentThread().getName() + "上车了###");
                }
            }, "同学" + i);

           hashSet.add(t);
        }

        int i = 1;
        Iterator<Thread> it = hashSet.iterator();
        while (it.hasNext()) {
            Thread t = it.next();
            t.start();
            Thread.sleep(200);
            
            if (i == 10) {
                System.out.println("%%%%%%%同学们,车还没有到,你们先回去%%%%%%%");
                cyclicBarrier.reset();
            }
            
            i++;
        }
        
    }

}

运行代码,其中一次的结果如下:

同学41签到...
同学30签到...
同学33签到...
同学11签到...
同学28签到...
同学45签到...
同学47签到...
同学10签到...
同学12签到...
同学37签到...
%%%%%%%同学们,车还没有到,你们先回去%%%%%%%
同学41返回!!!
同学45返回!!!
同学33返回!!!
同学11返回!!!
同学12返回!!!
同学30返回!!!
同学10返回!!!
同学47返回!!!
同学37返回!!!
同学28返回!!!
同学17签到...
同学29签到...
同学39签到...
同学7签到...
同学16签到...
同学9签到...
同学27签到...
同学40签到...
同学31签到...
同学44签到...
同学8签到...
同学38签到...
同学25签到...
同学5签到...
同学3签到...
同学42签到...
同学19签到...
同学48签到...
同学22签到...
同学24签到...
同学26签到...
同学46签到...
同学34签到...
同学32签到...
同学18签到...
同学35签到...
同学4签到...
同学13签到...
同学14签到...
同学2签到...
同学15签到...
同学21签到...
同学20签到...
同学1签到...
同学36签到...
同学49签到...
同学6签到...
同学23签到...
同学50签到...
同学43签到...

通过结果可以看到,当已签到10个同学的时候,老师突然通知车还没到,已经签到的同学先回去,正在等待上车的同学收到了这个通知就返回了;但是我们看到剩下的40位同学依然过来进行了签到,并且全部都未上车。

从运行代码的IDE或命令窗口可以看到程序被挂起了。

为什么代码会有这样的表现?

我们可以看到与之前的代码不同的是,上面的代码中增加了一段这样的代码:

if (i == 10) {
    System.out.println("%%%%%%%同学们,车还没有到,你们先回去%%%%%%%");
    cyclicBarrier.reset();
 }

要知道这段代码的作用,我们需要知道reset的作用, API中描述如下:

public void reset()

Resets the barrier to its initial state. If any parties are currently waiting at the barrier, they will return with a BrokenBarrierException.

将屏障重置为其初始状态。如果存在参与者(们)目前在屏障处等待,则会唤醒它们,同时抛出一个BrokenBarrierException异常。

看了这个API的描述,我们应该就可以说明上面程序表现那样的行为的原因了:当i=10的时候,这时候有10个学生线程处于等待状态,调用了reset方法会做两个动作:

  • 会重置状态
    也就是说之前10个学生的签名作废,签名单重新需要50个同学签到才能有效
  • 通知这10个正在等待的线程
    唤醒的时候会抛出BrokenBarrierException异常,await方法会捕获到,我们上面的代码捕获到后做了让签到的同学返回的处理。

屏障被重置了,重新需要50个同学签到,这时已经有10个学生回去了,虽然后面的40个学生依然成功签到,但是如果那10个学生不回来重新签到的话,所有的学生就都无法上车。

await() API

Waits until all parties have invoked await on this barrier.

在所有等待在屏障的参与者都调用了await 方法之前,此参与者将一直等待。

If the current thread is not the last to arrive then it is disabled for thread scheduling purposes and lies dormant until one of the following things happens:

如果当前线程不是最后一个到达的线程,出于调度目的,将禁用它,且在发生以下情况之一前,该线程将一直处于休眠状态:

  • The last thread arrives; or
  • Some other thread interrupts the current thread; or
  • Some other thread interrupts one of the other waiting threads; or
  • Some other thread times out while waiting for barrier; or
  • Some other thread invokes reset() on this barrier.
  • 最后一个线程到达;或者
  • 其他某个线程中断当前线程;或者
  • 其他某个线程中断另一个等待线程;或者
  • 其他某个线程在等待 barrier 时超时;或者
  • 其他某个线程在此 barrier 上调用 reset()。

从这里可以看到对于中断,中断当前线程和中断另一个等待的线程都会对当前线程有唤醒影响。

If the current thread:

  • has its interrupted status set on entry to this method; or
  • is interrupted while waiting

then InterruptedException is thrown and the current thread's interrupted status is cleared.

如果当前线程:

  • 在进入此方法时已经设置了该线程的中断状态;或者
  • 在等待时被中断

则抛出 InterruptedException,并且清除当前线程的已中断状态。

If the barrier is reset() while any thread is waiting, or if the barrier is broken when await is invoked, or while any thread is waiting, then BrokenBarrierException is thrown.

如果在线程处于等待状态时barrier被reset(),或者在调用await时barrier被损坏,抑或任意一个线程正处于等待状态,则抛出 BrokenBarrierException 异常。

If any thread is interrupted while waiting, then all other waiting threads will throw BrokenBarrierException and the barrier is placed in the broken state.

如果任何线程在等待时被中断,则其他所有等待线程都将抛出 BrokenBarrierException异常,并将barrier置于损坏状态。

If the current thread is the last thread to arrive, and a non-null barrier action was supplied in the constructor, then the current thread runs the action before allowing the other threads to continue. If an exception occurs during the barrier action then that exception will be propagated in the current thread and the barrier is placed in the broken state.

如果当前线程是最后一个到达的线程,并且构造方法中提供了一个非空的屏障操作,则在允许其他线程继续运行之前,当前线程将运行该操作。如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,并将barrier置于损坏状态。

CyclicBarrier工作原理

从两个方面来研究CyclicBarrier的工作原理:

  1. 计数值
  2. 挂起和唤醒

计数值

CyclicBarrier通过计数值来计算到达的线程数。类中属性的定义如下:

/**
    * Number of parties still waiting. Counts down from parties to 0
    * on each generation.  It is reset to parties on each new
    * generation or when broken.
*/
private int count;

这个是CyclicBarrier中的一个普通属性,不过现在问题来了CyclicBarrier对象会被多个线程使用,也就是说会并发得被访问,那就需要保证每次只能一个线程对其修改操作。CyclicBarrier是怎么做的呢?

/**
 * Main barrier code, covering the various policies.
 */
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    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();
        }

        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();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        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 {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

从上面的代码可以看到,使用了ReentrantLock进行加锁处理,保证了count属性的同步访问。

挂起和唤醒

通过Condition接口的await和signalAll()方法实现。

底层的方法不是很难,主要用ReentrantLock和Condition来操作。这两个类不在本文讲述范围内,所以工作原理的研究就只点到为止。

总结

下面的内容摘抄自《Java并发编程的艺术》

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被挂起,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。

使用场景

下面的内容摘抄自《Java并发编程的艺术》

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均流水,最后,在用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

实现原理

内部通过ReentrantLock和Condition实现。

给定一个令牌总数,线程调用await方法将令牌数减1,如果令牌剩余不为0,调用Condition的await方法将线程放入到条件队列中,当最后一个线程调用await方法后,令牌剩余数为0,则通过Condtion的signalAll方法唤醒所有在条件队列中等待线程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容