滴滴一面:在项目中使用多线程时遇到过哪些问题?
文章内容收录到个人网站,方便阅读: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,限制上游提交速度)+ 分批提交。
- 网关与客户端:线程本地上下文未清理导致链路串线,改为显式传参或使用作用域清理策略。
这些问题的共同点是:线程带来的不是“写法变化”,而是“时间与状态的组合数爆炸”。
多线程代码的风险不在于复杂,而在于复杂到无法凭直觉穷举。
大厂每日一道面试题!
字节跳动公司福利 1363人发布
查看23道真题和解析