Java多线程 之 CyclicBarrier源码分析

​ 目录

1、背景介绍

2、运行实例

3、源码分析

4、CyclicBarrier图示


一、背景介绍

CyclicBarrier 字面直译意思是循环栅栏,他要做的事情是让一组各自执行自己任务的子线程共同到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时候,屏障才会开门。所有被屏障拦截的子线程才会继续运行,完成后续任务,如果后续同样有栅栏进行阻塞,循环往复执行同样的流程。

二、运行实例

下面先通过一个简单的实例感受一下CyclicBarrier的功能。

/**
 * @author: wenyixicodedog
 * @create: 2020-07-27
 * @description:
 */
public class TestCyclicBarrier {
​
    public static void main(String[] args) {
​
        CyclicBarrier barrier = new CyclicBarrier(4);
        for (int i = 0; i < barrier.getParties(); i++) {
            new Thread(() -> {
                for (int j = 0; j < 3; j++) {
                    try {
                        int a = new Random().nextInt((3000 - 1000) + 1) + 1000;
                        Thread.sleep(a);
                        System.out.println(Thread.currentThread().getName() + ", 通过了第" + j + "个障碍物, 耗时 " + ((double) a / 1000) + "s");
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        System.out.println("===============main 方法执行完毕===============");
    }
​
}

执行结果如下:

===============main 方法执行完毕===============
Thread-1, 通过了第0个障碍物, 耗时 1.042s
Thread-0, 通过了第0个障碍物, 耗时 1.872s
Thread-2, 通过了第0个障碍物, 耗时 2.017s
Thread-3, 通过了第0个障碍物, 耗时 2.244s
Thread-2, 通过了第1个障碍物, 耗时 1.264s
Thread-3, 通过了第1个障碍物, 耗时 1.997s
Thread-1, 通过了第1个障碍物, 耗时 2.333s
Thread-0, 通过了第1个障碍物, 耗时 2.889s
Thread-0, 通过了第2个障碍物, 耗时 1.042s
Thread-3, 通过了第2个障碍物, 耗时 1.047s
Thread-1, 通过了第2个障碍物, 耗时 1.262s
Thread-2, 通过了第2个障碍物, 耗时 2.287s

在这个场景中实现的功能是定义了四个线程,在每个线程的run方法中执行一个for循环,这个for循环体循环三次,在每一次循环中都调用barrier.await进行拦截当前线程执行,阻塞到栅栏处。代码并不复杂,通过看最终的执行结果也能够清晰地看出其执行规律,四个线程在每次跨越栅栏的顺序是不一定的,但是三个栅栏跨越的时机顺序是固定的,只有通过第一个栅栏、第一个栅栏才能通过第二个栅栏,这样一批线程在局部执行无序地、整体跨越栅栏有序的在工作。

三、源码分析

CyclicBarrier的实现其实也并不复杂。它的源码当中可以发现其核心功能是使用ReentrantLock和Condition组合进行实现的。

首先我们看其构造方法。

 public CyclicBarrier(int parties) {
        this(parties, null);
    }
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
​
    // 可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    // Condition条件等待队列
    private final Condition trip = lock.newCondition();
    // 线程数量
    private final int parties;
    // 换代前执行的任务
    private final Runnable barrierCommand;
    //表示每一个栅栏的当前代
    private Generation generation = new Generation();

构造方法首先判断parties的合法性,如果不大于零,就直接抛出异常,然后赋值成员变量,parties表示线程数量,count用来计数,初始值也是parties,barrierCommand表示换代前执行的任务,通过构造方法可以指定。除了这些成员变量还定义了ReentrantLock可重入锁、lock.newCondition Condition条件等待队列、generation每一个栅栏的当前代。

接下来barrier.getParties()方法很简单是用来获取线程数量parties。

然后最重要的是barrier.await()方法,这个方法用来拦截当前线程。让其在栅栏处等待。

await方法直接调用dowait方法,传入参数false,0L表示没有设置等待时间,如果该线程没有被唤醒或者响应中断异常将会一直处于等待状态。

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // 不会发生
        }
    }

doWait方法源码如下:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException, TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //创建当前generation代
            final Generation g = generation;
            //当前栅栏是否被打翻
            if (g.broken)
                //如果当前栅栏被踢翻,直接抛出异常
                throw new BrokenBarrierException();
            //如果线程被中断过,踢翻当前栅栏,直接抛出异常
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            //计数器减1
            int index = --count;
            //如果计数器减1之后为0则需唤醒所有线程并转换到下一代
            if (index == 0) {  
                //标志是否换代之前执行了command任务
                boolean ranAction = false;
                try {
                    //执行command任务
                    //唤醒所有线程前先执行指定的任务
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //执行command任务之后进行换代
                    nextGeneration();
                    //执行command任务之后进行换代完成返回0
                    return 0;
                } finally {
                    if (!ranAction)
                        //如果ranAction仍然为false,就踢翻栅栏。
                        breakBarrier();
                }
            }
​
            // 循环直到唤醒,中断或超时
            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();
        }
    }

可以看到在dowait方法中首先会创建当前generation代,然后对当前栅栏是否被打翻进行判断,如果这个时候栅栏被踢翻,就直接抛出异常,然后判断线程是否被中断过,如果中断过就直接抛出异常,然后接下来分为两部分:

①、将计数器count减1,减完后进行判断是否等于0,如果等于0的话就会先去执行之前指定好的command任务,执行完之后再调用nextGeneration方法将栅栏进行换代,在换代方法中会将所有线程唤醒,将计数器的值重新设为parties,最后会重新设置栅栏代次,就进入了下一代的执行周期。

②、如果计数器不等于0的话就进入for循环,根据时间参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,如果调用awaitNanos,意味着当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。若指定时间内被signal()或signalALL()唤醒则返回nanosTimeout减去已经等待的时间,就是相对于超时时间的剩余时间;若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。如果调用await使当前线程加入await() 等待队列中,并释放锁,当其他线程调用signal()会重新请求锁。如果在等待过程中当前线程被中断就会执行breakBarrier方法,这也就意味着如果在等待过程中任一线程被中断,则当前代执行就结束了,所有之前被阻塞的线程都会被唤醒。线程唤醒后会进行三个判断:

①、是否因为breakBarrier方法踢翻栅栏被唤醒,如果是则抛出异常;

②、是否是正常的换代操作而被唤醒,如果是则返回计数器的值;

③、是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。

如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。

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

CyclicBarrier对外提供的方法不多,有些方法只是在内部使用,我们看下reset方法,其他方法都很好理解。

源码如下:

