第四节后面最好再补一句“算完还得压测校准”——N 用 Runtime.getRuntime().availableProcessors() 获取,但容器里要留意 JDK 版本:8u191 之前读到的常是宿主机核数,会直接把线程数配爆;8u191+ 默认开启了容器支持,才准确。

有界队列的容量也有估算方法,比随手填 200 更靠谱:容量 ≈ 峰值 QPS × 你能容忍的最大排队秒数。比如 200 QPS、忍 1 秒,容量就是 200;填太大等于偷偷退化成无界队列,填太小则频繁触发拒绝,用户看到报错。这里正好接上你说的拒绝策略——只有队列有界,四种策略才有意义。选法我一般这样定:核心链路用 AbortPolicy,并在业务侧捕获 RejectedExecutionException 后降级(同步执行或返回“稍后再试”);CallerRunsPolicy 适合批处理类削峰场景,但它会阻塞提交任务的线程,在 Web 请求线程、Netty EventLoop 里千万别用,容易拖慢整条链路;Discard* 只在日志、埋点这类可丢场景用;生产环境更常见的是自定义 handler,先把任务和线程池状态记录到日志,再决定抛异常还是降级。

还有两个容易遗漏的点:keepAliveTime 默认对核心线程无效,需要调用 allowCoreThreadTimeOut(true) 才会回收;线程池配好后要持续监控,getActiveCount() 和 getQueue().size() 定时记录下来,比事后排查 OOM 有用得多。

补充一个高频问题:在 Spring 里用 @Async 时,如果不配置 TaskExecutor,Spring 会使用默认的 SimpleAsyncTaskExecutor;它每次提交任务都新建线程,且没有上限,等于绕开了你辛苦配置的线程池。因此要自定义 ThreadPoolTaskExecutor 并注册为 Bean。