你还在 Thread.sleep() 等异步?——等到“刚好通过”的那天,你不心虚吗?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
1)测试异步消息、定时任务时的痛点:不是难,是“难受”😵💫
异步代码的测试,最烦的从来不是“写不出来”,而是写出来之后你心里没底:
- Thread.sleep() 玄学:睡短了偶发失败,睡长了测试慢得像拖拉机;更离谱的是——你改个机器、换个 CI 环境,睡眠时间又要重新“调参”。
- 最终一致性:消息消费、异步回调、定时任务,本来就不保证立刻完成,你却要在单测里“当场验收”。
- 失败不可定位:sleep 只是等,等不到你也不知道是“没发生”还是“发生晚了”还是“线程炸了”。
- 测试语义被污染:你本来想表达“最终应该变成 X”,结果测试代码充满了轮询、锁、超时、计数器……业务含义被淹没。
Awaitility 的存在感就一句话:把“等待异步完成”这件事写得像人话。它用 DSL 让你表达“在指定时间内,条件最终满足”,而不是手写一堆线程与 sleep。
2)Awaitility DSL:await().atMost().until(...)(把“等到”为止写清楚)
先把最核心的一句背下来(放心,不是八股,是救命):
await().atMost(...).until(...):在最多等待 X 时间内,直到条件为真。
Awaitility 的官方用法示例里就有同款表达:默认会等一段时间,不满足就抛超时异常;想调整就 await().atMost(5, SECONDS).until(...)。
2.1 最小示例:从“睡觉”等待,变成“条件”等待
假设异步线程会把 AtomicInteger 加到 1:
import org.awaitility.Awaitility;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
class Demo {
void asyncIncrement(AtomicInteger n) {
Executors.newSingleThreadExecutor().submit(() -> {
try { Thread.sleep(200); } catch (InterruptedException ignored) {}
n.incrementAndGet();
});
}
@org.junit.jupiter.api.Test
void should_eventually_increment() {
AtomicInteger n = new AtomicInteger(0);
asyncIncrement(n);
await()
.atMost(2, SECONDS)
.untilAtomic(n, equalTo(1));
}
}
你看,测试意图非常直白:两秒内它应该最终变成 1。这比“睡 500ms 然后 assert”靠谱多了🙂
3)别只会 atMost:pollDelay / pollInterval 才是“稳定性开关”🔧
很多人用 Awaitility 只会 atMost,然后遇到偶发抖动就怪它……冤枉啊😂。
Awaitility 的 Javadoc 专门提醒过:它通过轮询检查条件,第一次检查发生在 poll delay 之后,之后按 poll interval 周期检查;而且如果你改了 poll interval,poll delay 默认也会跟着变(除非你显式指定)。
3.1 一个更“工程化”的等待写法(建议你直接抄)
import static org.awaitility.Awaitility.with;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
@Test
void wait_with_polling_strategy() {
with()
.pollDelay(50, MILLISECONDS) // 先等 50ms 再开始第一次检查
.pollInterval(100, MILLISECONDS) // 之后每 100ms 检查一次
.await()
.atMost(3, SECONDS)
.until(() -> /* condition */ true);
}
什么时候要调这些?
- 你的条件依赖 IO(DB/消息队列/网络),“立刻检查”没意义;
- 你不想把 CPU 跑满(默认频率不一定适合你的场景);
- 你想让测试更可预测:避免“刚好在某次轮询卡边缘”的偶发失败。
4)结合 JUnit 5 测 Spring @Async:让异步“按时交卷”📌
Spring 的 @Async 本质是让方法在另一个线程执行。并且 @Async 还支持通过 value 指定要用哪个 Executor/TaskExecutor(按 bean 名或 qualifier 匹配)。这是 Spring 官方 Javadoc 的明确语义。
下面我们用一个“最像真实业务”的例子:异步方法最终会写入某个仓库(这里用内存仓库模拟,避免把测试写成集成测试)。
4.1 业务代码(示例)
@Service
public class ReportService {
private final ReportStore store;
public ReportService(ReportStore store) {
this.store = store;
}
@Async("eventExecutor")
public void generateReportAsync(String userId) {
// 模拟耗时任务
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
store.save(userId, "OK");
}
}
@Component
public class ReportStore {
private final ConcurrentHashMap<String, String> data = new ConcurrentHashMap<>();
public void save(String k, String v) { data.put(k, v); }
public String get(String k) { return data.get(k); }
}
线程池配置(测试里也建议明确配置,别赌默认行为😅):
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(2);
exec.setMaxPoolSize(4);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("event-");
exec.initialize();
return exec;
}
}
4.2 JUnit 5 + Awaitility 测试:不 sleep,只等“结果出现”
@SpringBootTest
class ReportServiceTest {
@Autowired ReportService reportService;
@Autowired ReportStore store;
@Test
void async_report_should_eventually_be_generated() {
String userId = "u-100";
reportService.generateReportAsync(userId);
await()
.atMost(2, TimeUnit.SECONDS)
.until(() -> "OK".equals(store.get(userId)));
}
}
这类测试的好处是:
- 你表达的是“最终一致”而不是“等 500ms 应该好了”;
- 环境再慢也能顶住(只要不超过 atMost);
- 环境再快也不会浪费时间(条件一满足就立刻结束)。
5)测试异步消息/队列消费的最终一致性:别追“立刻”,追“最终”✅
消息队列消费这种东西,最正常的行为就是:生产者发完不等消费者。
所以你在测试里追“立刻一致”,大概率是在跟系统的设计理念对着干(然后你会更痛苦😇)。
Awaitility 的定位就是:在给定时间窗口内反复检查,直到条件满足,否则超时失败。官方文档/示例也强调它适用于“发布异步消息到 broker,然后等待系统状态更新”。
5.1 用一个“内存消息队列”模拟真实消费(思路比框架更重要)
class InMemoryQueue {
private final BlockingQueue<String> q = new LinkedBlockingQueue<>();
void publish(String msg) { q.offer(msg); }
String take() throws InterruptedException { return q.take(); }
}
class Consumer {
private final InMemoryQueue queue;
private final AtomicInteger consumed = new AtomicInteger(0);
Consumer(InMemoryQueue queue) { this.queue = queue; }
void start() {
new Thread(() -> {
while (true) {
try {
queue.take();
consumed.incrementAndGet();
} catch (InterruptedException e) {
return;
}
}
}).start();
}
int consumedCount() { return consumed.get(); }
}
测试:
@Test
void message_should_eventually_be_consumed() {
InMemoryQueue queue = new InMemoryQueue();
Consumer consumer = new Consumer(queue);
consumer.start();
queue.publish("m1");
queue.publish("m2");
await()
.atMost(2, SECONDS)
.until(() -> consumer.consumedCount() == 2);
}
这就是最终一致性测试的“骨架”:
- 你不关心 10ms 内是不是处理完;
- 你关心的是:在合理时间内,结果会到达你期望的状态。
6)一些很实用、但容易被忽略的“等待哲学”🧠
6.1 给每个等待起个名字(排查时你会感谢自己)
Awaitility 支持给等待取别名,多个 await 混在一起时非常好定位哪个失败(不少用法文档/示例都会这么做)。
6.2 把超时当成“业务 SLA 的缩影”
atMost(2s)不是随便写的,它最好对应你系统在测试环境的合理 SLA;- 过短=偶发失败;过长=问题发现晚 + 测试拖慢。
6.3 别拿 Awaitility 做性能测试
Awaitility 依赖轮询,它的目标是验证条件最终成立,不是测毫秒级性能(Javadoc 也明确讲了它基于轮询机制,poll delay/interval 的语义会影响检查节奏)。
结尾:你用 sleep 的那一刻,其实是在赌运气🎲
我最后送你一句有点欠揍但很真实的话:
Thread.sleep() 写进测试里,本质上是在跟机器“拼人品”。
Awaitility 做的事很简单:把“我赌它会完成”改成“我等到它完成(或明确失败)”。这才像测试——有边界、有语义、有确定性。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.cn/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)