多线程并发编程教程

接入与开发

给后端与测试开发者的多线程并发落地指南:线程池怎么配、共享状态怎么防、压测怎么测、日志和降级怎么留,附排查顺序与不适用场景。

写完接口一压测就超时、跑批任务越跑越慢、偶发数据错乱复现不出来——这些卡点多半不是语法问题,而是线程模型、共享状态和资源边界没设计好。这篇不讲语言层面的入门语法,只讲工程里怎么把并发写稳:线程池与任务切分、共享状态与幂等、压测与观测、超时与降级,最后给一套线上出问题的排查顺序。

写完接口一压测就超时、跑批任务越跑越慢、偶发数据错乱复现不出来——这些卡点多半不是语法问题,而是线程模型、共享状态和资源边界没设计好。这篇不讲语言层面的入门语法,只讲工程里怎么把并发写稳。

只要你需要给后端和测试同学讲清楚落地写法,就按下面这条线走:先定线程模型和池参数,再处理共享状态与幂等,接着量出并发能力、补上超时与降级,最后留一套线上排查顺序。下面每一节都只给判断标准和可执行动作,不给绝对值。

先定线程模型,再谈线程池参数

很多人上来就调池参数,其实第一步是判断任务性质。CPU 密集的任务把线程数压到核数附近就够,再多只是增加上下文切换;IO 密集的任务等待时间占大头,才允许线程数远超核数。具体给多少,先按类型定区间,再去站内核对当时环境的实测数据,别照抄固定数字。

核心线程数
池里常驻的线程数量,决定平时能并发处理多少任务
最大线程数
队列满后允许临时扩到的上限,用来吸收突发流量
队列容量
等待执行的任务缓冲区,容量决定背压什么时候才反馈给调用方
拒绝策略
队列和线程都满时怎么办,是抛异常、丢任务还是交给调用方跑
线程命名
给线程起可识别的名字,否则后面日志里根本区分不出谁在干活
  • 先判断任务是 CPU 密集还是 IO 密集,两类任务的线程数量级完全不同
  • 线程池四件套一次定齐:核心与最大线程数、队列类型与容量、拒绝策略、线程命名
  • 别用无界队列兜底,它会把背压问题藏起来,最后表现成内存涨和响应变慢
  • 任务粒度按单批耗时切分:太碎锁和调度开销吃掉收益,太大一个慢任务卡住整条队列
  • 给池子设监控点:活跃线程数、队列深度、拒绝次数,这三个数没有就别谈调优

共享状态、锁与幂等:并发错误高发区

并发 bug 九成出在共享可变状态上。先把真被多线程读写的变量列出来,再逐个决定处置方式:能改成不可变对象就改,能用局部变量或线程封闭就隔离,实在要共享才加锁。这一步做完,能省掉大量后续的锁竞争问题。

状态类型处置方式什么情况下不适用
只读配置不可变对象,启动时加载配置需要运行期热更新时改用原子引用
线程内计数器局部变量或线程封闭需要跨线程汇总时改用并发容器
共享计数/累加原子类或并发容器需要多个字段一起保持一致性时改用锁
数据库记录乐观锁版本号或条件更新热点行冲突极高时可考虑串行化或分片
写操作重试唯一键、去重表或请求 ID业务本身允许重复写入时不必加去重

加锁有个基本纪律:只保护临界区,不要把 IO、网络调用、远程请求塞进锁里。锁粒度一大,等于把并发改回串行,还顺带把下游的延迟算进了锁等待里。数据库层面同理,并发改同一条记录优先用条件更新,别只靠应用层加锁。

重试和并发天生一起出现,所以写操作必须幂等。唯一键、去重表、请求 ID 三选一,否则一次超时重试就是一次重复下单。判断标准很简单:同一个请求发两次,结果必须和发一次一样;做不到这一点的接口,先别加重试。

压测与观测:并发数要量出来,不是猜出来

  1. 先量基线单请求串行压,记录延迟基线区间,后面所有结论都相对这个基线看
  2. 再按梯度加压按并发梯度逐档加,比如从低到高分几档,每档跑够稳定时间再记录
  3. 记录拐点找到吞吐不再涨、延迟开始陡升的那一档,那才是真实并发能力
  4. 看错误爬升同时记录错误率,错误率抬头的位置通常就是保护机制该生效的位置

