首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >AI帮我写代码,我给它擦屁股,一个并发问题的排查实录

AI帮我写代码,我给它擦屁股,一个并发问题的排查实录

原创
作者头像
悟空码字
发布2026-06-16 15:00:37
发布2026-06-16 15:00:37
1760
举报
文章被收录于专栏:编程技术编程技术

大家好,我是小悟。

引言:从“无脑复制”到“谨慎求证”

现在用AI辅助编程已成为常态。但你是否遇到过这样的情况:AI信心满满地给出了一段代码,运行后却报错;或者代码能跑,但结果明显不对?这篇文章将分享我用AI生成Java代码时遭遇的一个典型案例——一个看似简单的多线程数据聚合服务,从功能异常到性能优化的完整排查过程。

背景与任务

需求:实现一个服务,从多个数据源(模拟为HTTP接口)并发获取用户信息,然后聚合、去重、排序后返回。数据量不大(单次最多100个用户ID),但对响应时间有要求(<500ms)。

我给AI(以某常见大模型为例)的提示词摘要:

“用Java 8实现一个UserDataService,提供getUsers(List<Integer> userIds)方法。要求:对每个userId并发调用外部API获取用户详情,如果某个调用失败则重试1次,最后返回按userId升序排列的List<User>,重复的userId只保留一个。”

AI很快给出了完整的代码。看似工整,我稍作调整后便集成到项目中。

问题显现:功能与性能的双重“惊喜”

现象1:偶尔返回重复用户

测试时发现,当传入的userId列表包含重复ID(如[101, 102, 101]),返回结果中101偶尔会出现两次。按理说已经在最后做了distinct(),为什么还会重复?

现象2:响应时间远超预期

压测时,100个userId的响应时间稳定在1.2秒左右,完全超出500ms的指标。代码中用了CompletableFuture并发,理论上应该很快,问题出在哪?

排查过程:从表象到根源

第一步:稳定复现问题

写了一个单元测试,传入包含重复ID的列表,并循环调用100次。发现约20%的情况返回重复数据。同步现象:打印每次请求的时间戳,发现所有调用似乎“串行”了。

第二步:检查并发实现

AI生成的核心代码片段(简化后)如下:

代码语言:javascript
复制
public List<User> getUsers(List<Integer> userIds) {
    // 去重保留顺序
    List<Integer> distinctIds = userIds.stream().distinct().collect(Collectors.toList());
    
    List<CompletableFuture<User>> futures = distinctIds.stream()
        .map(id -> CompletableFuture.supplyAsync(() -> fetchWithRetry(id)))
        .collect(Collectors.toList());
    
    // 等待全部完成
    List<User> users = futures.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.toList());
    
    // 去重(?)并排序
    return users.stream()
        .filter(Objects::nonNull)
        .distinct()  // 这里看起来多余但实际上没起作用
        .sorted(Comparator.comparing(User::getUserId))
        .collect(Collectors.toList());
}

第三步:深挖重复数据的来源

重复数据不是来自输入,而是在users列表中出现了相同userId的两个User对象。这意味着:

  1. 同一个userId被调用了两次?
  2. 或者fetchWithRetry对于同一个id返回了不同对象?

添加日志后发现:并发执行时,同一个userId的多次请求被同时发起了。但我们的distinctIds已经去重了,怎么可能?

进一步检查fetchWithRetry的实现:

代码语言:javascript
复制
private User fetchWithRetry(Integer userId) {
    int attempt = 0;
    while (attempt <= 1) {
        try {
            return externalApi.fetchUser(userId);
        } catch (Exception e) {
            attempt++;
            if (attempt > 1) throw new RuntimeException(e);
            // 重试前等待
            try { Thread.sleep(100); } catch (InterruptedException ie) {}
        }
    }
    return null;
}

单独看没问题。但结合调用上下文:AI没有考虑到 CompletableFuture.supplyAsync 默认使用 ForkJoinPool.commonPool(),而这个公共线程池在任务阻塞(如等待外部API、sleep)时,可能触发工作窃取并导致某些任务被重复执行?这个猜测方向其实错了——后来发现真正的原因更隐蔽。

根源揭秘

真正的原因出现在两个地方:

  1. 隐式共享状态externalApi.fetchUser内部使用了基于userId的缓存,但缓存的computeIfAbsent实现是非线程安全的(比如用了HashMap而非ConcurrentHashMap)。并发调用同一个userId时,多个线程几乎同时发现缓存缺失,然后各自调用真实API,并在之后put到非线程安全的Map中,可能导致数据错乱或重复插入。
  2. AI的“聪明反被聪明误”:AI在fetchWithRetry中加了Thread.sleep(100)做退避等待。在并发场景下,这个sleep不仅没有帮助,反而让ForkJoinPool的任务等待时间变长,配合默认并行度(CPU核数-1)不够大,导致大量任务被排队,整体执行时间大幅增加。

第四步:性能瓶颈的精确测量

用JMC(Java Mission Control)采样发现,超过60%的时间线程处于WAITING/TIMED_WAITING状态,主要源于:

  • Thread.sleep(100)在重试逻辑中
  • 外部API调用本身耗时约30ms
  • CompletableFuture.join()的阻塞等待

加上ForkJoinPool的默认并行度在我的机器上是7,但任务数量是100,线程切换和队列等待开销显著。

优化全记录

优化1:修复重复数据

代码语言:javascript
复制
// 方案A:确保缓存线程安全
private final ConcurrentHashMap<Integer, User> cache = new ConcurrentHashMap<>();
​
public User fetchUser(Integer userId) {
    return cache.computeIfAbsent(userId, id -> callRemoteApi(id));
}

