一行CompletableFuture代码引发的P0级事故

向量数据库大模型关系型数据库

昨晚凌晨 2 点,我司电商平台的订单服务突发崩溃。用户支付请求堆积超20万条,数据库连接池耗尽,直接损失预估百万级。

根本原因:一行未指定线程池的 CompletableFuture 代码,在高并发下触发默认线程池资源耗尽,导致任务队列无限堆积,最终内存溢出(OOM)。

你以为这只是偶然?数据揭示真相:

  • 80% 的异步编程事故源于线程池配置不当;
  • 90% 的开发者对 CompletableFuture 异常处理一知半解;
  • 70% 的线上问题因任务依赖链断裂导致。

今天,我们通过这起真实事故,拆解 CompletableFuture 的正确使用姿势,教你实战避坑!

  1. 事故还原

以下代码完全复现线上问题,请勿在生产环境运行:


      
      
          

        
 
 public
 
 
        
  
 
        
 
 
 class
 
 
 
  
 
 
 
 OrderSystemCrash
 
 
 
  
 
 
        
 {
 
        
   

 
        
   

 
        
     
 
        
 
 // 模拟高并发场景
 
 
        
   

 
        
     
 
        
 
 
 public
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 main
 
 
 
 
 (String[] args)
 
 
 
  
 
 
        
 {
 
        
   

 
        
         
 
        
 
 for
 
 
        
  (
 
        
 
 int
 
 
        
  i = 
 
        
 
 0
 
 
        
 ; i < Integer.MAX\_VALUE; i++) {
 
        
   

 
        
             processPayment();
 
        
   

 
        
         }
 
        
   

 
        
         
 
        
 
 // 阻塞主线程观察结果
 
 
        
   

 
        
         
 
        
 
 try
 
 
        
  {
 
        
   

 
        
             Thread.sleep(Long.MAX\_VALUE);
 
        
   

 
        
         } 
 
        
 
 catch
 
 
        
  (InterruptedException e) {
 
        
   

 
        
         }
 
        
   

 
        
     }
 
        
   

 
        
   

 
        
     
 
        
 
 // 模拟订单服务接口:支付完成后发送通知
 
 
        
   

 
        
     
 
        
 
 
 public
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 processPayment
 
 
 
 
 ()
 
 
 
  
 
 
        
 {
 
        
   

 
        
         
 
        
 
 // 致命点:使用默认线程池 ForkJoinPool.commonPool()
 
 
        
   

 
        
         CompletableFuture.runAsync(() -> {
 
        
   

 
        
             
 
        
 
 // 1. 查询订单(模拟耗时操作)
 
 
        
   

 
        
             queryOrder();
 
        
   

 
        
             
 
        
 
 // 2. 支付(模拟阻塞IO)
 
 
        
   

 
        
             pay();
 
        
   

 
        
             
 
        
 
 // 3. 发送通知(模拟网络请求)
 
 
        
   

 
        
             sendNotification();
 
        
   

 
        
         });
 
        
   

 
        
     }
 
        
   

 
        
   

 
        
     
 
        
 
 // 模拟数据库查询(耗时100ms)
 
 
        
   

 
        
     
 
        
 
 
 private
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 queryOrder
 
 
 
 
 ()
 
 
 
  
 
 
        
 {
 
        
   

 
        
         
 
        
 
 try
 
 
        
  {
 
        
   

 
        
             Thread.sleep(
 
        
 
 100
 
 
        
 );
 
        
   

 
        
         } 
 
        
 
 catch
 
 
        
  (InterruptedException e) {
 
        
   

 
        
         }
 
        
   

 
        
     }
 
        
   

 
        
   

 
        
     
 
        
 
 // 模拟支付接口(耗时500ms)
 
 
        
   

 
        
     
 
        
 
 
 private
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 pay
 
 
 
 
 ()
 
 
 
  
 
 
        
 {
 
        
   

 
        
         
 
        
 
 try
 
 
        
  {
 
        
   

 
        
             Thread.sleep(
 
        
 
 500
 
 
        
 );
 
        
   

 
        
         } 
 
        
 
 catch
 
 
        
  (InterruptedException e) {
 
        
   

 
        
         }
 
        
   

 
        
     }
 
        
   

 
        
   

 
        
     
 
        
 
 // 模拟通知服务(耗时200ms)
 
 
        
   

 
        
     
 
        
 
 
 private
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 sendNotification
 
 
 
 
 ()
 
 
 
  
 
 
        
 {
 
        
   

 
        
         
 
        
 
 try
 
 
        
  {
 
        
   

 
        
             Thread.sleep(
 
        
 
 200
 
 
        
 );
 
        
   

 
        
         } 
 
        
 
 catch
 
 
        
  (InterruptedException e) {
 
        
   

 
        
         }
 
        
   

 
        
     }
 
        
   

 
        
 }
 
        
   

 
      
    

