线程池配置太大导致内存溢出

71 阅读7分钟

背景

在周四这个投产日,一台服务器突然一直告警,某一个接口连接超时,我还想我还没投产呢,一开始我以为服务器down啦,然后登录服务器查询原因,看到 java.lang.OutOfMemoryError: unable to create new native thread 看到这个知道不是服务器down啦 觉的问题应该不大

企业微信截图_17635158989085.png

然后就找代码查询原因 ,我当时觉得可能是线程的问题,以下是代码,我看到代码的时候给我整笑啦,我真想问下开发的同学,这是哪个爹教的啊,不懂可以问下啊,还有一点代码review的同学在看什么呢,这么大问题,肉眼是可以看出来的啊,问题没发现就说明一点,他们不懂线程配置

@Bean
public ThreadPoolExecutor threadPool() {
    return new ThreadPoolExecutor(
            100,  // 核心线程:越多越快!
            200,  // 最大线程:留足buffer!
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000), // 大队列:绝不丢任务!
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
    );
}

简单说下线程池配置的三大错误

1:线程越多性能越好

1. 上下文切换开销激增

当线程数超过CPU物理核心数时,系统会频繁进行线程切换。每次切换都需要保存和恢复线程状态,消耗CPU资源。测试数据显示,线程数超过物理核心数后,性能可能下降15%-30%‌。例如10核CPU上,线程数超过10个后性能反而会下降‌。

2. 资源竞争加剧

过多线程会导致内存带宽不足、缓存命中率下降、锁竞争加剧等问题‌。就像多人挤在小厨房干活,不仅效率降低,还容易发生冲突‌。线程池任务队列设置不当(如使用无界队列)还可能引发内存溢出。

3. 内存占用过高

每个线程都需要独立的内存空间(如线程栈),线程数过多会导致内存占用飙升,可能触发垃圾回收(GC),进一步影响性能‌。

合理设置建议

  • CPU密集型任务‌:线程数 ≈ CPU核心数(避免上下文切换)‌
  • I/O密集型任务‌:线程数 ≈ CPU核心数 ×2(利用I/O等待时间)

正确实例

// 正确做法:根据业务类型设置
int corePoolSize = Runtime.getRuntime().availableProcessors();

// CPU密集型:N+1
ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(
        corePoolSize + 1, corePoolSize + 1, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>()
);

// IO密集型:2N 
ThreadPoolExecutor ioExecutor = new ThreadPoolExecutor(
        corePoolSize * 2, corePoolSize * 2, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(100)
);

队列越大越安全

真相:无界队列 = 内存泄漏的定时炸弹

// 灾难配置:无界队列
new LinkedBlockingQueue<>(); // 默认Integer.MAX_VALUE

// 当任务产生速度 > 处理速度时:
// 1. 队列不断堆积
// 2. 内存持续增长  
// 3. 最终OOM,系统崩溃

// 生产环境配置:
new LinkedBlockingQueue<>(100); // 设置合理的边界
new ArrayBlockingQueue<>(200);  // 固定大小,快速响应

幻觉3:拒绝策略无所谓

真相:选错拒绝策略 = 数据丢失或服务雪崩

// 案例:订单支付系统
// 错误选择:直接丢弃
new ThreadPoolExecutor.DiscardPolicy(); // 订单静默丢失,用户已付款但系统没记录

// 错误选择:抛出异常
new ThreadPoolExecutor.AbortPolicy(); // 用户体验差,支付失败

// 正确选择:让调用线程执行
new ThreadPoolExecutor.CallerRunsPolicy(); // 降级方案,保证订单不丢失

线程池配置参数标准

在高并发应用中,线程池是控制并发度、提升吞吐、保障稳定性的关键组件。不同于简单的线程创建与销毁,合适的线程池配置能够把资源利用率、响应时间和系统可预测性统一在一个可控的范围内。本文就线程池的配置参数形成一个可落地的标准框架,聚焦参数含义、取值原则、选型策略、监控与演化路径,力求给出清晰可执行的参照。

一、核心参数及其含义

