Java多线程六脉神剑-商阳剑-CyclicBarrier

547 阅读3分钟

前言

商阳剑:商阳剑巧妙灵活,CyclicBarrier 可以灵活地设置线程同步的条件和重复使用,如同商阳剑般变化多端。

CyclicBarrier直译过来就是“循环栅栏”,在Java并发编程中,用于控制一组线程,先到达栅栏的等待其他未到达的线程,都到达后再一起继续执行,跑到下一个栅栏,也是同样等待所有线程都到达栅栏再继续到下一个栅栏,以此往复。

image.png

CyclicBarrier内有一个计数器,在构造函数初始化时会初始计数器的值,当调用await方法时,计数器会减一并将当前线程进行阻塞,表示次线程已到达栅栏,等待其他线程全部执行完毕,也就是计数器的值为0时。

CyclicBarrier方法详解

  • CyclicBarrier(int parties):构造方法,其中parties表示计数器的数量(一般parties为线程数)
  • CyclicBarrier(int parties, Runnable barrierAction):构造方法,barrierAction表示当线程到达屏障后先执行一个预定义的动作,再进行阻塞等待。
  • int await():计数器减一,阻塞等待其他线程到达栅栏。返回的值表示当前线程在本次屏障等待中到达的序号,序号从0开始。
  • int await(long timeout, TimeUnit unit):添加了一个超时时间,如果未在给定的timeout时间内被栅栏放行,将会抛出InterruptedException的异常。
  • reset():重置计数器,重置后,如果有其他线程在await栅栏放行,在await的线程将会抛出BrokenBarrierException的异常。
  • boolean isBroken():检查CyclicBarrier是否处于破损状态。当以下情况发生时CyclicBarrier会进入破损状态:
    1. 某个等待的线程被中断。
    2. 某个等待的线程超时。
    3. 屏障动作(如果有指定)在执行过程中抛出异常。
  • int getNumberWaiting():获取正在await的数量。
  • int getParties():获取总数,也就是在构造方法中我们指定的parties值。

举个栗子🌰

在我们玩一些PVP游戏中,我们一般在开始游戏的时候都会有 匹配玩家->选择角色->加载进入 的过程,而每一个流程都需要所有玩家全部确认完成后才能继续,每一个过程相当于就是一个栅栏,拦住全部的玩家,直到所有的玩家完成才进行下一步。 比较经典的一张图:

GIF 2024-8-3 21-16-52.gif

代码实现:

public class CyclicBarrierCase {
    //匹配游戏->选择角色->加载进入
    public static void produce(){
        List<String> player = new ArrayList<>();
        player.add("玩家一");
        player.add("玩家二");
        player.add("玩家三");
        player.add("玩家四");
        CyclicBarrier barrier = new CyclicBarrier(player.size());
        for (int i = 0; i < player.size(); i++) {
            new Thread(() -> {
                String threadName = Thread.currentThread().getName();
                sleep(); //准备游戏时间
                System.out.println(threadName+"准备完成");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
                sleep(); //选择角色时间
                System.out.println(threadName + "选择完成");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
                sleep(); //加载进入时间
                System.out.println(threadName + "加载完成");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } catch (BrokenBarrierException e) {
                    throw new RuntimeException(e);
                }
            },player.get(i)+"线程").start();
        }
    }
    public static void sleep(){
        //睡1到3秒的随机数
        int random = RandomUtil.randomInt(1000, 3000);
        try {
            Thread.sleep(random);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        produce();
    }
}

执行结果:

GIF 2024-8-3 21-23-15.gif

await时的异常

调用await时,会让我们捕获两个异常,一个是BrokenBarrierException另一个是InterruptedException。

出现BrokenBarrierException这种情况可能是CyclicBarrier的某个线程在等待期间被中断,或者CyclicBarrier被重置。这通常意味着CyclicBarrier无法正常工作,处理的方案可能有捕获异常并记录日志,通过某种共享状态或消息机制通知其他线程CyclicBarrier已损坏,执行一些恢复操作来尽量弥补或减轻由于栅栏损坏导致的影响。

出现InterruptedException是因为在调用await(long timeout, TimeUnit unit)我们指定了超时时间,当指定时间过后,线程还没有被放行,抛出超时异常,处理超时的策略可能包括重试机制或者回退逻辑。但重要的是要确保所有的线程在超时后都能正确地处理这种情况,避免资源泄漏或者线程阻塞。