 public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 踢翻栅栏
            breakBarrier();  
            // 换代操作  
            nextGeneration(); 
        } finally {
            lock.unlock();
        }
    }
​
    private void breakBarrier() {
        // 设置当前代为broken
        generation.broken = true;
        // 重置计数器
        count = parties;
        // 唤醒所有等待线程
        trip.signalAll();
    }
​
    private void nextGeneration() {
        // 唤醒所有等待线程
        trip.signalAll();
        // 重置计数器
        count = parties;
        // 重新设置当前代
        generation = new Generation();
    }

相关注释都已经写的很清楚。reset主要就两部分操作:踢翻栅栏和更新当前代操作。

如果我们对之前的示例进行稍微改造下,我们再看其执行过程。

public class TestCyclicBarrier {
​
    public static void main(String[] args) {
​
        CyclicBarrier barrier = new CyclicBarrier(4);
        for (int i = 0; i < barrier.getParties(); i++) {
            new Thread(() -> {
                for (int j = 0; j < 3; j++) {
                    try {
                        int a = new Random().nextInt((3000 - 1000) + 1) + 1000;
                        Thread.sleep(a);
                        System.out.println(Thread.currentThread().getName() + "通过了第" + j + "个障碍物, 耗时 " + ((double) a / 1000) + "s");
                        if (j == 0) {
                            barrier.reset();
                        }
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        System.out.println("===============main 方法执行完毕===============");
    }
​
}

我们在第一次线程成批执行的时候多了个重置操作。这个时候执行结果如下:

===============main 方法执行完毕===============
Thread-1通过了第0个障碍物, 耗时 1.696s
Thread-3通过了第0个障碍物, 耗时 1.773s
java.util.concurrent.BrokenBarrierException
  at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
  at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
  at com.cloud.web.concurrentprogrammer.cyclicbarrier.TestCyclicBarrier.lambda$main$0(TestCyclicBarrier.java:27)
  at java.lang.Thread.run(Thread.java:748)
Thread-0通过了第0个障碍物, 耗时 2.153s
java.util.concurrent.BrokenBarrierException
  at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
  at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
  at com.cloud.web.concurrentprogrammer.cyclicbarrier.TestCyclicBarrier.lambda$main$0(TestCyclicBarrier.java:27)
  at java.lang.Thread.run(Thread.java:748)
Thread-2通过了第0个障碍物, 耗时 2.344s
java.util.concurrent.BrokenBarrierException
  at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
  at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
  at com.cloud.web.concurrentprogrammer.cyclicbarrier.TestCyclicBarrier.lambda$main$0(TestCyclicBarrier.java:27)
  at java.lang.Thread.run(Thread.java:748)
Thread-0通过了第1个障碍物, 耗时 1.849s
Thread-3通过了第1个障碍物, 耗时 2.522s
Thread-1通过了第1个障碍物, 耗时 2.929s
Thread-1通过了第2个障碍物, 耗时 1.852s
Thread-3通过了第2个障碍物, 耗时 2.424s
Thread-0通过了第2个障碍物, 耗时 2.64s
Thread-2通过了第1个障碍物, 耗时 2.808s
Thread-2通过了第2个障碍物, 耗时 2.507s

这个时候确实会出现异常,这是因为我们在创建CyclicBarrier实例的时候已经创建了一个Generation当前代,但是第一个线程执行的时候执行到reset方法,踢翻了栅栏,设置栅栏状态为broken,然后唤醒所有线程,又重新创建了新一代。当第二个线程执行到reset方法的时候,和第一个线程同样的过程,这个时候第二个线程在踢翻栅栏唤醒所有等待线程的时候会把第一个线程唤醒,第一个线程被唤醒后执行后面的操作(doWait方法中✨标注),这个时候进行判断是因为什么原因被唤醒的,发现是被踢翻栅栏唤醒,这个时候就会执行doWait方法249行代码抛出异常,后续线程执行过程类似。

四、CyclicBarrier图示

我们感受了CyclicBarrier的执行功能,由此也分析了其源码实现,接下来最后我们以CyclicBarrier在本例中的执行图示作为结束。

 

更多内容持续更新中,感兴趣的朋友请移步至个人公众号,谢谢支持😜😜......

公众号:wenyixicodedog

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值