并发、并行与异步:从概念到实战彻底讲透
1. 从一次线上事故说起先把并发到底是什么讲明白上次值班我负责的一个消息推送服务突然报警接口延迟从个位数毫秒涨到好几百毫秒看监控CPU 利用率不高网络带宽也没打满内存还剩一大半可请求就是一直排队。同事第一反应是高并发把线程池打爆了准备去调 JVM 参数。我对着火焰图盯了十分钟最后定位到的问题根本不是线程不够而是一把锁把本该并行的流程强行串行化了。这场景做后端的人估计都熟——大家嘴上天天挂着并发、并行、异步可真到压力测试或者事故排查的时候三者之间到底是什么关系、各自解决什么问题、怎么组合使用很多人心里其实是含糊的。面试的时候能背出并发是逻辑上的同时并行是物理上的同时异步是不阻塞但换个角度问你这个服务到底该加线程还是该上协程这段代码慢是因为并发不够还是因为异步写法错了立刻就卡壳了。这篇文章我想把这件事彻底说透。我不打算从教科书定义讲起而是用一个一个实际场景把这三个概念拆开、对比、串起来告诉你它们分别在什么层面起作用什么时候该用哪个什么时候要混着用。内容覆盖面会比较杂——从操作系统线程、数据库锁、消息队列到多卡训练、硬件总线都会有所涉及。因为并发、并行、异步本来就是跨层概念Nginx 的百万连接靠异步事件驱动LAMMPS 的多核模拟靠数据并行数据库的高并发安全靠锁和事务AI Agent 的高并发靠的则是异步编排加多卡推理的组合。把这些都摆到一起看你才能形成自己的判断力而不是背几个名词。为了方便对号入座先说明一下这篇文章适合谁正在做后端服务、对高并发架构有疑惑的开发者做数据分析或科学计算想搞清楚多核并行和异步之间差别的朋友以及面试前想把并发相关问题彻底理清楚的求职者。看完之后你起码能做到面对一个性能问题先分清楚它是并发调度的锅、并行不够的锅还是异步阻塞的锅。2. 并发、并行、异步到底在解决什么不同的问题先说一个我观察了很久的现象很多人把这几个词当近义词用甚至在实际代码里混着用。问并发和并行有什么区别标准答案是一个是交替执行一个是同时执行。这话方向没错但不完整而且漏掉了异步这个维度导致后续很多分析都建立在一个模糊的基础上。我的分法比较简单先看三个核心问题并发解决的是多件事同时存在的问题。它强调任务之间能够交错推进、共享有限的处理资源目的是在单位时间内塞进更多的任务。并行解决的是多件事真正同时推进的问题。它依赖多个执行单元在同一个时刻各干各的目的是缩短完成一个大任务所需的时间。异步解决的是调用方不必干等一件还没做完的事的问题。调用发起后立即返回等事情完成时通过回调、事件、消息或协程恢复来通知调用方目的是提升系统的整体吞吐和响应速度。打个比方。你是一个只有一套灶具的厨师同时来了三桌客人并发你不需要一桌做完再做下一桌。炒菜的间隙顺手蒸一锅饭切菜的间隙去把汤烧上所有任务在时间上交错安排这就是并发。并行现在厨房给你配了两套灶具你可以同时炒两个菜火候互不耽误这就是并行。异步你让服务员去给客人下单服务员把单子贴到窗口就回来接下一单菜做好之后厨房按单出餐服务员再来取。客人不需要盯着你炒菜你也不必等客人吃完才接下一单。这个比喻虽然简单但能解释很多实际问题。单核 CPU 上只能做并发不可能做并行多核 CPU 上才能有真正的并行执行而异步和硬件核数没有直接关系单核单线程照样能异步。再补一个关键的理解维度很多人纠结并发数这个词。并发数并不是线程数也不是连接数它描述的是某个时刻系统里处于处理中的请求/任务数量。一个线程可以同时处理多个并发请求异步模式一个请求也可能因为排队而在系统里逗留很久。Nginx 动辄支持几十万连接不代表它有几十万个线程在工作而是因为它用事件循环同时追踪大量连接真正正在处理的请求可能只有一小部分。我把几个概念的关键差异做成一张表方便对照记忆维度并发并行异步核心问题多任务如何交替推进多任务如何同时推进调用方如何不被等待拖住必要条件任务可切分、可调度多核/多执行单元事件循环、消息队列、回调、协程单核可用可以不行可以关键开销上下文切换、调度数据切分、结果合并、同步回调管理、事件调度典型场景线程调度、协程、Nginx 连接管理多核计算、LAMMPS、多卡训练Node.js、asyncio、消息推送本质追求利用率速度响应性与吞吐这张表建议收藏后文每一步分析都会回来看它。接下来逐个深入。3. 并发的实现路径从线程、锁到协程的一路演进讲并发之前得先承认一个反直觉的事实并发本身不等于效率提升。线程的创建、调度、切换都有成本如果是纯计算型任务单核上开多线程反而更慢。并发真正的收益在于等待的间隙能被利用起来。一个线程在网络请求返回前长时间阻塞如果不让出 CPU整个核就只能空转如果多个线程交替CPU 就能在等待期间去执行其他任务。所以高并发系统设计的本质就是在一堆等待和计算之间寻找最优调度策略。3.1 进程、线程、协程三级并发载体的成本对比第一级是进程。进程之间内存隔离某个进程崩溃不会拖垮整个服务安全性好。但进程切换需要换地址空间开销大创建数量也受资源限制所以大型服务不会为每个用户建一个进程。第二级是线程。线程共享进程内的地址空间切换成本比进程低是服务器端最经典的并发单位。Java 早期的一连接一线程模式就是这么来的。但这种模式的问题在连接数上来之后暴露无遗每来一个请求就新建线程线程一多CPU 大量消耗在上下文切换上真正的业务计算反而没分到多少时间。第三级是协程。协程跑在线程之上有自己的栈和上下文但切换不需要经过操作系统内核由用户态调度器完成切换代价比线程低一到两个数量级。Go 的 goroutine、Python 的 asyncio 任务、Java 21 的虚拟线程都属于这个方向。所谓百万并发连接的底气正是协程把每个连接的成本压到了极低。这里我必须强调一个最常见的误解协程不解决并行问题。哪怕你开一万个 goroutine在单核上它们依然是交错执行想让它们真正并行必须依赖多个物理核心。很多刚接触 Go 的同事默认goroutine 会自动用满多核这是错的——要不要并行取决于你把工作负载怎么拆分、调度器能不能把任务分到不同 P 上执行。这个问题如果理不清后面调优必踩坑。3.2 锁与竞态的本质共享可变状态需要秩序并发里最令人头疼的从来不是调度本身而是多个任务同时访问共享数据。两个线程同时读改写同一个变量结果既不是线程 A 的写入结果也不是线程 B 的写入结果而是中间态——这就是竞态条件。不处理竞态轻则数据错误重则程序崩溃。从 Java 的 synchronized 到 Python 的 Lock再到 Go 的 Mutex加锁的原理都是一样的进入临界区之前先获取许可出来再释放。听起来不难但锁一用错比不用锁更危险。我最常遇到的线上事故就是死锁两个线程各持有一把锁又同时去要对方的锁谁也不让整个调用链卡死。import threading import time lock_a threading.Lock() lock_b threading.Lock() def worker_x(): lock_a.acquire() time.sleep(0.01) # 人为放大竞争窗口 lock_b.acquire() # 等锁 B lock_b.release() lock_a.release() def worker_y(): lock_b.acquire() time.sleep(0.01) lock_a.acquire() # 等锁 A lock_a.release() lock_b.release()这段代码就是教科书式的死锁两个线程拿锁的顺序恰好相反。没有明显错误提示没有异常日志唯一的症状是相关请求永久性超时。我在团队里定的规矩很简单多把锁必须按固定顺序获取要么永远先 A 后 B要么就在设计阶段直接消除多锁依赖。死锁的重试机制比如数据库里的死锁检测只是兜底不能当主要依赖。3.3 从可见性到内存屏障并发不只是锁里面那点事再深挖一层并发问题还有一个隐蔽来源——CPU 缓存导致的可见性问题。每个核都有自己的 L1/L2 缓存线程 A 在一个核上修改了变量线程 B 在另一个核上读到的可能还是旧值。这就是为什么 Java 里不加 volatile 的多线程变量会读不到最新值为什么 Go 的 race detector 能查出看似无辜的共享变量读写。加锁之所以能同时解决互斥和可见性是因为锁的原语里带了内存屏障释放锁时把修改刷出到主存获取锁时把缓存失效重新读取。这一点在写无锁代码时尤其需要留意——很多手写的无锁队列用了一个又一个原子操作但漏了内存序结果低负载时跑得欢高负载下数据莫名错乱。我的建议是绝大多数项目不要自己造无锁结构直接用语言和生态提供的并发容器真正需要无锁的时候先把内存序和 cache line 对齐这类问题研究透再说。3.4 硬件里的异步复位与并发时钟和软件是同一个套路并发和异步不只是软件概念。做 FPGA 和数字电路的朋友应该熟悉异步复位同步释放——异步复位信号可以随时让电路复位但释放时如果和时钟沿冲突可能导致寄存器进入亚稳态所以要先经过同步器再释放。这个思路映射到软件里就是异步事件可以随时到来但进入共享状态前必须先做同步规整跟线程池里突然到来的异步回调要先入队再处理本质上是一回事。理解了这一层你再去看异步 FIFO、跨时钟域这些硬件术语就不会觉得它们是另一个世界的知识了。4. 并行的落地场景多核调度、数据切分与硬件并行并行要解决的是更快。它不是把任务随便拆一拆就能跑的需要把一个大问题分解成若干相对独立的子问题再让多个执行单元同时推进最后合并结果。软件里最常见的三种并行方式是数据并行、任务并行、流水线并行。4.1 数据并行 vs 任务并行为什么会决定加速比上限数据并行的核心是分数据不分逻辑把大块数据切成多份每个执行单元跑同一段逻辑处理自己那份。矩阵乘法是最直观的例子——把矩阵按行切块每个核算一块最后汇总。这种模式对算法依赖小只要数据分片和结果合并没有严重的依赖关系加速比可以接近线性提升。任务并行则是分逻辑不分数据把一条处理链上的不同阶段分配到不同执行单元上形成流水线。比如数据处理流程里的读取、解析、计算、写库四个阶段分别交给不同的线程去跑。任务并行最大的敌人是负载不均衡——某个阶段慢一点整个流水线就被拖住了。所以工程实践中流水线方案往往要配合双缓冲、背压、动态调度这些手段不像数据并行那样天然好扩展。一个特别典型的实战案例是 LAMMPS 的并行安装和运行。LAMMPS 是分子动力学模拟软件要模拟几十万到上亿个原子的运动单核推演根本跑不动。它的并行方式是空间分解把模拟盒子切成多个子区域每个 MPI 进程负责自己区域内的原子进程之间只交换边界原子的坐标和力。子区域之间耦合很弱所以增大核数可以近似线性提升模拟规模和速度——这就是教科书级的数据并行。你在 Ubuntu 上编译 LAMMPS 时看到的 MPI 依赖、OpenMP 选项、GPU 加速选项背后都在围绕把空间切好、把通信同步好这两件事做文章。4.2 并行不止发生在 CPU 上也在总线、网卡和芯片里很多只写应用层代码的人容易把并行等同于多核 CPU。实际上硬件领域的并行先例比比皆是。AD7606 是一颗 16 位多通道同步采样 ADC它的并行体现在多个模拟通道同时采样然后通过并行总线一次性把多通道结果读走。软件里讲并行是多个线程在同一时刻各算各的硬件里讲并行是多个物理通道在同一时刻各采各的底层思想同出一源。网络设备的并行也值得留意。现代网卡有多队列RSS机制可以把不同连接的数据包哈希到不同的收包队列再由多个 CPU 核并行处理。这其实也是在数据层面做切分——按连接维度把流量拆开让每个核只处理自己那部分避免锁竞争。如果你在调优高并发网关时发现只有一个核在忙、其他核闲着大概率就是网卡队列和 CPU 的亲和性没配好数据没被充分并行化。4.3 从四卡并行到 DDP多卡训练要解决的全新同步问题最近经常看到四卡并行方案PyTorch DDP 并行这些词训练大模型时并行问题被提到了台面上。PyTorch 的 DistributedDataParallel 属于数据并行把模型复制到多张卡上每张卡处理不同的数据批次每一步训练结束时通过 all-reduce 集合通信把各卡的梯度求和再同步更新所有副本。这里要注意DDP 的隐含假设是模型能塞进单卡内存如果模型大到一张卡装不下就得改用模型并行、张量并行、流水线并行了。多卡并行里最容易被低估的是通信瓶颈。四张卡之间走 PCIe 或 NVLink每一步训练都要同步梯度通信时间一旦超过计算时间加速比就会严重打折。所以分布式训练社区特别强调计算通信重叠把下一批数据搬运和当前梯度同步放在同一个时间窗口内让 GPU 在等待通信的时候也在算。你看这里就是并行和异步的交叉——多卡同时计算是并行计算与通信重叠是异步。两者叠加才让集群训练的效率真正上去。5. 异步的核心机制事件循环、回调与 async/await并发和并行都在讨论任务怎么跑异步讨论的则是调用方怎么不被卡住。异步的底层机制其实特别简单事件循环加完成通知。它不依赖线程数量多单线程内部就可以实现很高的 I/O 吞吐——因为大部分时间线程都花在等网络、等磁盘、等下游返回上异步把这些等待时间重新分配给其他任务了。5.1 从回调地狱到 async/await异步写法的一路演变早期 JavaScript 的异步全靠回调函数读文件读完回调发请求返回回调。操作一多嵌套层级加深代码变成一座金字塔俗称回调地狱。代码可读性差还是小事更麻烦的是错误处理——嵌套太深之后任何一个回调里漏了 catch错误就不知道飘到哪去了。Promise 的出现解决了链式组织和错误传播的问题。它把异步操作包装成对象用 then 串起来用 catch 统一接错。再后来async/await 直接写成同步式的语法底层仍然是 Promise 加事件循环但代码读起来像从上到下的顺序执行async function fetchUserData(userId) { const user await fetch(/api/user/${userId}); const orders await fetch(/api/orders?uid${user.id}); return { user, orders }; }我见过不少人对这段代码有误解以为用了 await 就变成同步等待了。实际上await 并不会让整个线程停下来等网络返回它在遇到网络等待时把当前协程挂起让出执行权事件循环去处理别的连接等网络响应回来再恢复执行。这才是异步真正的价值看起来像同步代码单线程却能同时服务成千上万个请求。5.2 Python asyncio同一个模型换个语言依然是那套逻辑Python 的 asyncio 也是事件循环模型。一个线程里跑一个 event loop不断检查哪些 I/O 事件就绪、哪个协程可以恢复执行。配合 await 关键字线程在等待 I/O 的间隙就会切去执行其他协程。用代码说明import asyncio import httpx async def fetch(url): async with httpx.AsyncClient() as client: resp await client.get(url) return resp.status_code async def main(): # 同时发起 10 个请求线程在等待期间来回切换 results await asyncio.gather(*[fetch(fhttps://example.com/{i}) for i in range(10)]) print(results) asyncio.run(main())这里 asyncio.gather 同时发起了 10 个请求但它们不是并行执行的 10 个线程——在单线程事件循环里它们是交替推进的 10 个协程。执行顺序由 I/O 就绪情况决定谁先返回谁先恢复。这条认知线很多新手一直没搭起来总以为异步就是多线程的另一种叫法结果遇到 CPU 密集型任务也往协程里塞反而把整个事件循环卡死。我刚入行时就犯过这类错误在一个 FastAPI 的异步视图里直接调了同步的 requests.get结果某个下游接口一慢整个服务的所有请求都跟着变慢。原因很简单——requests.get 是同步阻塞调用它把事件循环所在的那个线程堵住了其他所有协程都得排队等它结束。后来换成 httpx.AsyncClient问题才彻底解决。这个坑背后的排查思路我留到第 7 章细讲。5.3 异步在生产里的日常形态定时任务、消息通知、验签处理日常后端服务里异步不止体现在接口调用。我总结了三类最常见的异步场景异步定时任务凌晨跑报表、定期清理过期数据、定时拉取上游对账单。这类任务通常由调度框架触发执行放在独立线程池或独立进程里避免把主服务的响应拖慢。异步通知与验签支付回调、开放平台 Webhook 这类场景外部系统通知我们之后我们不能在 HTTP 响应里同步做验签和处理否则容易超时。正确做法是先接收消息、立即返回把完整的验签和业务处理放到消息队列的消费者里异步执行。验签必须在消费端做不能省否则伪造请求可能绕过校验。跨系统解耦的异步消息生产者不等待消费者消费者也不会因为生产端瞬时暴涨而崩溃中间全靠一个队列缓冲。这就是软件版的异步 FIFO——不同速率的生产者和消费者之间用缓冲区解耦底层思想和硬件里的异步 FIFO 跨时钟域传递数据完全一致。异步的通用套路归纳起来就一句话发起请求时不等待结果注册一个完成时做什么的机制真正干活的系统在后台推进完成后通过回调、状态变更、消息或者协程恢复把结果带回来。6. 生产环境的组合拳IM、数据库锁控与 AI 服务的并发骨架单讲概念容易飘必须落到真实场景里。这一节我选三个典型场景把并发、并行、异步是怎么组合出拳的讲清楚。6.1 高并发 IM 与 Nginx事件驱动连接异步流转消息IM 系统是并发和异步结合的典型。先看连接侧一个 IM 服务要维持几十万甚至上百万的长连接如果每个连接配一个线程光线程栈内存就能把一台机器压垮。所以主流方案是事件驱动——连接来了注册到事件循环上有消息进来就把回调推进处理逻辑。Nginx 能支撑高并发连接用的就是同样的思路epoll 同时监听大量文件描述符worker 进程根本不按连接数起线程而是根据事件的就绪状态去处理。这也是热词里nginx 最大并发连接数老是用超这个问题的根源之一。许多人以为把 worker_connections 调大就行但真正决定上限的是后端处理方式是否阻塞。如果 worker 进程里跑的是同步阻塞代码一个慢请求就能占住整个 worker连接再多也只是在排队调大参数只会让延迟更难看。再看消息流转。IM 里用户发一条消息从客户端到服务器再到接收方中间要经过网关接入、消息存储、离线推送、实时路由。任何一个环节都不能让入口线程阻塞等待。标准做法是网关收到消息后立刻写入消息队列或协调通道向客户端返回已收到后台消费者异步处理存储和推送。这种异步削峰的设计让消息在峰值时刻也能平滑流过系统。6.2 数据库并发锁乐观锁、悲观锁以及事务边界数据库并发锁是高并发场景绕不开的话题。锁的选型主要看冲突概率冲突少用乐观锁冲突多用悲观锁。悲观锁是先拿锁再干活。SELECT ... FOR UPDATE 把相关行锁住防止其他事务修改。适合订单扣减、库存扣减这类竞争激烈的场景。缺点也明显持锁期间其他事务要等待锁等待时间一长数据库的活跃连接就会被拖垮。乐观锁是用版本号或条件更新来模拟 CAS 思想UPDATE inventory SET stock stock - 1, version version 1 WHERE sku_id 12345 AND version 7;如果影响行数为 0说明版本号变了本次更新失败业务层决定是重试还是返回错误。乐观锁没有持锁等待吞吐更高但冲突率高时重试成本会放大反而额外消耗数据库连接和业务资源。比选锁更重要的是事务边界。我在团队里反复强调事务里绝不放慢网络调用、外部接口、长时间计算。否则哪怕用了乐观锁也会因为长事务把大量请求拖进等锁状态。高并发数据库调优的第一课永远是缩短事务时间减少锁的持有范围。6.3 AI Agent 怎么扛并发接入异步、编排并发、推理并行最近很多人问AI Agent 怎么扛并发。Agent 服务和普通 Web 服务不太一样一次请求可能要调用多个模型每个模型响应动辄几秒到几十秒而且普遍采用流式输出。这种场景下瓶颈不是连接数而是下游推理资源。我习惯把 Agent 服务的并发骨架分成三层接入层用异步框架接收请求比如 FastAPI async。尽快把任务写入消息队列不让 HTTP 连接长时间占用线程。编排层Agent 逻辑里多个模型的调用如果相互独立用 asyncio.gather 或类似机制同时发起把串行的先后等变成并发的同时等。但这里要清醒同时发请求不代表下游真的在并行处理如果模型服务只有单机单卡十个请求可能还是排队执行。推理层真正的瓶颈在这里。要提升单机吞吐需要用多卡并行把模型同时跑在多个设备上一张卡处理一个 batch多卡同时推理不同请求。这就是前面第 4 章讲的并行问题。三层组合起来才是健康的 Agent 并发架构接入层异步化负责消峰编排层并发调用负责等待期的利用推理层多卡并行负责吞吐上限。每一层都有自己的核心概念少了任何一层系统都会在某个环节卡住。7. 我踩过的并发坑竞态、死锁与隐式阻塞的排查链路理论说再多不如亲自踩坑。下面分享几个我线上遇到过、一步一步排查出来的真实问题。这些坑如果提前知道能帮你省下无数个本应好好睡觉的夜晚。7.1 统计字段并发加一数值怎么都对不上现象某个统计服务每天定时把各用户的会话数汇总到内存 Map 里。上线第三天数字不对了——明明完成了 1000 个会话统计结果只有 900 多。排查链路先看日志发现两个线程几乎同时打印了即将加一当时觉得顺序正常没发现问题。再看代码发现更新逻辑是标准的读-改-写三步不是原子操作。两个线程同时读到旧值各自加一再写回最后只加了一次。这是最经典的竞态条件。解决方案改用 AtomicLong 的原子自增或给更新操作加锁。最后我选了 CAS 版本因为它在这里就是一条 CPU 原子指令没有线程阻塞。这个案例的教训是并发问题平时不一定报错它往往在高负载时悄悄破坏数据单靠测试根本发现不了必须靠代码审查和工具。Go 有 race detectorJava 有各种并发检查工具没事多跑跑比出事再查强。7.2 异步任务里的异常被吞掉消息像人间蒸发现象消息队列的消费者偶尔会漏处理一些消息数量不多但没有任何错误日志消息像是凭空消失了。排查链路先看消费确认机制——确认消息确实被取出再看消费者代码发现某个异步分支里直接调了第三方 SDK而 try-catch 只包住了同步段异步回调里的异常根本没被捕获。线程池和事件循环抓不到子任务或回调里的异常消息框架以为任务执行成功把消息确认掉了可业务其实没做完。处理办法给团队立了一条规矩——所有异步入口必须有兜底异常处理回调或协程内必须 catch 所有 Throwable消息消费环境配合手动 ack 和重试异常时不要把消息标记为成功让未完成的消息重新进入队列。异步系统里异常捕获的边界一定要比同步系统更严格因为你根本不知道异常会从哪个线程冒出来。7.3 异步框架里混进同步阻塞调用整体延迟雪崩现象一个用 asyncio 写的小服务平时几百并发毫无压力某天出现一个慢接口后整个服务所有请求延迟一起恶化。排查链路先看 CPU不高再看等待分析所有协程几乎都在等同一个资源吞吐接近归零。顺着日志查调用链发现某段代码在 async 函数里调了同步的 requests 去请求第三方接口。这个慢接口一出现整个事件循环线程被阻塞其他所有协程全部排队。说白了就是异步语法 同步实现的错配。修复方法把所有第三方调用换成 aiohttp 或 httpx.AsyncClient加上超时和重试问题彻底消失。这类问题的排查方法我整理成了一个三步判断法看延迟是局部还是全局——只有个别接口慢去查那个接口的下游全链路一起变慢多半是公共资源被堵住了比如锁、全局队列、事件循环。看 CPU 高不高——CPU 高通常是计算密集或自旋等待CPU 不高但吞吐低一定是等待型阻塞重点查锁和网络等待。看异步代码里的同步阻塞点——把所有第三方调用的客户端翻一遍凡是 async 语境里出现同步 IO 的全是最可疑的嫌疑人。7.4 很多并发问题其实是设计问题还有一类问题要特别提醒表面上是并发难题根源是架构设计。比如所有线程都在抢同一个全局锁压力一大吞吐骤降——与其调锁参数不如把全局锁拆成分片锁或者按 key 分桶甚至干脆改成队列串行化。再比如数据库死锁频繁根源其实是多个事务以不同顺序改了同一组表那就从应用层统一更新顺序这比依赖数据库死锁重试靠谱得多。遇到并发问题第一反应不应该是我要换什么高级并发原语而是先问这个共享状态能避免吗这个锁的粒度能再小吗这段逻辑的顺序能统一吗把设计理顺大部分并发问题会自己消失。8. 我的并发决策心法与调优顺序最后不夹带新概念了分享几条我反复验证过的决策原则可以直接当 checklist 用。8.1 第一原则先减少共享再缩小锁的范围最后才考虑性能优化如果两个任务之间没有共享状态就不存在竞态不需要任何锁。这是绝对的最优解。实践中可以优先做写独立缓冲最后合并、用不可变对象传数据、用消息队列替代共享内存。当确实需要共享时减小锁的粒度不要在锁里面执行网络请求、文件写入、外部接口调用这类耗时操作。锁持有越短竞争窗口越小性能问题越少。8.2 第二原则IO 密集用异步计算密集用并行混合负载做分层怎么判断任务性质大部分时间在等外部系统返回——网络、磁盘、下游接口——就是 IO 密集型适合异步大部分时间在消耗 CPU 运算——数据解析、加解密、复杂计算——就是计算密集型适合多进程或多线程并行。实际系统里几乎都是混合的外层用异步框架接请求内部把 CPU 密集片段丢到独立线程池或进程池避免阻塞事件循环。8.3 第三原则并发数不是越大越好瓶颈在资源不在参数大家总爱调大各种线程池、连接池、超时时间。但高并发系统真正的瓶颈往往是资源不是数字。线程上限由内存和切换成本决定连接上限由文件描述符和事件循环能力决定吞吐上限由链路里最慢的环节决定。与其把线程池从 200 调到 2000不如做一轮压测找到真正的瓶颈到底在哪。压测的时候别只盯着 QPS。P99 延迟和错误率才是要命的指标——QPS 很高但 P99 已经漂到几秒说明系统在超卖错误率悄悄往上走说明系统已经过载。这时候要做的不是加参数而是限流、排队、削峰让系统在可承载范围内稳定输出。8.4 一个从线程爆炸到吞吐翻倍的调优案例讲一个印象很深的小项目。某个报表服务早期实现是来一个请求就起一个线程查库、汇总、返回。上线后并发一高就疯狂报线程创建失败。第一次优化我加了固定大小 50 的线程池问题稳定了但吞吐只有几百 QPS。第二次优化我把查库逻辑改成异步版线程池没动QPS 直接翻倍。为什么因为查库本来就是等待型操作同步模式下 50 个线程里有 45 个在等数据库返回异步模式下这些等的时间被释放出来处理别的请求了。第三次优化我发现多个报表请求其实可以共享同一份原始数据于是加了缓存和批量查询命中后基本不再查库QPS 又上了一个台阶。这个故事想说的是顺序先降低单位请求的资源消耗再谈扩资源先异步化 IO再谈并行化计算。很多团队一上来就加机器其实软件层面的优化空间往往比硬件大得多。8.5 最后再说一句个人体会我做了这么多年并发相关的项目最大的感受是并发是让系统看起来同时在做很多事并行是让系统真同时做很多事异步是让系统不用等也能把事做完。三者不是对立关系而是一套可以叠加的组合拳。理解它们不是为了在面试中把名词背得滚瓜烂熟而是为了在系统真的扛不住的时候你能准确判断手里的工具是什么、该用在哪一层。从项目实操来看把共享最小化IO 异步化计算并行化这三件小事做好绝大多数高并发问题都能在架构层面解决一大半。剩下的交给监控、压测和限流去兜底而不是临时抱佛脚去调那些看起来很吓人的参数。希望这篇文章能帮你在下次排查线上问题时少走几步我走过的弯路。