Java中CountDownLatch教程

在本文中,我们将了解如何使用 Java CountDownLatch 编写考虑并发性的测试用例。

Java CountDownLatch自版本 1.5 起就可用,它是java.util.concurrent包含许多其他与线程相关的实用程序的包的一部分。

什么是Java CountDownLatch 类
Java CountDownLatch 类最重要的方法是await和countDown方法。

  • await方法用于暂停当前线程的执行,直到CountDownLatch的计数器达到值0或者线程被中断。
  • countDown方法用于减少内部计数器的值。

因此,Java CountDownLatch 类的目标是控制等待 CountDownLatch 的一个或多个线程恢复执行的时刻。

协调 Java 线程
假设我们有以下示例,其中我们从执行主线程启动 5 个工作线程:

@Test
public void testNoCoordination() {
    LOGGER.info("Main thread starts");

    int workerThreadCount = 5;

    for (int i = 1; i <= workerThreadCount; i++) {
        String threadId = String.valueOf(i);
        new Thread(
            () -> LOGGER.info(
               
"Worker thread {} runs",
                threadId
            ),
           
"Thread-" + threadId
        ).start();
    }

    OGGER.info(
"Main thread finishes");
}

运行上述测试用例时,我们得到以下输出:

[main]: Main thread starts
[main]: Main thread finishes
[Thread-4]: Worker thread 4 runs
[Thread-1]: Worker thread 1 runs
[Thread-5]: Worker thread 5 runs
[Thread-3]: Worker thread 3 runs
[Thread-2]: Worker thread 2 runs

主线程在工作线程执行之前启动和完成,如果我们想要运行一些断言来验证工作线程执行的结果,这可能会出现问题。

因此,我们需要协调主线程和工作线程,让主线程等待工作线程完成后再完成自己的执行。

为此,我们将使用 CountDownLatch 来协调主线程和工作线程:

@Test
public void testCountDownLatch() throws InterruptedException {
    LOGGER.info("Main thread starts");

    int workerThreadCount = 5;

    CountDownLatch endLatch = new CountDownLatch(workerThreadCount);

    for (int i = 1; i <= workerThreadCount; i++) {
        String threadId = String.valueOf(i);
        new Thread(
            () -> {
                LOGGER.info(
                   
"Worker thread {} runs",
                    threadId
                );

                endLatch.countDown();
            },
           
"Thread-" + threadId
        ).start();
    }

    LOGGER.info(
"Main thread waits for the worker threads to finish");

    endLatch.await();

    LOGGER.info(
"Main thread finishes");
}

它endLatch是使用与工作线程数匹配的计数器值创建的。

主线程将等待endLatch,并且只有当计数器值指示 的值时才会恢复执行0。

因为每个工作线程都会使用方法调用来减少计数器值countDown,所以在所有工作线程完成后,计数器将达到 的值0,主线程将恢复执行。

执行testCountDownLatch测试用例时,我们可以看到 CountDownLatch 按预期工作:

[main]: Main thread starts
[main]: Main thread waits for the worker threads to finish
[Thread-1]: Worker thread 1 runs
[Thread-3]: Worker thread 3 runs
[Thread-2]: Worker thread 2 runs
[Thread-5]: Worker thread 5 runs
[Thread-4]: Worker thread 4 runs
[main]: Main thread finishes

这次,主线程等待工作线程完成后再结束自己的执行。

一个现实生活中的例子
要了解可以在哪里使用 Java CountDownLatch,请考虑我之前有关竞争条件的文章中的集成测试:

@Test
public void testParallelExecution() {
    assertEquals(10L, getAccountBalance("Alice-123"));
    assertEquals(0L, getAccountBalance(
"Bob-456"));

    int threadCount = threadCount();

    CountDownLatch startLatch = new CountDownLatch(1);
    CountDownLatch endLatch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        new Thread(() -> {
            awaitOnLatch(startLatch);

            transfer(
"Alice-123", "Bob-456", 5L);

            endLatch.countDown();
        }).start();
    }

    LOGGER.info(
"Starting threads");
    startLatch.countDown();
    awaitOnLatch(endLatch);

    LOGGER.info(
"Alice's balance: {}", getAccountBalance("Alice-123"));
    LOGGER.info(
"Bob's balance: {}", getAccountBalance("Bob-456"));
}

这次,我们使用两个 CountDownLatch 对象:
  • startLatch– 立即启动工作线程
  • endLatch– 通知主线程工作线程已完成处理

在构建startLatch对象时,我们向 CountDownLatch 构造函数提供计数器值1,因为在创建工作线程后,主线程将对其值进行倒计时。

工作线程开始运行后所做的第一件事就是等待startLatch. 这样,工作线程将立即继续执行其任务,从而增加争用的可能性,这正是我们在竞争条件测试用例中的目标。

它endLatch使用与我们正在创建的工作线程数量相匹配的计数器值,因为每个线程在完成其处理任务后都会对该值进行倒计时。

工作线程创建并启动后,主线程将倒计时startLatch,使所有工作线程恢复执行。

启动工作线程后,主线程暂停执行并开始等待endLatch所有工作线程完成。

所有工作线程完成后,endLatch计数器达到 的值0,主线程恢复执行并打印帐户余额。

这就是 CountDownLatch 非常有用的原因,因为它允许主线程在所有工作线程完成处理后打印帐户余额。

结论
当我们必须协调线程执行以断言其执行结果时,Java CountDownLatch 会非常有用。

因此,如果您想测试数据访问逻辑在并发环境中的工作方式,CountDownLatch 对象可以帮助您实现目标。