百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

最常用的CountDownLatch, CyclicBarrier你知道多少?(必会知识)

cac55 2024-09-19 17:03 37 浏览 0 评论

CountdownLatch,CyclicBarrier是非常常用并发工具类,可以说是Java工程师必会技能了。不但在项目实战中经常涉及,而且在编写压测程序,多线程demo也是必不可少,所以掌握它们的用法和实现原理非常有必要。

等待多线程完成的CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。也就是说通过使用CountDownLatch工具类,可以让一组线程等待彼此执行完毕后在共同执行下一个操作。具体流程如下图所示,箭头表示任务,矩形表示栅栏,当三个任务都到达栅栏时,栅栏后wait的任务才开始执行。

CountDownLatch维护有个int型的状态码,每次调用countDown时状态值就会减1;调用wait方法的线程会阻塞,直到状态码为0时才会继续执行。

在多线程协同工作时,可能需要等待其他线程执行完毕之后,主线程才接着往下执行。首先我们可能会想到使用线程的join方法(调用join方法的线程优先执行,该线程执行完毕后才会执行其他线程),显然这是可以完成的。

使用Thread.join()方法实现


public class RunningRaceTest {
    public static void main(String[] args) throws InterruptedException {
        Thread runner1 = new Thread(new Runner(), "1号");
        Thread runner2 = new Thread(new Runner(), "2号");
        Thread runner3 = new Thread(new Runner(), "3号");
        Thread runner4 = new Thread(new Runner(), "4号");
        Thread runner5 = new Thread(new Runner(), "5号");
        runner1.start();
        runner2.start();
        runner3.start();
        runner4.start();
        runner5.start();

        runner1.join();
        runner2.join();
        runner3.join();
        runner4.join();
        runner5.join();

        // 裁判等待5名选手准备完毕
        System.out.println("裁判:比赛开始~~");
    }
}

class Runner implements Runnable {
    @Override
    public void run() {
        try {
            int sleepMills = ThreadLocalRandom.current().nextInt(1000);
            Thread.sleep(sleepMills);
            System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Thread.join()完全可以实现这个需求,不过存在一个问题,如果调用join的线程一直存活,则当前线程则需要一直等待。这显然不够灵活,并且当前线程可能会出现死等的情况。

更加灵活的CountDownLatch

jdk1.5之后的并发包中提供了CountDownLatch并发工具了,也可以实现join的功能,并且功能更加强大。

// 参赛选手线程
class Runner implements Runnable {
    private CountDownLatch countdownLatch;
    
    public Runner(CountDownLatch countdownLatch) {
        this.countdownLatch = countdownLatch;
    }

    @Override
    public void run() {
        try {
            int sleepMills = ThreadLocalRandom.current().nextInt(1000);
            Thread.sleep(sleepMills);
            System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 准备完毕,举手示意
            countdownLatch.countDown();
        }
    }
}

public class RunningRaceTest {
    public static void main(String[] args) throws InterruptedException {
        // 使用线程池的正确姿势
        int size = 5;
        AtomicInteger counter = new AtomicInteger();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(size, size, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), (r) -> new Thread(r, counter.addAndGet(1) + " 号 "), new ThreadPoolExecutor.AbortPolicy());
        
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < size; i++) {
            threadPoolExecutor.submit(new Runner(countDownLatch));
        }

        // 裁判等待5名选手准备完毕
        countDownLatch.await(); // 为了避免死等,也可以添加超时时间
        System.out.println("裁判:比赛开始~~");

        threadPoolExecutor.shutdownNow();
    }
}

输出结果:

5 号  选手已就位, 准备共用时: 20ms
4 号  选手已就位, 准备共用时: 156ms
1 号  选手已就位, 准备共用时: 288ms
2 号  选手已就位, 准备共用时: 519ms
3 号  选手已就位, 准备共用时: 945ms
比赛开始~~


同步屏障CyclicBarrier

CyclicBarrier可以实现CountDownLatch一样的功能,不同的是CountDownLatch属于一次性对象,声明后只能使用一次,而CyclicBarrier可以循环使用

从字面意义上来看,CyclicBarrier表示循环的屏障,当一组线程全部都到达屏障时,屏障才会被移除,否则只能阻塞在屏障处。

public class RunningRace {
    public static void main(String[] args) {
        // 使用线程池的正确姿势
        int size = 5;
        AtomicInteger counter = new AtomicInteger();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(size, size, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), (r) -> new Thread(r, counter.addAndGet(1) + " 号 "), new ThreadPoolExecutor.AbortPolicy());

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("裁判:比赛开始~~"));
        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.submit(new Runner(cyclicBarrier));
        }
    }
}

