滴滴一面:在项目中使用多线程时遇到过哪些问题?

多线程带来的收益往往写在压测曲线上,代价则藏在"偶发、难复现、只在生产出"的问题里。

项目里遇到的坑,大多集中在并发正确性、资源治理、可观测性性能错觉四个层面。

并发正确性:最贵的是"偶发错误"

数据竞争可见性

同一份共享状态被多个线程读写,既可能写丢,也可能读到过期值。

最常见的表象是计数不准、状态机跳转异常、缓存命中率异常波动。

根因通常是缺少互斥与内存可见性保障(例如仅靠普通变量传递"已初始化/已关闭"信号)。

复合操作非原子

"先检查再执行"在单线程里很自然,在多线程里就是经典竞态:

  • 余额校验后扣款、库存校验后减库存、Map 里不存在则创建等场景,容易出现重复创建、超卖、重复扣减。
  • 即便单次读写是原子的,复合语义也不是。

死锁与锁顺序问题

两个线程分别持有锁 A、锁 B 并互相等待,表面是接口卡死、线程数上升但吞吐为零。

更隐蔽的是"锁顺序不一致":模块各自加锁没问题,一旦组合调用就触发环路。

活锁饥饿优先级反转

线程没有被阻塞,但一直在"礼让/重试/自旋",CPU 占用很高,业务没有推进,这是活锁。

某类任务长期抢不到锁或线程池资源则是饥饿。

实时性场景里,低优先级线程持锁导致高优先级线程等待,会出现优先级反转。

线程生命周期与资源治理:不是"开了线程就完了"

线程泄漏与失控增长

手动 new 线程或不受控的异步提交,遇到突发流量会把线程数顶到系统极限:

  • 上下文切换飙升,吞吐下降;
  • 句柄、栈内存被吃光,最终 OOM 或系统拒绝创建新线程。

线程池配置不当

常见错误包括:

  • 队列无限大:短期"稳定",长期延迟雪崩,最终积压到不可恢复;
  • 队列太小 + 拒绝策略不当:高峰直接丢任务或把调用方拖死;
  • I/O 密集与 CPU 密集混用同一线程池:I/O 阻塞把 CPU 任务饿死,或者 CPU 任务把 I/O 延迟拉长。

阻塞调用放错位置

把网络 I/O、磁盘 I/O、远程 RPC、数据库查询放在持锁区间或关键线程(事件循环、调度线程)里,会把"局部等待"放大成"全局卡顿"。

表现为 P99/P999 延迟突然抬头,且伴随线程堆栈集中在同一阻塞点。

取消与关闭不完整

服务下线、重启、发布时最容易出事:

  • 任务无法响应中断(interrupt)或取消信号;
  • 后台线程未停止导致进程无法退出;
  • 资源(连接、文件、锁)未释放,出现半关闭状态。

性能与容量:多线程不等于更快

锁竞争与伪共享

锁把并发变串行,竞争激烈时反而更慢。

伪共享(false sharing)则更隐蔽:多个线程频繁写位于同一缓存行的不同变量,导致缓存一致性流量暴涨,CPU 忙但吞吐上不去。

线程数超过硬件并行度

线程过多会让系统花大量时间做调度与上下文切换,典型症状是:CPU 使用率不低,但有效工作占比下降;RT 抖动变大;

同样的请求在高并发下反而更慢。

错把异步当并行

异步只是把等待从调用栈移走,不一定带来并行。

若最终仍被单个锁、单连接、单队列或单核瓶颈限制,线程增多只是在更快地堆积等待。

可观测性与排障:问题常常"看不见"

难复现与不可重放

竞态条件依赖时序,日志和断点会改变调度,导致"加日志就好了"。

一些问题只在特定 CPU 核数、特定负载、特定 GC 时机下出现,开发环境很难复刻。

日志与链路追踪串线

并发下使用线程本地变量(ThreadLocal)承载 traceId、租户信息、用户上下文。

如果在线程池中复用线程但未清理,会出现链路串号、权限串用、脏上下文污染。

指标口径误判

看 QPS、平均延迟往往不够,多线程问题更常体现在:

  • P95/P99 延迟、队列长度、拒绝次数、上下文切换次数;
  • 锁等待时间、线程池活跃线程数、任务排队时间;
  • CPU steal、run queue 长度等系统指标。

工程化陷阱:从"能跑"到"可靠"差一套规范

非线程安全组件被误用

常见于缓存、日期格式化器、随机数生成器、连接对象、集合类迭代器等,被多个线程共享后出现数据污染或崩溃。

问题常被误归因到"网络抖动/数据库慢",实际是并发写坏了内部状态。

回调与异常吞掉

异步任务的异常如果没有被统一捕获和上报,会变成"静默失败":业务看似正常,某些任务永远没做完,只在对账或数据校验时爆雷。

顺序一致性与业务幂等

并发消费/并行处理会打乱顺序,触发业务假设失效;消息重复投递或重试放大并发写入,若幂等缺失会造成重复扣款、重复发券、重复入账。

典型项目场景速写

  • 订单与库存:并发扣减导致超卖,最终通过"原子扣减 + 幂等单号 + 失败补偿"兜底。
  • 统计与计费:计数器在高并发下偏小或偏大,最终替换为分段累加/无锁结构或集中聚合。
  • 批处理与导入:线程池队列堆积导致内存吃满,改为有界队列 + 背压(backpressure,限制上游提交速度)+ 分批提交。
  • 网关与客户端:线程本地上下文未清理导致链路串线,改为显式传参或使用作用域清理策略。

这些问题的共同点是:线程带来的不是"写法变化",而是"时间与状态的组合数爆炸"。

多线程代码的风险不在于复杂,而在于复杂到无法凭直觉穷举。

相关推荐
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记5 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6166 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364576 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao7 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒8 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰9 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox9 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全