Skip to content

场景实战:多线程与并发问题调优 ​


❓ 面试实战:你们系统中哪里用到了多线程?怎么用的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 我们在“耗时任务异步化”和“多接口并行聚合查询”这两个高频场景中大量使用了多线程(线程池),将接口响应时间从秒级压缩到了百毫秒级。

📝 详细业务场景落地(面试高分答题模板):

场景一:耗时任务异步化(提升主流程响应速度)

  • 业务痛点:用户注册/下单成功后,系统需要:1. 写入数据库;2. 发送成功短信;3. 发送新用户优惠券;4. 记录操作日志。如果串行执行,整个接口可能要耗时 2~3 秒,用户体验极差(一直在转圈圈)。
  • 实战做法:
    • 核心主流程(写库)依然同步执行,耗时 50ms。
    • 非核心流程(发短信、发券、写日志),直接封装成 Runnable 或 Callable 任务,扔给自定义的业务异步线程池(如 AsyncEventThreadPool)去后台默默执行。
    • 主线程不用等它们执行完,直接返回“注册成功”。接口耗时瞬间降到 50ms。
  • 坑点防范:如果线程池满了或者服务器突然宕机,这些扔在内存队列里的异步任务会丢失。因此对于极度重要的非核心任务(如发券),我们最终会改用 MQ(消息队列) 来替代本地多线程,保证消息持久化不丢失。

场景二:多接口并行聚合查询(CountDownLatch/CompletableFuture)

  • 业务痛点:在“App 首页”或者“商品详情页”,前端调用我们一个接口,但后端需要去组装大量数据。比如查商品信息(200ms)、查店铺信息(100ms)、查商品评价(150ms)、查用户浏览历史(100ms)。串行查完总共需要 200+100+150+100 = 550ms。
  • 实战做法:
    • 我们使用了 JDK 8 的 CompletableFuture 进行多线程编排。
    • 将这 4 个查询任务分别提交给线程池并行执行。
    • 使用 CompletableFuture.allOf(f1, f2, f3, f4).join() 阻塞等待所有任务全部完成。
    • 最终耗时:取决于最慢的那个任务(即 200ms),性能直接提升了一倍多!

❓ 面试实战:线上如果出现了 CPU 100% 或者线程死锁,你怎么排查定位?【高级】 ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论):

  • 排查 CPU 100%:使用 top 命令找到最耗 CPU 的进程 PID -> 用 top -Hp 找到该进程下最耗 CPU 的线程 ID -> 把线程 ID 转成16进制 -> 用 jstack 导出线程快照,搜索该 16 进制定位到具体的 Java 代码行数(通常是写了死循环或者频繁 Full GC)。
  • 排查死锁:直接使用 jstack 或者 JDK 自带的 jconsole / jvisualvm,工具会自动帮你检测并在日志最后打印出 Found one Java-level deadlock。

📝 详细排查命令实操(死记硬背,面试必考):

第一题:排查 CPU 飙高

  1. 找出最耗 CPU 的进程 ID(假设是 10086):
    bash
    top -c
  2. 查看这个进程内部,哪个线程最耗 CPU(假设是 10090):
    bash
    top -H -p 10086
  3. 将十进制的线程 ID (10090) 转换成十六进制(假设结果是 276a):
    bash
    printf "%x\n" 10090
  4. 绝杀:使用 jstack 打印堆栈日志,并 grep 搜索这个十六进制的线程号:
    bash
    jstack 10086 | grep -A 20 "276a"
    结果:终端会直接打印出这个线程正在执行的堆栈信息,你一眼就能看出是哪个类、哪一行的 while(true) 代码导致了 CPU 狂飙,或者看到是 VM Thread 正在疯狂做垃圾回收。

第二题:排查死锁(Deadlock)

  • 死锁产生的四个必要条件:互斥、请求与保持、不剥夺、循环等待。我们写代码最容易犯的就是循环等待(A 拿了锁 1 等锁 2,B 拿了锁 2 等锁 1)。
  • 排查方法:直接执行 jstack 进程号,滑到日志的最底部。JVM 非常智能,它会明确告诉你:
    text
    Found one Java-level deadlock:
    =============================
    "Thread-A":
      waiting to lock monitor 0x0000... (object 2),
      which is held by "Thread-B"
    "Thread-B":
      waiting to lock monitor 0x0000... (object 1),
      which is held by "Thread-A"
    根据日志提示的代码行数,去修改加锁的顺序即可解决。

🌟 面试加分项(现代化工具): "当然,在现代企业级开发中,我们如果再去敲那一堆 Linux 命令行就有点过时了。我们线上通常会部署 Alibaba Arthas(阿尔萨斯) 诊断工具。 排查 CPU 100% 只需要敲一行命令:thread -n 3,它就会直接打印出当前最忙的 3 个线程的堆栈和耗费 CPU 的具体行数。 排查死锁也只需要一行命令:thread -b,瞬间揪出造成阻塞的罪魁祸首。"