我不止一次在评审清单里看到“禁用 SimpleAsyncTaskExecutor”这条规则。有次它排在某个项目规范的第 69 条,编号挺好记,后来聊起来,发现不少人根本不清楚这条规则为什么存在——甚至有人反问:“这不就是 Spring 官方提供的线程池实现吗?官方给的还能有问题?”问题恰恰出在这。SimpleAsyncTaskExecutor 名义上叫线程池,实际上它根本不做线程复用,每次提交任务都新起一个线程,任务执行完线程就原地销毁。听起来好像没什么,但在生产环境里,一旦并发量稍微上来,线程数会像失控一样疯涨,最终 OOM 或者把 CPU 打到 100%。
这篇内容我打算从头讲清楚这件事:SimpleAsyncTaskExecutor 到底是个什么机制,为什么项目规范里要明确“禁用”,以及如果你想替换它,应该用哪套方案、配置参数怎么定、坑在哪里。内容面向正在做 Spring Boot 性能优化、代码规范治理,或者刚接触异步任务开发的后端工程师,希望能帮你们少走弯路。
1. 先看懂 SimpleAsyncTaskExecutor 的执行机制
1.1 从源码看线程创建的“风暴”
SimpleAsyncTaskExecutor 在 Spring 里的定位非常直接:实现 TaskExecutor 接口,每次收到一个任务就 new 一个新 Thread 去执行。核心逻辑用一句话概括就是“提交一个任务,起一个线程,任务结束,线程进入不可复用状态”。
它的 execute 方法实现大概长这样:
java复制public void execute(Runnable task, long startTimeout) {
Runnable taskToUse = task;
if (this.taskDecorator != null) {
taskToUse = this.taskDecorator.decorate(task);
}
// 关键点在这里:直接 new Thread,而不是从池里取
Thread thread = new Thread(taskToUse);
thread.setName(this.threadNamePrefix + this.threadCounter.incrementAndGet());
thread.start();
}
你可以把 SimpleAsyncTaskExecutor 理解成一个“只负责点火、不负责回收”的线程发射器。任务一多,它不会排队,不会限流,也不会拒绝,而是有多少任务就创建多少线程。线程本身也不是池化复用,任务执行完这个线程就被 JVM 回收了。
这套机制在实际运行中会带来三个直接后果。
第一个是线程创建开销被无限放大。Java 线程的创建涉及操作系统内核调用,要分配栈内存(默认 1MB 左右)、建立线程控制块,一组操作下来耗时不小。如果系统每秒提交几百上千个任务,那大部分 CPU 时间都花在“创建线程”和“销毁线程”上,真正执行业务逻辑的预算反而被压缩了。
第二个是资源上限不可控。SimpleAsyncTaskExecutor 没有队列、没有最大线程数、没有拒绝策略。一旦任务提交速度大于执行速度,存活的线程数就会线性增长。线程多了之后,大量线程同时竞争 CPU、内存和 IO,最终引发系统整体瘫痪。
第三个问题更隐蔽:任务之间互相干扰。比如你有一个定时任务在跑批,另一个接口在做异步通知,两个任务都丢给 SimpleAsyncTaskExecutor,线程数一多,大家抢 CPU,慢任务拖垮快任务,整个应用的响应时间曲线会变得异常难看不规律。
1.2 面试里爱问、生产上爱踩的三个细节
面试题里经常问“Spring 里的 TaskExecutor 实现有哪些,区别是什么”,很多人背得出类名,但说不清楚细节。这里我补几个很容易在开发中被忽略的点。
第一,SimpleAsyncTaskExecutor 有 concurrencyLimit 概念,但它只是一个“限制并发阈值”的手段,不是线程池。它通过 semaphore 信号量来控制并发数,任务执行前尝试获取许可,获取不到就阻塞等待。默认的 concurrencyLimit 是 -1,表示不限制并发;如果你手动设成一个正数,那它最多同时跑这么多任务,多出来的任务会在信号量上排队。但注意,排队是阻塞在调用线程上的,不是放到队列里,效果和一个无界阻塞队列完全不同。
第二,SimpleAsyncTaskExecutor 实现了 AsyncListenableTaskExecutor 接口,所以它可以返回 ListenableFuture,支持回调。这一点让很多人误以为它和 ThreadPoolTaskExecutor 在能力上等价。能力等价指的是对外 API,不表示内部机制等价。
第三,也是最重要的一条——Spring Boot 里 @Async 默认走的是 SimpleAsyncTaskExecutor 吗?很多老项目里确实会遇到这种情况。Spring Boot 2.1 之前,如果不额外声明 TaskExecutor Bean,@Async 的默认执行器就是 SimpleAsyncTaskExecutor。从 2.1 开始,Spring Boot 自动配置了一个 applicationTaskExecutor,底层使用的是 ThreadPoolTaskExecutor,默认核心线程数 8,最大线程数 Integer.MAX_VALUE,队列容量用的是一个无界队列。你没看错,Spring Boot 官方默认配置的最大线程数是“几乎无限大”,等于变相放弃了过载保护。所以即使你的项目用了 Spring Boot 2.1+,也别以为默认配置就万事大吉,必须自己显式定义线程池参数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么团队规范里会明确“禁用”
2.1 三个真实事故场景
我在不同团队里见过三次因为 SimpleAsyncTaskExecutor 翻车的事故,场景基本覆盖了它的全部典型使用方式。
第一次事故发生在消息推送服务。业务方对接了一个外部短信网关,网关响应很慢,平均要 3 秒。开发图省事,直接在网关客户端里用 SimpleAsyncTaskExecutor 异步发送短信。促销活动一开始,瞬时提交的短信任务量达到每秒 1500 条,线程数在 20 分钟内从个位数涨到 2 万以上,应用内存直接 OOM,整个服务重启了三次才缓过来。
第二次事故发生在定时任务里。当时系统里有一个每 5 分钟执行一次的批处理任务,任务内部会开多线程去同步外部数据。这个任务用 SimpleAsyncTaskExecutor 管理内部线程,但外层定时任务本身是串行执行的。某天上游数据量暴增,单次批处理耗时从 2 分钟涨到 15 分钟,新的定时任务又不断触发,线程数叠加增长,最终把数据库连接池打满了。
第三次事故则和 @Async 的默认执行器有关。老项目升级 Spring Boot 时,团队没有关注异步执行器的自动配置变化,代码里大量使用 @Async 注解处理发送邮件、日志记录这类轻量任务。Spring Boot 2.1 之后默认执行器换成了 ThreadPoolTaskExecutor,本来觉得没问题了,结果发现核心线程数只有 8,任务量大时全部阻塞在队列里,邮件发送延迟从原来的秒级变成小时级。这个案例其实说明另一个问题:如果团队只是“禁用了 SimpleAsyncTaskExecutor”但没有正确配置替代方案,一样会踩坑。
这三个案例放到一起,核心规律很容易看出来:SimpleAsyncTaskExecutor 本身没有“背锅”的自觉,凡是用它的地方,几乎都存在“没有限制的并发”和“无上限的资源占用”。在并发量可控、任务量极小的内部工具脚本里,它可能一辈子不出问题;但只要是线上服务,就经不起这种不确定性。
2.2 它和 ThreadPoolTaskExecutor 的本质差异
我做了个对比表格,方便你直观理解两者差异:
| 对比维度 | SimpleAsyncTaskExecutor | ThreadPoolTaskExecutor |
|---|---|---|
| 线程复用 | 不复用,每个任务新建线程 | 复用核心线程,线程执行完任务后不销毁 |
| 最大线程数 | 默认无上限(或由信号量限制并发) | 可配置 corePoolSize / maxPoolSize |
| 队列机制 | 没有队列,超出并发限制时阻塞调用线程 | 支持无界/有界队列,任务先入队 |
| 拒绝策略 | 没有真正意义上的拒绝策略 | 支持 AbortPolicy / CallerRunsPolicy 等 |
| 任务优先级 | 不支持优先级排序 | 可通过 PriorityBlockingQueue 实现优先级 |
| 资源控制 | 基本不可控 | 核心参数可调,可预测 |
拿生活化的场景类比:SimpleAsyncTaskExecutor 就像一个来一个顾客就招一个临时工的快餐店,没有门店规模上限,也没有后厨排队区,客流量一大就手忙脚乱;ThreadPoolTaskExecutor 则是一个有固定编制的前台团队,忙的时候最多再叫一组临时工,实在忙不过来就让顾客在等候区排队,等不起了直接告知“今天不接了”。后者虽然也会出现问题,但至少处处都有边界,排障时可以按规则判断问题出在哪个环节。
3. 清理存量代码:如何定位哪里用了 SimpleAsyncTaskExecutor
3.1 静态检索四板斧
如果你们项目现在还没有这条禁用规范,你可以先动手把代码里的使用点全部揪出来。不需要什么高级工具,常规手段就够了。
第一板斧:搜类名。在 IDE 里全局搜索 SimpleAsyncTaskExecutor,把搜索结果按文件分组,逐个检查每个使用点的上下文。
第二板斧:搜 Bean 定义。老项目里经常有这样的代码:
java复制@Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
这种显式声明最容易被发现,因为它藏在配置类里,单纯搜类名也能搜到。但更隐蔽的是下面这种写法:
java复制@Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor("async-");
}
构造函数参数是线程名前缀,如果不仔细看,很容易把它和 ThreadPoolTaskExecutor 混淆。
第三板斧:检查 @Async 的默认执行器。如果项目里大量使用 @Async,但是没有看到任何 TaskExecutor Bean,那默认执行器有可能是 SimpleAsyncTaskExecutor。这个要分版本看:Spring Boot 2.1 之后默认是 applicationTaskExecutor;如果你的项目是纯 Spring 5 或更早,没有配置的话,默认就是 SimpleAsyncTaskExecutor。简单确认方式是在配置类里加一个 TaskExecutor 的 Bean,让使用点显式依赖它,避免被默认执行器带着走。
第四板斧:查依赖树。有时候项目里没有一个地方直接写 SimpleAsyncTaskExecutor,但引用的第三方库内部用了它。比如一些老版本的通知类 SDK、消息网关客户端,内部默认用 SimpleAsyncTaskExecutor 发异步请求。你需要在 pom.xml 或 build.gradle 里搜索 spring-context 相关依赖,定位到具体是哪个库把 Spring 带进来的。如果依赖版本锁得死,没法升级,那你只能通过设置 concurrencyLimit 来做粗粒度限制。
3.2 运行时定位:线程名、指标、堆栈
静态搜索只能看到“代码里写了什么”,线上运行时还需要一套方法确认“到底有没有在用”。
方法一是看线程名。SimpleAsyncTaskExecutor 创建的线程名默认格式是“类名-数字”,比如 SimpleAsyncTaskExecutor-1。如果你通过 jstack 或者 Arthas 抓线程快照,发现大量这种命名的线程,那基本可以确定有地方在用。
方法二是看指标。Spring Boot Actuator 暴露的 jvm.threads.live 指标如果呈现出快速上涨的趋势,同时 GC 频率也在加速,可以先查一下是不是异步执行器的问题。
方法三是用 Arthas 做运行时诊断。Arthas 的 thread 命令可以按 CPU 占用排序,SimpleAsyncTaskExecutor 的线程如果大量处于 RUNNABLE 状态且 CPU 占用高,很容易在列表里看到。也可以通过 trace 命令拿到调用栈,反向定位到具体业务代码。
我遇到过一次比较难排查的情况:线程名被自定义的 taskDecorator 改了,导致线程名里看不到 SimpleAsyncTaskExecutor 的特征。后来是用 Arthas 反查线程池实例,又通过当前线程的 contextClassLoader 定位到具体业务模块,才找到元凶。这种属于极端场景,但对于排查线上问题来说,思路比工具更重要——你要先怀疑这里有异步资源失控,再顺着线索往下挖。
3.3 新增代码防再犯的配置约束
定位到存量代码之后,还要防止新增代码再踩同一个坑。光靠人肉 Review 力度不够,我见过更可靠的做法是加 Checkstyle 或 SpotBugs 规则。团队里可以自定义一个禁止直接实例化 SimpleAsyncTaskExecutor 的检查规则,违反直接 CI 失败。这种方式初期会招来开发者的抱怨,但坚持几个迭代之后,新人基本都知道这条红线了。
另一个做法是在代码规范文档里明确写清楚“异步任务的执行器必须使用 ThreadPoolTaskExecutor 或符合团队技术选型的替代方案”,然后配合团队内部的代码评审 check list 一起落实。规范存在的意义不是限制大家,而是把已经踩过的坑记录下来,避免后人重复踩。
4. 替换方案实战:从 ThreadPoolTaskExecutor 到虚拟线程
4.1 ThreadPoolTaskExecutor 的配置与参数计算
替换 SimpleAsyncTaskExecutor,大家最常用也最容易上手的方案就是 ThreadPoolTaskExecutor。它有几个核心参数需要认真对待:核心线程数、最大线程数、队列容量、拒绝策略。
接下来我们看一个基础配置:
java复制@Configuration
public class AsyncConfig {
@Bean("businessTaskExecutor")
public ThreadPoolTaskExecutor businessTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("biz-exec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
这套参数怎么定?我建议遵循一个思路:先估算业务高峰期每秒提交的任务量,再结合每个任务的平均执行时间来推算核心线程数。
计算公式很简单:核心线程数 = 每秒任务量 x 平均任务耗时(秒)。如果每个任务耗时 50ms,每秒提交 200 个任务,那核心线程数大约需要 200 x 0.05 = 10。这时候你设置核心线程数 8 就有点紧,建议留出 20% 到 30% 的余量,设成 12 或者 16 更合理。
最大线程数的设置要谨慎。它决定的是“核心线程耗尽 + 队列满了”之后的额外线程上限。如果队列容量设得很大,那最大线程数往往派不上用场,因为任务都在队列里排队,不会触发扩容逻辑。如果你的业务要求高峰期尽量多并发、可以牺牲一点排队时间,那就把队列容量调小,比如 100,同时把最大线程数调到 20 或 30,这样能更快消化突发流量。
队列容量的选择其实是最容易踩坑的环节。有人喜欢把队列设得很大,认为这样可以兜底所有任务。但如果队列无界且没有上限,任务蜂拥而入,内存消耗会持续上涨,最终一样 OOM。所以我更建议使用有界队列,配合一个合理的拒绝策略。CallerRunsPolicy 是我比较推荐的一种:任务满了之后,由提交任务的线程自己执行这个任务。这样一来,任务不会丢,代价是调用方的接口响应会变慢,消费者会主动做反向压力控制。对大多数业务系统来说,这个行为比直接抛异常要安全得多。
还有两个容易被忽略的参数值得单独说明。第一个是 setWaitForTasksToCompleteOnShutdown(true),它能让 Spring 容器关闭时等待已提交任务执行完成,而不是粗暴中断线程。第二个是 setAwaitTerminationSeconds(30),它是等待超时时间,防止某些任务卡死导致进程永远退不出去。做线上发布时,这两个参数能极大减少服务重启带来的任务丢失问题。
4.2 按业务场景配置多个执行器
很多项目只有一个 TaskExecutor,所有异步任务都往里面塞。这种做法有一个隐患:一个任务的执行策略会影响所有任务。比如你有一个数据库批量同步任务,耗时很长,把核心线程都占住了;这时候用户的邮件发送任务进来,只能排队等,体验很差。
比较好的做法是按业务场景拆分执行器。比如:
java复制@Bean("mailTaskExecutor")
public ThreadPoolTaskExecutor mailTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("mail-exec-");
return executor;
}
@Bean("reportTaskExecutor")
public ThreadPoolTaskExecutor reportTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("report-exec-");
return executor;
}
使用的时候通过 @Async 注解的 value 指定执行器名称:
java复制@Async("mailTaskExecutor")
public void sendMailAsync(MailDTO mail) {
// 业务逻辑
}
@Async("reportTaskExecutor")
public void generateReportAsync(String date) {
// 业务逻辑
}
这里的设计原则是:不同业务的重要程度、耗时、并发量都不一样,不能为所有任务设置同一套参数。邮件发送的并发不能太高,否则可能触发邮件服务商的反垃圾策略;报表生成的耗时较长,线程数反而不能太大,否则数据库压力会被放大。拆开之后,每个执行器的参数都可以独立调优,互相之间不会影响。
还有一种情况要特别注意:如果你在一个事务方法内部调用了另一个类的 @Async 方法,由于 Spring AOP 基于代理对象,内部方法调用不会经过代理,异步不会生效。解决方案是把异步方法拆到单独的 Bean 里,通过注入该 Bean 来调用。这个坑和线程池本身无关,但排查起来很费时间,我在这里先提醒一下。
4.3 Java 21 虚拟线程执行器
如果你的项目已经升级到 Java 21,并且 Spring Boot 版本在 3.2 以上,那你可以尝试用虚拟线程来替代平台线程池。Spring Boot 3.2 提供了 VirtualThreadTaskExecutor,底层基于 JVM 的虚拟线程实现,线程创建成本远低于平台线程,能支撑更高的并发数。
配置方式也很简单:
yaml复制spring:
threads:
virtual:
enabled: true
开启之后,Spring Boot 会自动配置一个 VirtualThreadTaskExecutor,@Async 默认也会用它。如果你需要显式声明一个自定义的虚拟线程执行器,可以这样写:
java复制@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new VirtualThreadTaskExecutor("virtual-");
}
虚拟线程能解决 SimpleAsyncTaskExecutor 的“无线程复用”问题吗?其实角度不太一样。虚拟线程不强调复用这个概念,因为它的创建成本极低,可以理解为“使用完即销毁,但销毁成本也很低”。所以即使每次任务都创建虚拟线程,系统资源消耗也不会像平台线程那样失控。不过要注意,虚拟线程不是万能的。如果你的任务是 CPU 密集型的,虚拟线程并不会提升执行效率,因为 CPU 核心数就那么几个;它真正擅长的是 IO 密集型任务,比如调用外部 API、读写数据库、发送消息等。
如果你的团队正在做新的技术选型,我建议先评估业务场景:如果是老项目维护,优先用 ThreadPoolTaskExecutor,配置成熟、资料多、同事都熟悉;如果是新项目并且已经用了 Java 21,可以尝试虚拟线程,但要先做压测,确认它在你们的业务负载下表现稳定。
5. 常见问题与排查技巧实录
我把实际工作中围绕“禁用 SimpleAsyncTaskExecutor”最容易遇到的问题整理成一张速查表,方便大家按图索骥。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 线程数持续上涨,内存飙升 | SimpleAsyncTaskExecutor 无上限创建线程 | jstack 查看线程名 | 替换为 ThreadPoolTaskExecutor 并限制线程数 |
| @Async 不生效,任务串行执行 | 同类内部方法调用,代理未生效 | 断点检查是否进入代理对象 | 拆分为独立 Bean,通过注入调用 |
| 线程池满后任务丢失 | 使用了 AbortPolicy 且没有异常处理 | 查看日志是否有 TaskRejectedException | 改用 CallerRunsPolicy 或自定义拒绝策略 |
| 任务执行顺序错乱 | 使用无界队列 + 多线程,无法保证顺序 | 观察日志时间戳 | 引入顺序队列或为关键任务设置线程池单线程 |
| 自定义线程名前缀后依然很乱 | taskDecorator 修改了线程名 | Arthas 反查线程上下文 | 通过堆栈定位业务代码,更换执行器 |
| 服务关闭时任务被中断 | 没有配置等待任务完成 | 观察关闭日志 | 配置 waitForTasksToCompleteOnShutdown 和 awaitTerminationSeconds |
| 高峰时接口响应变慢 | CallerRunsPolicy 导致调用线程主动执行任务 | 监控调用线程耗时 | 扩大线程池容量或拆分执行器 |
| 数据库连接池被打满 | 并发线程数过高,同时拿连接 | 查看活跃连接数和 SQL 监控 | 降低线程数,通过信号量限制并发 |
5.1 最简单但最实用的测试方法
替换完执行器之后,最担心的就是“配置了半天,其实没用上”。我分享一个最简单的验证手段:
java复制@Component
public class TaskExecutorCheck {
private final TaskExecutor taskExecutor;
public TaskExecutorCheck(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printExecutorInfo() {
System.out.println(taskExecutor.getClass().getName());
if (taskExecutor instanceof ThreadPoolTaskExecutor) {
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) taskExecutor;
System.out.println("corePoolSize = " + executor.getCorePoolSize());
System.out.println("maxPoolSize = " + executor.getMaxPoolSize());
}
}
}
在应用启动后调用一次 printExecutorInfo,看控制台输出。如果打印出来的类名是 SimpleAsyncTaskExecutor,说明你的配置没生效,需要检查一下是不是被其他配置类覆盖了;如果是 ThreadPoolTaskExecutor,那基本就走上正轨了。
5.2 一个被忽略的配置细节
团队在替换执行器时,还经常忽略一个细节:如果项目里配置了多个 TaskExecutor,同时 @Async 注解又没有指定执行器名称,那任务会进入哪一个?
Spring 的处理逻辑是:如果存在唯一一个 TaskExecutor Bean,默认使用它;如果存在多个,但其中一个名字是 applicationTaskExecutor,@Async 默认使用这个;如果没有任何 TaskExecutor Bean,Spring Boot 自动配置的 applicationTaskExecutor 会兜底。
这意味着,你自定义执行器时,最好把名字直接设为 applicationTaskExecutor,这样所有没有显式指定执行器的 @Async 任务都会自动走进这个执行器。如果你想要区分不同的业务执行器,那一定要在 @Async 注解上写清楚执行器名字,不能偷懒。
5.3 压测时的关键观察指标
替换完执行器,建议做一轮针对性的压测,重点观察几个指标。
第一个是活跃线程数。你可以在压测过程中通过 Actuator 的 /actuator/metrics/executor.active.threads 接口观察。如果活跃线程数始终接近 corePoolSize 而且队列没满,说明配置夯实;如果队列一直满且最大线程数全员在线,说明容量还是不够,需要扩。
第二个是任务等待时间。这个指标不一定有现成的监控面板,但可以通过业务日志里的耗时来估算。如果一个任务提交到开始执行的时间明显变长,说明队列排队严重,要考虑提升核心线程数或降低单个任务耗时。
第三个是拒绝次数。如果配置了 CallerRunsPolicy,拒绝不会报错,但会影响调用方耗时;如果配置了 AbortPolicy,会在日志里看到 TaskRejectedException。这两个信号都说明流量超过了线程池处理能力,需要做扩容或者削峰。
我在实际压测中曾经遇到过一个很典型的情况:队列容量设置太大,压测时所有请求都进入队列,线程数几乎没有增长,接口 RT 因为排队时间变长而直线上升,但线程池本身 CPU 占用却不高。当时第一反应是加线程数,后来发现根本问题在于队列容量设计不合理,导致大量任务被“囤”在队列里,实时处理能力被有效屏蔽了。调整队列容量并观察任务等待之后,RT 才恢复正常。
写在最后的一个经验
我接触过不少被 SimpleAsyncTaskExecutor 坑过的项目,说实话,这个类本身不是不能用,如果你明确知道自己只需要跑几个一次性任务、并发量极低,用它确实方便。但“方便”不等于“合适”,尤其是在一个多人协作、迭代频繁的代码库里面。你没法保证后来接手的人知道这里只有一个任务在跑,也没法保证下个版本不会有人往这个执行器上叠加新的异步任务。禁用规则的真正价值,是把这种不确定性尽可能关进笼子里,让执行策略变得可预期。
我个人比较推荐的方式是:团队规范里写清楚“禁止直接实例化 SimpleAsyncTaskExecutor”,代码扫描工具里加对应的检查规则,同时在评审 check list 留下一行提醒。这三道关卡加在一起,比单独靠某个人记住这条规则可靠得多。另外,替换执行器时别只改一处,记得把线程名、监控指标、拒绝策略一起梳理一遍,否则等线上出了问题再补,那付出的代价可就不止改几行配置了。
