ExecutorService 必须通过Executors工厂方法或显式配置ThreadPoolExecutor创建,因手动new易触发OOM或任务堆积,根本原因是默认无界队列和AbortPolicy拒绝策略导致;实操需明确队列容量、拒绝策略、线程命名及业务前缀,并在Spring中用@Bean声明配合@PreDestroy。

为什么 ExecutorService 不能直接用 new ThreadPoolExecutor(...) 手动构造
手册里反复强调“线程池必须通过 Executors 工厂方法创建”,但实际落地时很多人图省事直接 new,结果在线上压测或流量突增时频繁触发 OOM 或任务堆积。根本原因不是语法错,而是默认参数陷阱:Executors.newFixedThreadPool() 底层用的是无界 LinkedBlockingQueue,任务持续提交却消费不及时,队列无限膨胀;而手动 new ThreadPoolExecutor 时若忽略 RejectedExecutionHandler,默认的 AbortPolicy 会直接抛 RejectedExecutionException,业务没兜底就挂了。
实操建议:
- 强制使用
new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit, new ArrayBlockingQueue(queueCapacity), r -> new Thread(r, "biz-task-" + counter.getAndIncrement()), new ThreadPoolExecutor.CallerRunsPolicy())—— 明确队列容量、拒绝策略、线程命名 - 所有线程池变量名带业务前缀,如
orderNotifyExecutor,禁止泛用executor - 在 Spring 环境中统一用
@Bean声明,配合@PreDestroy调用shutdown(),避免应用停机时线程残留
synchronized 和 ReentrantLock 到底该选哪个
不是“新代码一律用 ReentrantLock”,也不是“老代码全留着 synchronized”。关键看是否需要可中断、超时获取、公平锁或多个条件变量。日常临界资源保护(比如缓存更新、计数器累加),synchronized 更轻量、JVM 优化成熟、不易漏写释放逻辑;而涉及复杂协作(如生产者消费者需分别等待不同条件),ReentrantLock 的 newCondition() 才能解耦。
常见错误现象:
立即学习“Java免费学习笔记(深入)”;
- 用
ReentrantLock却忘记在finally块调用unlock(),导致死锁 - 在持有
synchronized时调用外部服务(如 HTTP 请求),阻塞整个锁粒度,吞吐骤降 - 误以为
ReentrantLock性能一定优于synchronized—— 在无竞争场景下,后者通常更快
为什么 ThreadLocal 不清理就会内存泄漏
根本不在“线程没销毁”,而在 ThreadLocalMap 的 key 是弱引用,value 是强引用。当 ThreadLocal 变量被回收后,key 变为 null,但 value 仍挂在 map 里,且该 map 隶属于线程对象,只要线程(尤其是线程池里的常驻线程)不死,value 就一直占着堆内存。线上最典型的表现是:服务运行几天后 Full GC 频繁,dump 发现大量业务对象无法回收。
实操建议:
- 所有
ThreadLocal必须配对调用remove(),尤其在try-finally中,不能只依赖set(null) - 禁止在工具类里定义静态
ThreadLocal存储用户上下文,应改用方法参数透传或InheritableThreadLocal(仅限父子线程传递) - Spring 环境下优先用
RequestContextHolder,它内部已封装了remove()逻辑
CompletableFuture 异步链中异常到底怎么捕获
用 thenApply 或 thenAccept 处理结果,但上游抛了异常,下游根本收不到——因为异常不会穿透到这些方法,而是静默吞掉,最终整个链在某个环节卡住或返回 null。真正起作用的是 exceptionally()、handle() 或 whenComplete(),但三者语义不同:前者只处理异常,后两者都执行但 handle() 允许你返回新值,whenComplete() 只做副作用(如打日志、发告警)。
容易踩的坑:
- 在
thenCompose返回的新CompletableFuture上漏加异常处理器,导致嵌套异常丢失 - 用
join()阻塞获取结果,却没包 try-catch,把CompletionException直接抛给容器(如 Tomcat),触发 500 - 多个异步分支用
allOf()汇聚,但没逐个检查每个 future 的isCompletedExceptionally(),误判整体成功
复杂点在于:异常类型可能被包装多层,getCause() 得连剥两层才能拿到原始业务异常。这点很容易被忽略,结果告警里全是 CompletionException,看不出真实问题。