computeIfAbsent在ConcurrentHashMap中是原子的,保证同一key的第一次计算只执行一次。

优化2:重构并发控制

放弃ForkJoinPool.commonPool(),使用自定义线程池,并根据任务特点调整参数:

代码语言:javascript
复制
private final ExecutorService executor = new ThreadPoolExecutor(
    20,  // corePoolSize 大于默认并行度
    50,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(200),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
​
public List<User> getUsers(List<Integer> userIds) {
    List<Integer> distinctIds = userIds.stream().distinct().collect(Collectors.toList());
    
    List<CompletableFuture<User>> futures = distinctIds.stream()
        .map(id -> CompletableFuture.supplyAsync(() -> fetchUserSafely(id), executor))
        .collect(Collectors.toList());
    
    // 不直接join,使用超时控制
    List<User> users = futures.stream()
        .map(f -> f.get(800, TimeUnit.MILLISECONDS))
        .filter(Objects::nonNull)
        .collect(Collectors.toList());
    
    // 此时数据已天然无重复(从源头保证),只需排序
    return users.stream()
        .sorted(Comparator.comparing(User::getUserId))
        .collect(Collectors.toList());
}

优化3:重试策略改进

去掉固定sleep,改用指数退避 + 快速失败

代码语言:javascript
复制
private User fetchUserSafely(Integer userId) {
    User cached = cache.get(userId);
    if (cached != null) return cached;
    
    int retries = 1;
    long waitMs = 50;
    Exception lastException = null;
    
    for (int i = 0; i <= retries; i++) {
        try {
            User user = externalApi.fetchUser(userId);
            if (user != null) {
                cache.put(userId, user);
                return user;
            }
        } catch (Exception e) {
            lastException = e;
            if (i < retries) {
                try { Thread.sleep(waitMs); } catch (InterruptedException ie) {}
                waitMs *= 2; // 指数退避
            }
        }
    }
    throw new RuntimeException("Failed to fetch user " + userId, lastException);
}

优化4:增加批量超时与降级

AI没有考虑到部分失败场景。增加降级逻辑:超时或失败的userId,返回默认User对象或跳过,不让整个请求失败。

代码语言:javascript
复制
// 在get()时使用超时,失败时记录日志并返回null
.map(f -> {
    try {
        return f.get(600, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
        log.warn("Fetch user timeout");
        f.cancel(true);
        return null;
    } catch (Exception e) {
        log.error("Fetch failed", e);
        return null;
    }
})

最终效果对比

指标

AI原始代码

优化后

重复数据概率

~20%

0%

平均响应时间(100 userIds)

1180ms

210ms

P99响应时间

1650ms

380ms

并发安全

失败降级

避坑指南 & 实用技巧

1. 警惕AI的“想当然”

AI经常默认使用ForkJoinPool.commonPool(),但它不适合I/O密集型任务(阻塞、等待多)。同样,AI喜欢在重试里加sleep,却在并发环境下放大延迟。

技巧:明确要求AI“使用自定义线程池,I/O密集型任务设置核心线程数较高(如CPU核数*2+1)”。

2. 共享可变状态是并发问题的温床

AI很容易写出HashMap + 缓存的模式,但几乎不会自动提醒线程安全问题。必须自己审查所有跨线程的共享变量

技巧:在提示词中加入“请确保线程安全,使用ConcurrentHashMap或同步机制”。

3. 永远对AI的“功能正确”保持怀疑

AI写的代码编译通过不等于逻辑正确。重复数据问题就是典型——它用了.distinct(),但源头上已经产生重复,后续去重也无法挽救。

技巧:单测覆盖边界条件(重复输入、并发调用、部分失败)。

4. 性能问题要量化,不要猜测

最初我怀疑是sleep导致,但通过JMC才定位到线程池调度和缓存竞争的复合问题。

技巧:准备一个性能测试基线,用工具(JMC、Async Profiler)替代直觉。

5. 让AI帮你审查自己的代码

这是个反向技巧:把你优化后的代码再给AI,问“这段代码在高并发下有哪些潜在问题?请指出三个”。AI往往能发现你忽视的边界情况。

总结

AI生成代码像是一个“热情但经验不足的结对程序员”——它速度快、不疲劳,但会依赖默认模式、忽视并发细节、对性能影响想得简单。作为开发者,我们的价值不是拒绝AI,而是:

  • 审查:特别关注线程安全、异常处理、资源管理
  • 测量:用数据验证性能,拒绝“看起来很快”
  • 迭代:把优化后的模式提炼成新的提示词,教会AI更好地产出

当你从AI那里复制Java代码时,不妨先问自己三个问题:它真的并发安全吗?失败时系统表现如何?有没有我本应该讲清楚的约束条件? 答案会让你少踩很多坑。

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:从“无脑复制”到“谨慎求证”
  • 背景与任务
  • 问题显现:功能与性能的双重“惊喜”
    • 现象1:偶尔返回重复用户
    • 现象2:响应时间远超预期
  • 排查过程:从表象到根源
    • 第一步:稳定复现问题
    • 第二步:检查并发实现
    • 第三步:深挖重复数据的来源
    • 根源揭秘
    • 第四步:性能瓶颈的精确测量
  • 优化全记录
    • 优化1:修复重复数据
    • 优化2:重构并发控制
    • 优化3:重试策略改进
    • 优化4:增加批量超时与降级
  • 最终效果对比
  • 避坑指南 & 实用技巧
    • 1. 警惕AI的“想当然”
    • 2. 共享可变状态是并发问题的温床
    • 3. 永远对AI的“功能正确”保持怀疑
    • 4. 性能问题要量化,不要猜测
    • 5. 让AI帮你审查自己的代码
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档