压测不是只看 QPS。要同时关注四类指标:吞吐、P95/P99 延迟、错误率、资源水位(CPU、内存、连接数、线程池队列深度)。只看平均值会漏掉尾延迟,很多线上超时就是被少数慢请求拖出来的。基准数字到站内工具页跑一遍当时的接口速度更有参考价值。

  • 日志里至少要能还原一次请求:请求 ID、线程名、排队耗时、执行耗时、重试次数
  • 偶发问题靠日志不靠复现,把线程名和请求 ID 打通才分得清是排队久还是执行慢
  • 给线程池和连接池都加队列深度监控,排队深度比平均延迟更早报警
  • 压测环境和生产环境的核心参数要保持一致,不一致的压测结论只能当参考

超时、限流与降级:把最坏情况兜住

并发能力再强,也要给最坏情况兜底。第一件事是把连接超时和读取超时分开设,读超时按下游正常延迟加余量,不要一个统一的大超时通吃所有调用——那样只会让线程被长时间占住,把局部问题放大成整体雪崩。

保护手段怎么设适用判断
连接超时按网络往返量级设短一些下游不可达时快速失败,别让线程卡在握手
读取超时按下游正常延迟加余量下游延迟波动大的接口要留更宽余量
重试只对可重试错误,指数退避加抖动,设次数上限写操作重试前先确认幂等
限流按下游容量设阈值,触发后快速拒绝自身是瓶颈时优先限自己出口
降级返回缓存、兜底值或异步化关键路径必须有降级开关

重试要克制:下游并发限制触发后按指数退避加抖动重试,次数设上限,并且只对可重试的错误重试。重试本身也是流量,发得太猛只会把下游推得更远。降级开关要提前想好触发条件,是自动触发还是人工拨,先保证不雪崩再谈恢复。

上线前还有一件事:想好转回去的办法。线程池参数和限流阈值都要能热改或有明确回滚项。如果改一个并发参数只能靠重启,那这套配置在故障时基本等于不可用。这类可运维性判断,可以顺带看看站内对中转服务的稳定性检查思路,逻辑是相通的。

线上并发问题排查顺序

线上出并发问题,最容易犯的错是上来就翻代码。正确做法是按顺序缩小范围,四步走完基本能定位到方向。顺序不要跳,跳步容易在无关方向上耗时间。

  1. 先看错误类型超时、拒绝、限流、数据错乱,四类对应完全不同的方向,先分类再动手
  2. 再看资源线程池队列深度、活跃线程数、连接池占用、GC 情况,判断是排队还是执行慢
  3. 然后看依赖下游接口延迟是否同步上涨,确认是自身问题还是被下游拖住
  4. 最后看数据只有涉及写操作和数据错乱时,才翻数据库锁等待与版本冲突记录

有些场景不适合硬上多线程:单机低并发、任务本身强顺序依赖、下游是不可并发的第三方接口。这几种情况硬上多线程只会放大问题,不如老老实实串行加超时控制。判断标准是:加并发换来的吞吐提升,能不能覆盖掉它引入的复杂度和故障面。

下一步把上面的池参数、幂等键和排查顺序落到你自己的服务里,再拿站内工具页的测速和价格追踪数据对照当时的实际环境,会比凭感觉调参稳得多。

常见问题

线程数到底该开多少,有没有一个通用公式?

没有能照抄的通用值。判断顺序是:先看任务类型(CPU 密集还是 IO 密集),再看下游能承受多少并发。CPU 密集从核数起步试探,IO 密集按「核数 × 单个任务等待占比」估算再实测收敛。任何公式都只是起点,最终以压测出来的吞吐拐点为准,具体基线可以到站内接口测速页量一遍。

并发写数据库经常出现重复数据,怎么防?

优先在数据库层解决:唯一索引或去重表挡住重复写入,更新用版本号做乐观锁或带条件的原子更新,别指望应用层加锁。再配合幂等设计——每个写请求带唯一请求 ID,服务端落库前查一次。注意这套只在写路径有效,纯查询接口不需要。

本地压测都正常,上线就超时,从哪查起?

先对比环境差异:实例规格、连接池上限、下游实例数和网络延迟。常见原因是本地并发没真正压起来、或者下游在线上是共享资源被别的业务占满。排查按「错误类型 → 线程池与连接池水位 → 下游延迟 → 日志还原单次请求」的顺序走,别一上来就改线程数。超时参数怎么分档设,可以对照站内超时配置那套思路。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。