class Runner implements Runnable {
    private CyclicBarrier cyclicBarrier;

    public Runner(CyclicBarrier countdownLatch) {
        this.cyclicBarrier = countdownLatch;
    }

    @Override
    public void run() {
        try {
            int sleepMills = ThreadLocalRandom.current().nextInt(1000);
            Thread.sleep(sleepMills);
            System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
            cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

由于CyclicBarrier可以循环使用,所以CyclicBarrier的构造方法中可以传入一个Runnable参数,在每一轮执行完毕之后就会立刻执行这个Runnable任务

CountDownLatch设计与实现

CountDownLath是基于AQS框架的一种简单实现,有两个核心的方法,即await()和countDown(),通过构造方法传入一个状态值,调用await()方法时线程会阻塞,直到状态码被修改成0时才会返回,每次调用countDown()时会将状态值减1。


wait方法:执行wait方法后,会尝试获取同步状态,如果为状态为0则方法继续执行,否择当前线程会被加入到同步队列中,详情可见笔者关于AQS的两篇文章。

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果状态码不为0,尝试获取同步状态,如果失败则被加入到同步队列中
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// 当状态码为0时返回1,否择返回-1,这个方法中参数没有任何用处
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

countDown方法:每次执行countDown方法时,会将状态码的值减1.

public void countDown() {
    sync.releaseShared(1);
}


CyclicBarrier的设计与实现

CyclicBarrier与CountDownLatch实现思想相同,也是基于AQS框架实现。不同的是CyclicBarrier内部维护一个状态值借助基于AQS实现的锁ReentrantLock来实现状态值的同步更新,以及AQS除了同步状态之外的另一个核心概念条件队列来完成线程的阻塞。

parties: 和CountdownLatch中的状态值一样,用来记录每次要相互等待的线程数量,只有parties个线程同时到达屏障时,才会唤醒阻塞的线程。

count临时计数器: 由于CyclicBarrier是可以循环使用的,count可以理解为是一个临时变量,每一轮执行完毕或者被打断都会重置count为parties值。

Generation内部类: 只有一个属性 broken表示当前这一轮执行是否被中断,如果被中断后其他线程再执行await方法会抛出异常(目的是停止本轮线程未执行线程的继续执行)。

await方法: 当执行await方法时,会同步得对内部的count执行--count操作, 如果count = 0,则执行barrierCommand任务(通过构造方法传来的Runnable参数)。

reset方法:中断本轮执行,重置count值,唤醒等待的线程然后开始下一轮,此时本轮正在执行的线程调用await方法会抛出异常。

// await方法实际执行的代码
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();
        }
        // 本轮执行的计数器 数值-1
        int index = --count;
        if (index == 0) {  // 计数器值=1, 本轮线程全部到达屏障,执行barrierCommand任务
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                nextGeneration();// 唤醒所有等待在条件队列上的任务
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // 如果状态不等于0,循环等待直到计数器值为0,本轮执行被打破,线程被中断,或者等待超时
        for (;;) {
            try {
                if (!timed)
                    // 状态码不为0,将当前线程加入到条件队列中,进入阻塞状态
                    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();// 唤醒所有条件队列中的线程,重置count的值
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

重置栅栏的状态

public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}
/**
 * Sets current barrier generation as broken and wakes up everyone.
 * Called only while holding lock.
 */
private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}

当一轮执行完毕之后,既count=0后,CyclicBarrier的临时状态会重置为parties

/**
 * 进入下一轮
 * 唤醒所有等待线程,充值count
 */
private void nextGeneration() {
    // signal completion of last generation
    trip.signalAll();
    // set up next generation
    count = parties;
    generation = new Generation();
}

总结

  1. CountDownLatch创建后只能使用一次,而CyclicBarrier可以循环使用,并且CyclicBarrier功能更完善。
  2. CountDownLatch内部的状态是基于AQS中的状态信息,而CyclicBarrier中的状态值是单独维护的,使用ReentrantLock加锁保证并发修改状态值的数据一致性。
  3. 它们的使用场景:允许一个或多个线程等待其他线程完成操作, 即当指定数量线程执行完某个操作再继续执行下一个操作。

笔者是95后java工程师,专注后端技术,小伙伴本可以关注wx公众号cruder,一起探讨学习和工作,一起进步!

相关推荐

苹果新macOS、新Mac还没出,但已经有新版虚拟机软件Parallels Desktop 19

自从苹果电脑全面转向ARM架构芯片之后,想在新款Mac电脑上安装Windows或Linux系统,就只能依靠虚拟机软件了,其中ParallelsDesktop应该是比较多Mac用户选择使用的一款,现在...

这个开源神器可快速帮你安装 MacOS 虚拟机

大家好,我是JackTian。安装Windows和Linux操作系统是最熟悉不过的必备技能了。那么,给大家推荐一个非常实用的开源脚本:macos-guest-virtualbox.sh,帮你...

如何在VMware虚拟机上安装运行Mac OS系统??

想在自己的Windows电脑上安装一个MacOS体验一下苹果系统的小伙伴,教程来了!!!一、安装前准备虚拟机运行软件:VMwareWorkstationPro,版本:16.0.0。(可以注册)VM...

效率!MacOS下超级好用的Linux虚拟工具:Lima

对于MacOS用户来说,搭建Linux虚拟环境一直是件让人头疼的事。无论是VirtualBox还是商业的VMware,都显得过于笨重且配置复杂。今天,我们要介绍一个轻巧方便的纯命令行Linux虚拟工具...

普通电脑安装苹果MacOS+Windows10双系统,这次可不是虚拟机

上篇文章中说到,有一朋友因为工作需要,得临时使用苹果系统,笔者给他用VmwareWorkStation安装了一个苹果系统的虚拟机,结果装是装上了,但是发现调整分辨率有点小问题,文件传输也不方便。虽说...

官方证实苹果M1芯片不支持Windows 11

中关村在线消息:近日根据微软官方透露,目前已经确定Windows11不支持运行在苹果M1芯片上,这意味着过往在Mac电脑上安装Windows系统的做法在M1芯片的Mac电脑上并不适用。不过此前有网友...

这可能是 Mac 共享文件最详细的教程了

如果希望让一台Mac访问另一台Mac上的文件,就可以使用Mac的文件共享功能。而且不仅是Mac之间,甚至用iPhone、iPad、WindowsPC都可以访问Mac的共享文件...

在 M1/M2 Mac 上,让 Windows 11 免费“跑”起来

自从苹果在产品中逐步使用自研的M系列芯片淘汰掉英特尔芯片之后,很多事情都发生了改变。作者|KirkMcElhearn和JoshuaLong译者|弯月出品|CSDN(ID:CS...

VMware Workstation克隆虚拟机后修改ip地址和mac地址

VMwareWorkstation克隆虚拟机,登录之后发现,克隆虚拟机不仅用户名相同,连ip地址、mac地址也是相同的,很显然访问相同ip地址的虚拟机是会出现ip地址冲突的。一、修改IP地址这就需要...

VirtualBox7中安装macOS big sur,在windows10&amp;11上「保姆级教程」

macOSBigSur是苹果公司研发的桌面端操作系统,于北京时间2020年6月23日在2020苹果全球开发者大会上发布。BigSur采用全新的精美设计,为主要app如Safari浏览器...

最强mac虚拟机Parallels Desktop 16 有哪些重要的新增功能?

ParallelsDesktop16正式发布,软件带来了一些显着的新功能和性能增强,包括对macOSBigSur的全面支持。当苹果推出macOSBigSur时,它终止了对Par...

关于在MacOS安装虚拟机的全过程(macos 安装虚拟机)

哈喽大家好,我是咕噜美乐蒂,很高兴又见面啦!下面美乐蒂将详细地给大家介绍一下在macOS上使用VMwareFusion创建虚拟机并安装操作系统的步骤:一、确认虚拟化支持:首先,确认你的Ma...

macOS上也能轻松运行Win系统的虚拟机,你还不知道吗?

在macOS系统上运行Win系统的方式,虚拟机篇吉安光头强原创你是否曾经为了在Mac上运行Windows系统而烦恼不用着急,下面我将分享一种简单易行的方法,让你轻松在Mac上运行Windows系统准备...

Mac M芯片上安装统信UOS 1070arm64虚拟机

原文链接:MacM芯片上安装统信UOS1070arm64虚拟机Hello,大家好啊!今天给大家带来一篇关于如何在苹果M系列芯片的Mac电脑上,通过VMware安装ARM64版统信UOS1070...

虚拟机不好用?Mac mini 多配一台Windows电脑,用远程桌面更好!

最近新入手了MacminiM4款,这里来更新一下相关问题,对于还没有购买Macmini,但是又想要用苹果电脑的朋友,一些参考,我觉得还是挺有用的!Macmini选择哪个渠道购买好?现在比较划算...

取消回复欢迎 发表评论: