🚀 使用 CountDownLatch 实现并发压测(Java 实战详解)
在进行接口性能测试或系统压测时,我们经常需要模拟多个线程在同一时刻同时发起请求。如果线程一个一个启动,就无法体现真实的并发效果。因此,我们需要借助并发工具类——CountDownLatch
来实现统一起跑、统计耗时等功能。
本文将深入讲解如何使用 CountDownLatch
实现高并发压测模拟场景,并最终给出面试回答模板。
🔧 一、为什么选择 CountDownLatch?
CountDownLatch
是 Java 并发包(java.util.concurrent
)中的同步辅助类,它的设计初衷就是让一个或多个线程等待其他线程完成操作后再继续执行。
其常用的应用场景包括:
- 主线程等待多个子线程完成
- 模拟高并发访问系统
- 服务初始化完成再统一启动等场景
在并发压测中,我们使用它的两个功能:
- 统一起跑:所有线程等待
startLatch.await()
; - 主线程等待全部线程完成:
doneLatch.await()
。
📦 二、实战:模拟高并发压测接口
我们以“100个线程同时请求一个接口”为例,完整代码如下:
import java.util.concurrent.CountDownLatch;
public class ConcurrentTest {
private static final int THREAD_COUNT = 100;
public static void main(String[] args) throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1); // 控制所有线程统一起跑
CountDownLatch doneLatch = new CountDownLatch(THREAD_COUNT); // 控制主线程等待所有线程执行完毕
Runnable task = () -> {
try {
startLatch.await(); // 等待起跑信号
long start = System.currentTimeMillis();
doSomething(); // 模拟实际请求
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " 执行完毕,耗时:" + (end - start) + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
doneLatch.countDown(); // 通知主线程“我干完了”
}
};
// 启动线程
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(task, "线程-" + i).start();
}
// 模拟准备阶段
System.out.println("准备中...");
Thread.sleep(1000);
System.out.println("开始并发请求!");
long totalStart = System.currentTimeMillis();
startLatch.countDown(); // 所有线程统一起跑
doneLatch.await(); // 等待所有线程执行完毕
long totalEnd = System.currentTimeMillis();
System.out.println("所有请求完成,总耗时:" + (totalEnd - totalStart) + "ms");
}
private static void doSomething() throws InterruptedException {
Thread.sleep((long) (Math.random() * 100)); // 模拟接口处理时间
}
}
🧠 三、关键点详解
变量 | 功能 |
---|---|
startLatch |
控制所有线程 统一开始执行(类似发令枪) |
doneLatch |
控制主线程 等待所有线程完成工作 |
countDown() |
每个线程工作完成后调用一次,计数器减1 |
await() |
当前线程阻塞,直到计数器为0为止 |
这种机制特别适合压测场景,因为它保证了并发启动 + 整体统计,能更真实反映系统在高并发下的表现。
📈 四、实际输出效果示例
准备中...
开始并发请求!
线程-3 执行完毕,耗时:27ms
线程-1 执行完毕,耗时:33ms
...
所有请求完成,总耗时:132ms
可以看到,各个线程在“统一起跑”后几乎同时执行,并输出自己的耗时,最后主线程统计总耗时。
📚 五、常见问题与陷阱
1. CountDownLatch
一次性使用?
是的,CountDownLatch
一旦计数器归零就不能再复用了。要复用请用 CyclicBarrier
。
2. await()
和 countDown()
的调用顺序?
无所谓。await()
先调用也没关系,它会一直等到 countDown()
把计数器减到0。
3. countDown()
不会阻塞?
是的,它是非阻塞的,仅仅是减1,不会阻塞线程。
💬 六、面试中如何回答:
❓ 面试官:你有没有在项目中用过 CountDownLatch?你是怎么利用它来做压测的?
✅ 面试最佳回答模板如下:
在我参与接口压测的项目中,我使用
CountDownLatch
来模拟多个用户同时发起请求的高并发场景。我们设定了一个startLatch
,所有线程在启动后立即await()
等待主线程发令。当主线程调用startLatch.countDown()
后,所有线程才同时发起请求,确保压测过程中并发起跑的真实性。此外,为了统计整体执行时间,我还使用了一个
doneLatch
,它的计数器值等于线程数量。每个线程在请求执行完后调用doneLatch.countDown()
,主线程使用doneLatch.await()
来等待所有线程执行完毕后再统计总耗时。这种方式保证了:
- 请求发起的同时性;
- 主线程可以统一等待所有线程结束;
- 整体压测数据的准确性和可重复性。
在后续的实际接口压测中,我们还将它封装为工具类,通过线程池 + 接口响应时间统计 + 成功率统计,进一步用于系统的压力分析和瓶颈排查。
🎯 总结
CountDownLatch
是并发编程中协调线程启动与结束的利器;- 在压测中非常实用,可以有效模拟真实并发场景;
- 与线程池结合使用效果更佳;
- 面试中可以作为并发编程 + 性能优化的实际经验亮点进行展示。