运行结果:picture.image

  1. 问题分析

接下来,我们深入探究 CompletableFuture 的源码。

当我们运用 CompletableFuture 执行异步任务时,比如调用

`CompletableFuture.runAsync(Runnable runnable)

或者 CompletableFuture.supplyAsync(Supplier supplier)`

这类未明确指定线程池的方法,CompletableFuture 会自动采用默认线程池来处理这些异步任务。

而这个默认线程池,正是

ForkJoinPool.commonPool()

下面,我们一同查看 CompletableFuture 中与之相关的源码片段。


      
      
          

        
 
 
 public
 
 
 
  
 
 
 
 static
 
 
 
  CompletableFuture<Void> 
 
 
 
 runAsync
 
 
 
 
 (Runnable runnable)
 
 
 
  
 
 
        
 {
 
        
   

 
        
     
 
        
 
 return
 
 
        
  asyncRunStage(asyncPool, runnable);
 
        
   

 
        
 }
 
        
   

 
        
   

 
        
 
 private
 
 
        
  
 
        
 
 static
 
 
        
  
 
        
 
 final
 
 
        
  Executor asyncPool = useCommonPool ?
 
        
   

 
        
     ForkJoinPool.commonPool() : 
 
        
 
 new
 
 
        
  ThreadPerTaskExecutor();
 
        
   

 
        
  
 
        
   

 
        
 
 private
 
 
        
  
 
        
 
 static
 
 
        
  
 
        
 
 final
 
 
        
  
 
        
 
 boolean
 
 
        
  useCommonPool =
 
        
   

 
        
     (ForkJoinPool.getCommonPoolParallelism() > 
 
        
 
 1
 
 
        
 );
 
        
   

 
      
    

从代码可知:

  • runAsync 调用 asyncRunStage 并传入 asyncPool;
  • asyncPool 依据 useCommonPool 取值选定:
  • useCommonPool 为 true 用

ForkJoinPool.commonPool()

  • 为 false 则用

new ThreadPerTaskExecutor()

  • useCommonPool 取决于

`ForkJoinPool.getCommonPoolParallelism()

是否大于 1。`

  • 该方法返回

`ForkJoinPool.commonPool()

的并行度(即线程数量,默认是系统 CPU 核心数减 1)。`

  • 若并行度大于 1,就以

ForkJoinPool.commonPool()

为默认线程池。

不过,话说回来,

ForkJoinPool.commonPool()

作为默认线程池,到底存在哪些问题呢?

  1. ForkJoinPool.commonPool() 的致命陷阱

1、全局共享:资源竞争的 “修罗场”

ForkJoinPool.commonPool()

是 JVM 全局共享的线程池,所有未指定线程池的 CompletableFuture 任务和并行流(parallelStream())都会共享它。

这就像早高峰的地铁,所有人都挤在同一节车厢,资源争夺不可避免。

2、无界队列:内存溢出的 “导火索”

ForkJoinPool.commonPool()

使用无界队列,理论上能存储大量任务,但实际受内存限制。

