FunTester 【连载 10】CountDownLatch

FunTester · 2025年01月09日 · 804 次阅读

2.5 CountDownLatch

前两个synchronizedReentrantLock都是解决线程安全问题的好手,就像两把宝剑,可以披荆斩棘大杀四方。下面我们来探索java.util.concurrent包下面解决线程同步问题的功能类。

在使用多线程进行性能测试的过程中,经常需要基于事件、时间点进行线程的同步。例如我们整点抢红包场景、前置数据并发初始化等。我们需要所有线程都到达某一个关键点之后,再进行下一步。在流程类的测试场景中,这种需求尤为常见。

要解决这类问题或者说实现此类需求,我们必然会用到线程同步类。CountDownLatch 是一个相对简单同步工具类,可以实现让一个或多个线程等待直到在其他线程中执行的操作完成后再继续执行。如果你觉得不太好理解,类似的概念就是 JMeter 中的集合点,其含义就是所有线程都到达集合点集合一下,然后再各走各路。CountDownLatch 适合用于一个或多个线程等待其他一组线程完成工作后再继续执行的场景。

CountDownLatch是通过线程安全的计数器数值判断是否到达集合点,工作流程如下:

(1)首先创建同步对象,并且设置同步数量
(2)任务线程执行任务,完成之后将计数器值减一
(3)等待线程(通常是 main 线程)会阻塞(可以设置超时),直到计数器归零,继续执行后面代码

2.5.1 基础方法

CountDownLatch 的构造方法如下:


    public CountDownLatch(int count) {

        if (count < 0) throw new IllegalArgumentException("count < 0");

        this.sync = new Sync(count);

    }

这个方法只有一个 int 参数 count,如果 count 小于 0,会抛出异常。这个 count 就是需要同步的数量,对应CountDownLatch工作流的第 1 步。

CountDownLatch工作流第 2 步,用到的计数器减一的方法如下:

    public void countDown() {

        sync.releaseShared(1);

    }

方法没有参数,直接调用即可,通常会跟try-catch-finally同时使用,以保障每一个任务线程都会执行countDown()方法。

CountDownLatch 工作流第 3 步,对应 2 个方法,方法一:

    public void await() throws InterruptedException {

        sync.acquireSharedInterruptibly(1);

    }

方法二:

    public boolean await(long timeout, TimeUnit unit)

        throws InterruptedException {

        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));

    }

方法一可以看做方法二的无限等待版本。

当等待同步的线程执行到这个方法,会阻塞继续执行,直到CountDownLatch计数器归零或者等待超时。一般来说,不建议新手使用某个无线等待的阻塞方法,但在 Java 性能测试最佳实战中,笔者会推荐使用方法一。原因有两点:一是CountDownLatch通常是用在前置或者后置数据处理,并发执行时间无法准确估计;二是我们可以通过框架功能设计,规避CountDownLatch真发生无限等待异常场景,还可以更加灵活控制集合时间。

2.5.2 最佳实战

CountDownLatch 方法较少,使用流程简单,通过下面这个例子,展示CountDownLatch使用最佳实战。

package org.funtester.performance.books.chapter02.section5;

import java.util.concurrent.CountDownLatch;

/**

 * CountDownLatch示例

 */

public class CountDownLatchDemo {

    public static void main(String[] args) {

        CountDownLatch countDownLatch = new CountDownLatch(3);// 创建一个CountDownLatch实例

        for (int i = 0; i < 3; i++) {// 创建3个线程

            new Thread(() -> {// 创建一个线程

                try {

                    Thread.sleep(100);// 睡眠100毫秒

                } catch (InterruptedException e) {

                    throw new RuntimeException(e);

                } finally {

                    countDownLatch.countDown();// 计数器减1

                }

                System.out.println(System.currentTimeMillis() + "  任务完成  " + Thread.currentThread().getName());// 打印日志

            }).start();// 启动线程

        }

        System.out.println(System.currentTimeMillis() + "  等待任务完成  " + Thread.currentThread().getName());// 打印日志

        try {

            countDownLatch.await();// 等待计数器归零

        } catch (InterruptedException e) {

            throw new RuntimeException(e);

        }

        System.out.println(System.currentTimeMillis() + "  等待结束  " + Thread.currentThread().getName());// 打印日志

    }

}

在上面的示例中,首先创建了同步数量为 3 的 CountDownLatch 对象,接着创建了 3 个线程,每个线程下面 100 毫秒,然后计数器减一,打印任务完成日志。下面是控制台输出:

1698497720691  等待任务完成  main

1698497720791  任务完成  Thread-0

1698497720791  任务完成  Thread-2

1698497720791  任务完成  Thread-1

1698497720791  等待结束  main

可以看到,main线程在到达await()方法后,阻塞了 100 毫秒后,3 个任务均完成,计数器归零,main线程执行了打印等待结束的代码。

2.5.3 使用场景

CountDownLatch常用的使用场景如下:

  • 线程等待。使用CountDownLatch可以让一个或者多个线程一直等待,直到其他线程均完成预定的任务。例如:在并发初始化前置数据场景。
  • 发送起止信号。使用CountDownLatch功能可以给一组线程发送信号,启动或者结束改组线程。例如:整点抢红包场景。

在使用场景中,CountDownLatchjava.lang.Thread#sleep(long)方法有部分重合,有些场景甚至相互替代。这里笔者建议在多线程场景中尽量少使用java.lang.Thread#sleep(long)方法,相比之下,CountDownLatch有一下几点优势:

  • 更加精准控制线程同步时机。CountDownLatch 可以精确控制等待的线程数目,而 sleep 只能通过轮询来实现等待,容易出现错误。
  • 更好的性能。CountDownLatch 可以使线程处于等待状态而不是占用 CPU 时间片,sleep 会导致线程不停醒来循环等待。
  • 更优雅退出。CountDownLatch 通过 await() 设置超时控制退出等待,而sleep不行;CountDownLatch一旦计数器归零,程序会立即退出等待,而sleep必须等到sheep结束才行。
  • 使用更优雅。CountDownLatch的接口简单直接,而 sleep 需要估算时间,设置不当容易造成浪费。
  • 代码可读性强。CountDownLatch同步逻辑简单易懂,而 sleep 方法若无注释很难理解。

因此在需要等待其他线程完成的场景下,CountDownLatch是一个更加简单、可靠、安全的选择。

人无完人,类无全能。下面说一下CountDownLatch的缺点:

  • 无法复用。CountDownLatch对象创建好之后,就无法重置计数器,无法复用对象。
  • 不够灵活。CountDownLatch对象创建好之后,就无法增加计数器数值,只能调用方法进行减一,无法应对复杂多线程场景。

总而言之,CountDownLatch 主要应用于一次性的简单等待场景。对于复杂的多线程协调还需要其他更高级的同步工具。

书的名字:从 Java 开始做性能测试

如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

FunTester 原创精华

【连载】从 Java 开始性能测试

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册