你还在 Thread.sleep() 等异步?——等到“刚好通过”的那天,你不心虚吗?

举报
bug菌 发表于 2026/01/13 11:59:20 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学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 -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。