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

文章内容收录到个人网站,方便阅读http://hardyfish.top/

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

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

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

数据竞争与可见性

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

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

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

复合操作非原子

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

  • 余额校验后扣款、库存校验后减库存、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,限制上游提交速度)+ 分批提交。
  • 网关与客户端:线程本地上下文未清理导致链路串线,改为显式传参或使用作用域清理策略。

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

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

大厂面试每日一题 文章被收录于专栏

大厂每日一道面试题!

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务