Java同步器之CyclicBarrier源码分析

本文详细介绍了Java并发工具类CyclicBarrier的原理和使用,通过实例展示了其如何协调多个线程同步执行。CyclicBarrier不同于CountDownLatch,它可以重复使用,允许多个线程在达到预设屏障点后一起继续执行。文章还深入探讨了CyclicBarrier的内部实现,包括Generation、ReentrantLock和Condition的运用,以及关键方法如await()和nextGeneration()的工作流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章参考:AQS源码探究_08 CyclicBarrier源码分析_兴趣使然的草帽路飞-CSDN博客

简介

CyclicBarrier,回环栅栏,它会阻塞一组线程直到这些线程同时达到某个条件才继续执行。它与CountDownLatch很类似,但又不同,CountDownLatch需要调用countDown()方法触发事件,而CyclicBarrier不需要,它就像一个栅栏一样,当一组线程到达栅栏处才能继续往下走。

使用案例

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                System.out.println("before");
                try{
                    cyclicBarrier.await();
                }catch (InterruptedException | BrokenBarrierException e){
                    e.printStackTrace();
                }
                System.out.println("after");
            }).start();
        }
    }
}

这段代码使用一个CyclicBarrier使得三个线程保持同步,当三个线程同时到达cyclicBarrier.await()方法出时,大家再一起往下运行。 

源码分析

内部类

    private static class Generation {
        boolean broken = false;
    }

Generation中文翻译为代,用于控制CyclicBarrier的循环使用。比如,上面示例中的三个线程完成后进入下一代,继续等待三个线程达到栅栏处再一起执行,而CountDownLatch则做不到这一点,CountDownLatch是一次性的,无法重置其次数。

主要属性

    //重入锁  因为barrier实现是依赖于Condition条件队列的,Condition条件队列必须依赖lock
    private final ReentrantLock lock = new ReentrantLock();
    
    //条件锁 名为trip,绊倒的意思,可能是指线程来了先绊倒,等达到一定数量了再唤醒
    //线程挂起实现使用的condition队列
    //条件:等待所有的线程到位,这个条件队列内的线程才会被唤醒
    private final Condition trip = lock.newCondition();
    
    //需要等待的线程数量:Barrier需要参与进来的线程数量
    private final int parties;

    //当唤醒的时候需要执行的命令  当前代最后一个到位的线程需要执行的事件
    private final Runnable barrierCommand;

    //代   表示barrier对象  当前代
    private Generation generation = new Generation();

    //表示当前代还需要等待的线程个数
    //当前代还有多少个线程未到位  初始值为parties
    private int count;

构造方法

    public CyclicBarrier(int parties) {
        this(parties, null);
    }    
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        //初始化parties
        this.parties = parties;
        //初始化count等于parties
        this.count = parties;
        //初始化 线程都到达栅栏处执行的命令
        this.barrierCommand = barrierAction;
    }

构造方法需要传入一个parties变量,也就是需要等待的线程数。

parties:Barrier需要参与的线程数量,每次屏障需要参与的线程数

成员方法

1.nextGeneration()方法

    //开启下一代的方法,当这一代所有线程到位后(假设barrierCommand不为空,还需要最后一个线程执行完事件),会调用nextGeneration()开启新的一代
    private void nextGeneration() {
        // signal completion of last generation
        //将在trip条件队列内挂起的线程全部唤醒
        trip.signalAll();
        //重置count为parties
        count = parties;
        //开启新的一代   使用一个新的generation对象,表示新的一代,新的一代和上一代没有任何关系
        generation = new Generation();
    }

 2.breakBarrier()方法

    //打破barrier屏障  在屏障内的线程都会抛出异常
    private void breakBarrier() {
        //将代中的broken设置为true  表示这一代是被打破的,再来到这一代的线程  直接抛出异常
        generation.broken = true;
        //重置count为parties
        count = parties;
        //将在trip条件队列挂起的线程全部唤醒,唤醒后的线程会检查当前代是否是打破的,
        //如果是打破的,接下来的逻辑和开启下一代唤醒的逻辑不一样
        trip.signalAll();
    }

await()方法

每个需要在栅栏处等待的线程都需要显示的调用await()方法等待其他线程的到来。

    //
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            //调用的dowait()方法作为返回值  不需要超时
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

dowait()方法

dowait()方法里的整个逻辑分为两部分

1.最后一个线程走上面的逻辑,当count减为0的时候,打破栅栏,它调用nextGeneration()方法通知条件队列中的等待线程转移到AQS的队列中等待被唤醒,并进入下一代

