概述
在本文中,我们将学习如何利用Java中的CountDownLatch工具类来编写考虑并发情况的测试用例。
Java从1.5版本开始就提供了CountDownLatch这个工具,它属于java.util.concurrent包的一部分。这个包里包含了许多与线程处理相关的实用工具。
通俗点来说,你可以把Java的CountDownLatch想象成一个```计数器```和```门锁```的结合体。在一个多线程环境中,它允许一个或多个线程等待其他线程完成特定数量的任务之后才能继续执行。比如,在一场接力赛中,所有队员都跑完自己的那一段(即完成任务)后,裁判才会打开终点的大门(启动后续操作)。
所以在编写需要考虑并发控制的程序时,特别是在进行单元测试时,CountDownLatch就是一个非常有用的工具,可以帮助我们更好地同步和协调各个线程的行为。
Java中的CountDownLatch类提供了以下关键方法:
最重要的两个方法是await
和countDown
。
await
方法就好比是一个“暂停键”,它会让当前线程暂时停止运行,直到CountDownLatch的计数器数值达到0,或者线程被中断。也就是说,这个线程会一直等待,直到得到其他线程发出的“许可信号”。countDown
方法则扮演了“倒计时”的角色,每次调用都会使内部计数器的值减一。
所以,Java CountDownLatch类的主要目标是用来精准控制那些正在等待CountDownLatch的一个或多个线程何时恢复执行。想象一下,如果多个线程在做各自的任务,而我们需要它们都完成之后再一起开始下一步操作,这时就可以借助CountDownLatch来实现同步协调。当所有任务完成并调用相应次数的countDown
后,等待的线程就会通过await
解除阻塞,继续执行后续代码。
如何使用
我们先启动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();
}
⠀
LOGGER.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
在Java编程中,假设主线程启动了几个工作线程后就结束了自身的执行。
这就像是一个项目经理给团队分配完任务后立刻离开了办公室,还没等团队完成任务他就走了。如果项目经理需要验证团队成员的工作成果(即运行针对工作线程执行结果的断言),那么这种情况下就会出现问题。
因此,我们需要一种方式来协调主线程和工作线程,确保主线程在结束自己之前,等待所有工作线程都执行完毕。就好比说,项目经理应该等到所有团队成员都报告任务完成后再离开,这样才能全面检查并确认所有任务是否按预期完成。
在Java中,我们可以使用```CountDownLatch```这样的并发工具类实现这个目标。
主线程创建一个CountDownLatch对象,并初始化计数器为工作线程的数量;每个工作线程在开始时“计数”一次(调用countDown方法),当所有工作线程都完成任务并“计数”后,主线程通过await方法阻塞等待,直到计数器归零,这时主线程才会继续执行后续的验证逻辑。
调整代码
@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的计数器,并将其初始值设置为工作线程的数量。这就像是在公园入口处放置了一个签到簿,上面记录了有多少个小组要完成任务后回来。
主线程就像是公园管理员,它会在入口处等待所有小组(即工作线程)都完成任务并“签到”。只有当这个签到簿上的数字归零时,管理员才会继续进行后续的工作。
每个工作线程在完成自己的任务后,都会调用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:用于一次性启动所有工作线程。当我们创建startLatch对象时,将其计数值设置为1,这是因为只由主线程在创建工作线程后进行计数器减一的操作。
这就好比是马拉松比赛的起跑信号枪,只有裁判员(主线程)发令后,所有运动员(工作线程)才能同时开始跑步(执行任务)。通过这种方式,确保所有工作线程几乎同时开始处理任务,从而提高产生竞态条件的可能性,这是我们在测试中所期望达到的效果。
endLatch:用于通知主线程所有工作线程已完成处理任务。这个计数器的初始值等于我们要创建的工作线程数量,因为每个工作线程完成自身任务后都会调用countDown方法减少该计数器的值。
当工作线程创建并启动后,主线程会先对startLatch进行计数器减一操作,这样一来,所有等待在startLatch上的工作线程将同时恢复执行。
启动工作线程后,主线程暂停自身的执行,并开始等待endLatch,即等待所有工作线程完成任务。
当所有工作线程都完成各自的任务后,endLatch的计数值将变为0,此时主线程会恢复执行,并打印出账户余额等结果数据。
这就是为什么CountDownLatch非常有用,因为它使得主线程能够在所有工作线程完成任务处理之后再进行下一步操作,比如打印结果或进行验证等。
总结
总结来说,当我们在多线程环境中需要协调各个线程的执行顺序以验证其执行结果时,Java CountDownLatch工具就显得非常实用。