核心线程数(corePoolSize)

指在无任务阻塞时,保持活动的最小线程数量。核心线程不会因为核心线程超时而被回收,除非显式开启核心线程超时。核心线程数量直接关系到并发请求的初始承载能力,过低容易造成阻塞与队列膨胀,过高则会增加上下文切换和上下文成本。

最大线程数(maximumPoolSize)

指能够创建的最大并发线程数量。超过核心线程数的部分线程在有新任务时创建,直至达到上限。设定过高会吞吐增加,但也可能挤占系统资源,引发GC压力和对其他组件的竞争。

线程空闲保持时间(keepAliveTime)与单位(TimeUnit)

非核心线程在空闲状态下的最长存活时间。合理设定可以在高峰与低谷之间保持自适应的弹性,避免空闲线程长期占用系统资源。若开启 coreThreadTimeout(允许核心线程超时),核心线程也可能被回收,此时 keepAliveTime 的意义扩展到核心线程。

工作队列(workQueue)

线程池用来缓存等待执行任务的队列。常见类型及影响:

有界队列(如 ArrayBlockingQueue):能对任务排队长度进行明确控制,防止系统因任务积压导致内存耗尽,但需合理设置容量,否则易发生拒绝策略触发。

无界队列(如 LinkedBlockingQueue,默认无界版本):避免了拒绝策略,但可能造成任务堆积和吞吐延迟的不可预测性,难以实现背压。

同步队列(SynchronousQueue):没有缓冲区,提交的任务必须直接被线程消费,提升低延迟、但对并发度与任务吞吐敏感。

拒绝策略

‌AbortPolicy(中止策略)‌ 行为:丢弃任务并抛出RejectedExecutionException异常。 适用场景:关键业务需及时反馈系统异常,如核心服务。 ‌

‌DiscardPolicy(丢弃策略)‌ 行为:静默丢弃任务,不抛出异常。 适用场景:非关键业务(如日志统计),可容忍任务丢失。 ‌

‌DiscardOldestPolicy(丢弃最旧任务策略)‌ 行为:丢弃队列中最旧任务,重新提交当前任务。 适用场景:需优先处理新任务,但可能影响旧任务完整性。 ‌

‌CallerRunsPolicy(调用者执行策略)‌ 行为:由提交任务的线程直接执行该任务。 适用场景:低并发或需避免任务丢失的场景,但可能降低性能。

4大拒绝策略

AbortPolicy(默认):当任务添加到线程池中被拒绝时,会抛出RejectedExecutionException异常。

CallerRunsPolicy:当任务添加到线程池中被拒绝时,会使用调用者所在的线程来执行该任务。

DiscardOldestPolicy:当任务添加到线程池中被拒绝时,会丢弃队列中最靠前(来的最久)的任务,然后尝试重新提交被拒绝的任务。

DiscardPolicy:当任务添加到线程池中被拒绝时,会丢弃该任务,不会有任何异常抛出。

corePoolSize(核心线程数目):线程池中同时存在的最小线程数,即使线程处于空闲状态,也不会被回收

maximumPoolSize(最大线程数):线程池中允许存在的最大线程数,当队列满时并且当前线程数小于最大线程数时,线程池会创建新的线程(救急线程)来处理任务。最大线程数=核心线程数+救急线程数的最大值。

keepAliveTime(生存时间):当线程池中的线程数量大于核心线程数时,多余的空闲线程(救急线程)在空闲时间超过keepAliveTime后会被回收。 unit(时间单位):用于设置keepAliveTime的时间单位,例如TimeUnit.SECONDS。

workQueue(工作队列):用于存储等待执行的任务的阻塞队列,当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建新线程执行任务

threadFactory(线程工厂):用于创建新线程的工厂,可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

handler(拒绝策略):当所有线程都在繁忙,workQueue也放满时,会触发拒绝策略

线程流程

企业微信截图_17636916026535.png

线程几大状态

企业微信截图_17636917366804.png

总结

喜欢的可以点点关注,不要白嫖,各位老铁顺顺顺