2.非最后一个线程走下面的for循环逻辑,这些线程会阻塞在condition的await()方法处,他们会加入到条件队列中,等待被通知,当他们唤醒的时候已经更新换代了,这时候返回

    //timed:表示当前调用await方法的线程是否指定了超时时长,如果true表示线程是响应超时的
    //nanos:线程等待超时时长纳秒,如果timed == false  ==> nanos==0
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        //获取全局锁对象
        final ReentrantLock lock = this.lock;
        //加锁  这里为什么要加锁呢
        //因为barrier的挂起和唤醒依赖的组件都是condition
        lock.lock();
        try {
            //当前代:获取barrier当前的代
            final Generation g = generation;
            //检查  如果当前代是已经被打破状态,则当前调用await方法的线程,直接抛出Broken异常
            if (g.broken)
                throw new BrokenBarrierException();
            //中断检查 如果当前线程的中断标记为true  则打破当前代  然后当前线程抛出中断异常
            if (Thread.interrupted()) {
                //1.设置当前代的状态为broken状态
                //2.唤醒在trip条件队列内的线程
                breakBarrier();
                throw new InterruptedException();
            }
            //执行到这里  说明当前线程中断状态是正常的false,当前代的broken为false(未打破状态)
            //正常逻辑
             
            //将count值减1
            //假设parties给的是5  那么index对应的值为4,3,2,1,0
            int index = --count;
            //如果count数量减到了0  则走这段逻辑
            //条件成立  说明当前线程是最后一个到达barrier的线程,此时需要做什么呢?
            if (index == 0) {  // tripped
                //标记true表示最后一个线程  执行cmd时未抛出异常  false 表示最后一个线程执行cmd时抛出异常了
                //cmd就是创建barrier对象时 指定的第二个Runnable接口实现  这个可以为null
                boolean ranAction = false;
                try {
                    //如果初始化的时候传了命令  在这里执行
                    final Runnable command = barrierCommand;
                    //条件成立:说明创建barrier对象时  指定了Runnable接口了,这个时候最后一个到达的线程就需要执行这个接口
                    if (command != null)
                        command.run();
                    //command.run()未抛出异常的话,那么线程会执行到这里
                    ranAction = true;
                    //调用下一代方法
                    //1.唤醒trip条件队列中挂起的线程,被唤醒的线程会依次获取到锁,然后依次退出await()方法
                    //2.重置count为parties
                    //3.创建一个新的generation对象,表示新的一代
                    nextGeneration();
                    //返回0  因为当前线程是此代  最后一个的到达的线程  索引index == 0
                    return 0;
                } finally {
                    if (!ranAction)
                        //如果command.run()抛出异常的话  会走这里
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            //这个循环只有非最后一个线程可以走到
            //自旋  一直等到条件满足  当前代被打破 线程被中断  等待超时
            for (;;) {
                try {
                    //true  条件成立   说明当前线程是不指定超时时间的
                    if (!timed)
                        //调用condition的await方法
                        //当前线程会释放掉lock,然后进入到trip条件队列的尾部,然后挂起自己,等待被唤醒。
                        trip.await();
                    else if (nanos > 0L)
                        // 超时等待方法:
                    // 说明当前线程调用await方法时 是指定了超时时间的!
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    // 抛出中断异常,会进来这里。
                // 什么时候会抛出InterruptedException异常呢?
                // Node节点在 条件队列内 时 收到中断信号时 会抛出中断异常!


                // 条件一:g == generation 成立,说明当前代并没有变化。
                // 条件二:! g.broken 当前代如果没有被打破,那么当前线程就去打破,并且抛出异常..
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // 执行到else有几种情况?
                    // 1.代发生了变化,这个时候就不需要抛出中断异常了,因为 代已经更新了,这里唤醒后就走正常逻辑了..只不过设置下 中断标记。
                    // 2.代没有发生变化,但是代被打破了,此时也不用返回中断异常,执行到下面的时候会抛出  brokenBarrier异常。也记录下中断标记位。
                        Thread.currentThread().interrupt();
                    }
                }

            // 唤醒后,执行到这里,有几种情况?
            // 1.正常情况,当前barrier开启了新的一代(trip.signalAll())
            // 2.当前Generation被打破,此时也会唤醒所有在trip上挂起的线程
            // 3.当前线程trip中等待超时,然后主动转移到 阻塞队列 然后获取到锁 唤醒。

            // 检查: 
            // 条件成立:当前代已经被打破
                if (g.broken)
                    // 线程唤醒后依次抛出BrokenBarrier异常。
                    throw new BrokenBarrierException();
            // 唤醒后,执行到这里,有几种情况?
            // 1.正常情况,当前barrier开启了新的一代(trip.signalAll()),
            // 3.当前线程trip中等待超时,然后主动转移到 阻塞队列 然后获取到锁 唤醒。
			// 正常来说这里肯定不相等
			// 因为上面打破栅栏的时候调用nextGeneration()方法时generation的引用已经变化了
           
            // 条件成立:说明当前线程挂起期间,最后一个线程到位了,然后触发了开启新的一代的逻辑,此时唤醒trip条件队列内的线程。
                if (g != generation)
                    //返回当前线程的index
                    return index;
                // 唤醒后,执行到这里,有几种情况?
                // 3.当前线程trip中等待超时,然后主动转移到阻塞队列然后获取到锁 唤醒。
                // 超时检查
                if (timed && nanos <= 0L) {
                    //打破barrier
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

4、总结
        CyclicBarrier会使一组线程阻塞在await()处,当最后一个线程到达时唤醒(只是从条件队列转移到AQS队列中)前面的线程大家再继续往下走;
        CyclicBarrier不是直接使用AQS实现的一个同步器;
        CyclicBarrier基于ReentrantLock及其Condition实现整个同步逻辑;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值