CountDownLatch适合模拟秒杀并发压测,因其能精准控制所有线程毫秒级“同时出发”,避免压力分散;它以await()阻塞线程、countDown()统一释放,充当发令枪角色,且不可重用特性契合单次压测场景,防止状态残留。

CountDownLatch 为什么适合模拟秒杀并发压测
它能精准控制所有线程“同时出发”,避免因线程启动时间差导致压力不集中——这是 Thread.start() 直接循环调用做不到的。秒杀场景的关键是瞬时流量洪峰,不是总请求数,所以必须让 N 个线程在毫秒级内全部进入临界区。
- 用
CountDownLatch的await()阻塞所有工作线程,等全部就绪后再统一释放 - 主线程调用
countDown()的那一刻,才是压测真正的“枪响”时刻 - 相比
CyclicBarrier,它更轻量、不可重用,反而更适合单次压测场景,避免状态残留干扰结果
典型错误:await() 放错位置导致压测失效
最常见的是把 await() 写在 run() 开头但没配合初始化逻辑,结果部分线程提前跑飞,压力分散成“小波浪”而非“海啸”。
- 正确做法:每个线程在
run()里先调用latch.await(),且主线程必须在start()全部调用完毕后,再调用一次latch.countDown() - 错误写法示例:
new Thread(() -> { latch.await(); doSeckill(); }).start();—— 若主线程还没countDown()就已 start 完毕,这些线程会永久阻塞 - 务必确保
CountDownLatch构造参数等于线程数,否则await()永远不会返回
结合 HTTP 请求的真实压测代码片段
单纯起线程不发请求没意义。下面是在 Java 中用 OkHttpClient 模拟用户抢购的最小可行结构:
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startLatch.await(); // 等统一信号
Response resp = client.newCall(new Request.Builder()
.url("http://localhost:8080/seckill")
.post(RequestBody.create("", MediaType.get("text/plain")))
.build()).execute();
if (resp.isSuccessful()) {
System.out.println("抢到:" + Thread.currentThread().getName());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
endLatch.countDown();
}
}).start();
}
TimeUnit.MILLISECONDS.sleep(100); // 确保线程全部 start 完
startLatch.countDown(); // ✅ 关键:此刻才放行
endLatch.await(); // 等所有请求结束
-
startLatch控制并发起点,endLatch收集完成信号,二者职责不能混用 - 别省略
TimeUnit.MILLISECONDS.sleep(100)—— JVM 线程调度有延迟,直接countDown()可能漏掉尚未执行到await()的线程 - 真实环境建议加请求超时(如
client.newBuilder().connectTimeout(500, TimeUnit.MILLISECONDS)),否则个别慢请求会拖垮整体统计
容易被忽略的系统瓶颈点
压测本身不报错,不代表业务逻辑扛住了。很多问题藏在响应码和日志背后:
- HTTP 返回
429 Too Many Requests或503 Service Unavailable,说明网关或服务端限流已触发,不是并发没打上去,而是被拦在了外面 - 数据库连接池耗尽(如 HikariCP 报
Connection is not available)比应用层超时更隐蔽,需查服务端datasource监控 - 即使所有请求都返回 200,也要检查业务结果:是否出现超卖?这说明库存扣减没加锁或没走 CAS,
CountDownLatch只负责发起并发,不解决线程安全
真正难的从来不是怎么“打满”,而是打完之后,数据还对不对。

















