DEBUG.WIKI
OnlineSIM:在线接收短信,虚拟号码租用
RackNerd;$21.99/年 · 1GB KVM
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证

Caffeine 报 Queue capacity exceeded,线程池却几乎空闲

Caffeine 3.3.0 的可复用清理任务可能触发 ForkJoinPool 队列异常。3.3.1 已改任务提交方式;区分受损队列与真实任务积压。

日志反复出现 Exception thrown when submitting maintenance task,内层是 RejectedExecutionException: Queue capacity exceeded,但 ForkJoinPool.commonPool() 的工作线程大多在等待,也看不到持续增长的任务积压。这时别急着增加缓存容量或线程数,先核对实际加载的 Caffeine 版本。

这里明确出现持续拒绝的组合是 Caffeine 3.3.0 与 JDK 25.0.4。Caffeine 3.3.0 的清理任务会重复提交同一个 ForkJoinTask 对象。在特定并发时序下,它可能触发公共池外部队列的状态损坏:槽位已经空了,队列的计数却仍显示占满。后续落到该队列的提交继续被拒绝,连移除监听器也可能记录 Exception thrown when submitting removal listener。

不同 JDK 对受损队列的表现并不相同;JDK 11、17、21 不能直接照搬 JDK 25 的“持续拒绝”症状判断,也可能有任务丢失等表现。

这条路径不能解释所有拒绝异常。真正的突发流量、长时间阻塞的监听器、自建执行器的有界队列,也会拒绝新任务;只有版本、调用栈和“持续拒绝但没有相应积压”的表现吻合,才适合按这里处理。

3.3.1 改掉了可复用 ForkJoinTask

3.3.1 已正式发布这一修正。它的清理任务只实现普通 Runnable,不再继承 ForkJoinTask,由公共池为提交建立适当的任务包装。Java 11 及以上、使用 Caffeine 3.x 的应用,可以在依赖管理中采用该正式版本,例如 Gradle Kotlin DSL:

implementation("com.github.ben-manes.caffeine:caffeine:3.3.1")

若版本由平台 BOM 或上层框架管理,要检查最终解析结果,避免声明写了 3.3.1,运行包里仍装入旧版。升级前保留原依赖锁定文件和部署包,同时检查上层组件对 Caffeine 主版本的要求。

Caffeine 3.x 要求 Java 11 及以上。仍需 Java 8 的项目不能直接把 2.x 替换为 3.3.1;截至 2026 年 10 月 23 日,不能把本次 3.3.1 发布当成 2.9.x 已有回补的依据。

已受损的公共池需要随进程重建

修正阻止的是这类任务复用继续发生,不会把正在运行的 JVM 里已损坏的公共池队列原地修好。正常部署新依赖并重启应用进程,才能同时取得新实现和新线程池。

不要关闭公共池来“清空缓存”,也不要在缓存调用点反复吞掉异常。公共池可能还被其他异步任务共享;Caffeine 遇到执行器拒绝时会尝试在调用线程做维护,异常没有直接传到业务入口,也不代表业务延迟和移除回调没有受到影响。

若必须临时隔离执行器,应由应用统一管理其生命周期,明确任务容量、拒绝策略及关闭顺序;这会改变维护和回调的运行位置,不能视为只改一行的无影响操作。

部署后继续观察同样的缓存负载,核对运行时版本、维护任务和移除监听器日志,并确认过期或淘汰后的回调仍能执行。偶发竞态没有再次出现,只能作为运行观察;如果队列实际不断增长,应另查阻塞任务和提交速度,不能把它也归入这次任务复用问题。