大量任务到来时,队列会不断消耗内存,一旦超过系统承受能力,会触发 OutOfMemoryError,服务直接宕机。

  1. 修复方案

      
      
          

        
 
 public
 
 
        
  
 
        
 
 
 class
 
 
 
  
 
 
 
 OrderSystemFix
 
 
 
  
 
 
        
 {
 
        
   

 
        
     
 
        
 
 // 1. 自定义线程池(核心参数:核心线程数=50,队列容量=1000,拒绝策略=降级)
 
 
        
   

 
        
     
 
        
 
 private 
 
 
        
 
 static
 
 
        
 
  final
 
 
        
  ExecutorService orderPool = 
 
        
 
 new
 
 
        
  ThreadPoolExecutor(
 
        
   

 
        
             
 
        
 
 50
 
 
        
 , 
 
        
 
 50
 
 
        
 , 
 
        
 
 0L
 
 
        
 , TimeUnit.MILLISECONDS,
 
        
   

 
        
             
 
        
 
 new
 
 
        
  LinkedBlockingQueue<>(
 
        
 
 1000
 
 
        
 ), 
 
        
 
 // 有界队列
 
 
        
   

 
        
             
 
        
 
 new
 
 
        
  ThreadPoolExecutor.AbortPolicy() { 
 
        
 
 // 自定义拒绝策略
 
 
        
   

 
        
                 
 
        
 
 @Override
 
 
        
   

 
        
                 
 
        
 
 
 public
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 rejectedExecution
 
 
 
 
 (Runnable r, ThreadPoolExecutor executor)
 
 
 
  
 
 
        
 {
 
        
   

 
        
                     
 
        
 
 // 记录日志 + 降级处理
 
 
        
   

 
        
                     System.err.println(
 
        
 
 "任务被拒绝,触发降级"
 
 
        
 );
 
        
   

 
        
                     
 
        
 
 // 异步重试或写入死信队列
 
 
        
   

 
        
                 }
 
        
   

 
        
             }
 
        
   

 
        
     );
 
        
   

 
        
   

 
        
     
 
        
 
 // 2. 修复后的订单服务
 
 
        
   

 
        
     
 
        
 
 
 public
 
 
 
  
 
 
 
 static
 
 
 
  
 
 
 
 void
 
 
 
  
 
 
 
 processPayment
 
 
 
 
 ()
 
 
 
  
 
 
        
 {
 
        
   

 
        
         CompletableFuture.runAsync(() -> {
 
        
   

 
        
             
 
        
 
 try
 
 
        
  {
 
        
   

 
        
                 queryOrder();
 
        
   

 
        
                 pay();
 
        
   

 
        
                 sendNotification();
 
        
   

 
        
             } 
 
        
 
 catch
 
 
        
  (Exception e) {
 
        
   

 
        
                 
 
        
 
 // 3. 异常捕获 + 降级
 
 
        
   

 
        
                 System.err.println(
 
        
 
 "支付流程异常:"
 
 
        
  + e.getMessage());
 
        
   

 
        
             }
 
        
   

 
        
         }, orderPool); 
 
        
 
 // 关键:显式指定线程池
 
 
        
   

 
        
     }
 
        
   

 
        
   

 
        
     
 
        
 
 // 其他代码同上...
 
 
        
   

 
        
 }
 
        
   

 
      
    

修复方案:

  • 线程池隔离: 创建独立线程池,避免占用公共线程池资源,确保其他业务不受影响。
  • 可控队列: 设有限容量的有界队列,配好拒绝策略,队列满时触发,防止任务堆积导致内存溢出。
  • 异常处理: 为异步任务配置异常处理器,捕获记录日志,快速定位问题,提升系统可观测性和稳定性。
  1. 总结 =======

这次事故,源于一段暗藏风险的代码。高并发下,默认线程池不堪重负,引发连锁反应,致使系统瘫痪。

现实中,类似隐患屡见不鲜:

  • 线程池配置失当: 直接沿用默认参数,未结合业务负载、服务器性能调校,高并发场景易过载。
  • 异常处理缺位: 捕获异常后不记录、不上报,还遗漏异步任务异常捕获,问题排查困难。
  • 并发安全失控: 共享变量操作未加锁,使用非线程安全集合类,高并发下数据错乱。
  • 任务依赖混乱: 不规划任务启动顺序,也不考虑依赖失败策略,一处出错就全盘皆输。

线上无小事,生产环境中要注意:

  • 默认配置是魔鬼,高并发下没有侥幸!
  • 监控是生命线:对线程池队列、内存使用率等关键指标,设置实时告警,以便第一时间察觉。

最后欢迎 加入苏三的星球 ,你将获得:苏三AI项目、 商城微服务实战、商城系统实战、秒杀系统实战 、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

送给大家一张40元优惠券,扫描下方二维码,即可加入星球:

picture.image

目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1600+小伙伴加入学习。

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
CV 技术在视频创作中的应用
本次演讲将介绍在拍摄、编辑等场景,我们如何利用 AI 技术赋能创作者;以及基于这些场景,字节跳动积累的领先技术能力。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论