上周订单导出功能重构,我想着用AI提速,就把需求丢给了Cursor——把原来for循环拼接的逻辑改成Stream并行处理。AI写得很快,三分钟就给我吐了一段看起来很优雅的parallelStream代码,我还觉得挺满意,review了一遍就合了。
上线第二天,凌晨告警就来了。订单服务堆内存飙到95%,GC根本回收不掉。翻日志一看,全是"Java heap space",导出接口超时率从0.3%直接干到了12%。
问题出在哪
AI给我的代码大概长这样:
java List<OrderExportDTO> result = orders.parallelStream() .map(order -> enrichOrderDetail(order)) // 每个订单查3次远程接口 .map(dto -> calculatePrice(dto)) // 价格计算,涉及BigDecimal运算 .collect(Collectors.toList());
看起来没毛病对吧?但这里藏了两个坑。
第一个坑是parallelStream的默认线程池。AI压根没提这茬——parallelStream用的是ForkJoinPool.commonPool(),这个池子的大小等于CPU核心数-1。我那台8核的机器,只有7个工作线程,而导出请求一秒能来二三十个。七个人干活,二三十个任务排队,全堆在内存里等着。
第二个坑更隐蔽。enrichOrderDetail方法里调了三个远程接口,每个接口平均耗时200ms。parallelStream的工作线程被远程调用阻塞在那,ForkJoinPool的任务队列疯狂堆积。原来for循环虽然慢,但至少是逐条处理,不会把几万条数据同时展开到内存里。改了Stream之后,反而是"快"出了问题——所有数据同时进入处理流水线,内存占用直接翻了十几倍。
我是怎么排查的
说实话,第一反应是怀疑远程接口变慢了。翻了一轮监控,接口RT没变化。然后我jmap了一把,看到内存里全是CompletableFuture和ForkJoinTask对象,才反应过来是并行流搞的鬼。
jstack看线程状态更直观——7个ForkJoinPool工作线程全部停在WAITING状态,等远程调用返回。而主线程在LinkedBlockingQueue.take()上阻塞,等着collect完成。
java // jstack关键信息 "ForkJoinPool.commonPool-worker-3" - WAITING on java.util.concurrent.CompletableFuture "ForkJoinPool.commonPool-worker-5" - WAITING on java.util.concurrent.CompletableFuture "http-nio-8080-exec-12" - WAITING on java.util.concurrent.LinkedBlockingQueue.take()
修复方案
不要在parallelStream里做IO密集型操作,这应该是个常识,但AI不会主动告诉你。它只负责把代码写出来,不管你的业务场景适不适合。
我改回for循环了?没有。换了个思路——用自定义线程池 + CompletableFuture组合,把并行度和IO隔离开:
```java ExecutorService exportPool = new ThreadPoolExecutor( 4, 4, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), // 限制队列长度,别让任务堆积 new ThreadPoolExecutor.CallerRunsPolicy() // 队列满了主线程自己跑 );
List> futures = orders.stream() .map(order -> CompletableFuture.supplyAsync( () -> enrichOrderDetail(order), exportPool )) .collect(Collectors.toList());
List result = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); ```
关键改动三个地方:一是用了独立线程池,不再跟ForkJoinPool抢资源;二是ArrayBlockingQueue限制队列长度100,超出就CallerRunsPolicy回退到同步执行;三是CompletableFuture和Stream分开,避免嵌套的并行流导致任务展开失控。
改完上线,堆内存稳在40%以下,导出接口超时率回到0.2%。
这事给我的教训
AI写Stream代码很快,但它不会帮你评估业务场景。parallelStream适合CPU密集型的纯计算任务,比如数据排序、集合过滤。一旦里面混了远程调用、数据库查询这种IO操作,它就是定时炸弹。
以后用AI生成并发代码,我多留个心眼:先看有没有IO操作,再看线程池是谁的,最后看有没有背压机制。AI给的答案永远是"能不能跑","跑得稳不稳"得自己掂量。
还有一个细节——AI生成的代码里没有做异常隔离。enrichOrderDetail如果抛了异常,parallelStream的整个管道会直接挂掉,连部分结果都拿不到。我后来加了try-catch包了一层,异常订单单独记录到失败列表,至少保证正常订单能导出来。
这种边界情况AI几乎不会主动处理,它只管happy path。你的线上环境可没有happy path。