在本文中,我们将了解如何使用 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 对象可以帮助您实现目标。