并发编程

并发的本质

并发系统模型

并发真正要解决的问题是:多个执行主体在不确定时间顺序下,对状态进行协作的问题。

并发系统 = 执行单元(谁在运行) + 状态(谁被修改) + 时间交错(顺序是否确定) + 协调机制(如何避免冲突)

并发的核心矛盾

共享(协同)vs 隔离(避免冲突)

所有并发模型都在这两个极端之间权衡:

模型倾向
Actor隔离
CSP隔离
STM共享
线程+锁共享

并发问题分析

充分必要条件

并发问题必须同时满足三个条件:多个执行主体 + 共享可变状态 + 时间交错

消除任意一个条件:

消除项结果
不共享无竞争
不可变无修改
无时间交错无并发

并发问题不在于线程,而是共享。

错误统一抽象

并发错误本质上是协调失效

类型含义本质
安全性不做错事状态协调失效
活跃性系统持续推进进度协调失效

状态协调失效(安全性)

三个永恒问题:

问题本质根源
可见性修改是否被观察到缓存 / 重排
原子性操作是否被打断时间交错
有序性执行顺序是否一致编译器 / CPU 优化

错误本质:基于已经失效的观察结果做出了决策。

问题本质具体含义
竞态条件观察失效观察到状态为 X,基于它做决策,但在决策提交前另一个线程已把状态改成 Y,导致决策基于失效前提
脏读观察滞后读到另一个线程正在修改但尚未提交的值,这个值可能回滚或被覆盖
双重创建状态失真对象构造期间(构造函数未执行完),状态还不完整就被其他线程看到
ABA 问题时间错觉观察到的值与之前相同(A),但中间经历了 A→B→A 的完整迁移,掩盖了状态变化的历史

不变式(Invariant)是对象在生命周期中必须始终保持的业务条件,例如账户余额 >= 0、订单状态不可逆。所谓的线程安全是:无论执行顺序如何,对象始终维持其不变式(Invariant)。

线程安全 = 不变式始终成立

线程安全分类

分类含义风险来源需要同步代表
不可变安全天然线程安全无(状态不可变)不需要String、final 字段
封闭安全不共享无(执行单元私有)不需要栈变量、ThreadLocal
相对安全单操作安全复合操作被打断需要(针对单操作)AtomicInteger
组合不安全多步骤破坏不变式业务不变式未被保护需要(业务层面)多字段组合的业务对象

锁、CAS、事务都只是维护不变式的手段。

共享 + 可变 + 未受控访问 = 不安全

设计优先级:不共享 > 不可变 > 消息通信 > 乐观协调 > 互斥锁

进度协调失效(活跃性)

活跃性问题的本质是:协调结构失效导致无法推进,各方对"对方会让步"的假设循环依赖。

问题本质具体表现
死锁循环资源等待线程互相持有对方需要的锁,都阻塞等待
活锁持续协商但永不成功CPU 繁忙但系统无进展,始终在响应但无法推进
饥饿长期得不到调度机会某些线程永远无法获得所需资源

死锁:循环资源等待。Coffman 条件:互斥、占有等待、不可剥夺、循环等待。

活锁:持续协商但永不成功。特点:CPU 很高,系统无进展。

饥饿:长期得不到调度机会。典型:优先级反转、非公平锁。

内存模型:并发世界的抽象规范

内存模型是什么

内存模型是多线程环境下内存访问行为的抽象规范,回答的核心问题是:

一个线程的内存操作,何时对另一个线程可见?

它是硬件能力与编程语言之间的契约层——约定了你能期望什么,而不需要关心硬件怎么实现。

为什么需要内存模型

现代硬件为了性能做的事:

优化导致的问题
CPU 缓存线程看不到其他线程的修改(可见性问题
指令重排程序顺序与执行顺序不符(有序性问题
异步写入复合操作被打断(原子性问题

内存模型定义:当程序员写了 X 代码,真实执行时 Y 行为是否可以接受

内存模型定义三个保证

保证解决什么问题
可见性保证写入何时对线程可见(为什么他看不到我的修改)
有序性保证哪些操作顺序是强制的(重排到什么时候是错的)
原子性保证哪些操作是不可分割的(复合操作怎么保证完整)

happens-before:跨线程的保证

happens-before 是可见性 + 有序性的组合保证:

A happens-before B=B 能看到 A 的结果且A 在 B 之前排序

它不是时间概念,而是语言层(Java JMM / C++ Memory Model / Rust Send/Sync)共同遵守的理论基础。

内存模型的光谱

不同硬件的"一致性代价"不同:

类型特点代表
强顺序(TSO)代价内化,始终慢x86
弱顺序代价显式,平时快,需要时贵ARM, PowerPC
顺序一致所有线程看到一致顺序默认模型
强顺序模型:硬件帮你做所有屏障,编程简单但始终有开销弱顺序模型:需要时手动加屏障,性能更好但编程复杂

硬件到语言的链路

硬件(CPU 缓存 / 重排 / 原子指令)    ↓ 提供能力内存模型(定义契约)    ↓ 提供语法语言(Java volatile / C++ memory_order / Rust Send/Sync)    ↓程序员(使用 API)

硬件负责"能做到什么",内存模型负责"做到了什么效果"——这是必要的抽象层,让软件在不同硬件上有一致行为。

并发实践

并发控制哲学

并发控制不是"保护代码",而是协调状态访问顺序——即并发系统四要素中"协调机制"的落地。所有控制手段按是否保留冲突分为两层:

层级思路策略回扣
消除型让并发问题不成立隔离(消共享)、不可变(消可变)充要条件表、线程安全分类
协调型冲突存在,管理它乐观(假设冲突少)、悲观/互斥(假设冲突多)悲观与乐观差异

统一优先级:消除 > 协调,即 不共享 > 不可变 > 消息通信 > 乐观 > 互斥。共享越少,复杂度越低。

消除型:从根源让问题不存在

锁是共享之后的补救,隔离与不变则从根源消灭竞争。

最好的锁 = 不存在的锁
手段本质条件
隔离(线程封闭)对象仅属单执行单元(栈变量、协程局部、Actor 内部状态)不逸出执行单元
不可变共享不可避免时,共享不可变状态状态不可改 + final(禁重排)+ 构造期不逸出

二者共同点:无需同步、无需可见性保证、无时间问题。

协调型:冲突存在时如何管理

二者分歧在对冲突概率的假设

维度悲观/互斥乐观
假设冲突一定发生冲突是小概率
策略先限制,再执行先执行,冲突再处理
典型Mutex、synchronized、数据库锁CAS(先执行失败重试)、版本(MVCC、COW 快照读)
代价阻塞、上下文切换、死锁风险重试、ABA、自旋消耗(CAS 系)

安全发布

并发问题很多不是"锁问题",而是发布时机问题:对象在构造完成前就进入了共享域,再完美的锁保护的也是半成品。

发布:对象从私有域进入共享域(放入缓存、注册监听器、返回给其他线程)。

逸出 = 失控的发布:对象在未准备好前被其他执行单元访问。最危险情况是构造期间 this 逸出(注册监听器、启动内部线程),导致 final 语义失效、对方读到不完整对象。

安全发布的本质不是"有没有加锁",而是是否建立了 happens-before。因为 happens-before 同时保证:

四种安全发布手段,本质都是在"发布"这一时间点建立 happens-before,区别只在途径:

手段建立 happens-before 的途径
静态初始化JVM 类加载锁保证初始化先于任何使用
final 字段构造结束的 freeze 屏障,对正确发布的对象可见
volatile写-读屏障传递可见性与有序性
锁保护发布解锁 happens-before 后续加锁

性能与伸缩性的本质

并发不是免费的。其代价与上限由两条根本定律决定,"减少共享"等优化口诀都是它们的推论。

伸缩性的两个天花板

定律结论含义
Amdahl 定律加速比上限 = 1 / 串行占比串行部分决定天花板,再多核也无法突破
USL(通用伸缩性定律)在 Amdahl 上叠加一致性开销该项随并发数二次增长,故加核到一定程度反而变慢

真正限制伸缩性的不是 CPU,而是共享热点——共享导致的一致性流量正是 USL 里那个二次恶化项。所以现代系统的演化方向是:共享最小化 + 局部性最大化。局部性之所以关键:共享的真实粒度是物理缓存行而非逻辑变量——逻辑上不相干的数据若落在同一缓存行,仍构成共享热点(即伪共享)。

代价的三个来源

并发损耗并非单一,分属不同抽象层:

来源成本抽象层
调度上下文切换OS
一致性缓存失效、内存屏障(Fence)硬件 / 内存模型
串行化锁竞争算法 / Amdahl 串行占比

后两类直接回扣内存模型章——缓存与屏障的代价,在伸缩性层面显形为一致性开销。

锁竞争的量化

临界区的串行化程度近似排队论的利用率

ρ ≈ 到达率 × 持锁时间

ρ 越接近 1,等待越剧烈。优化顺序据此推出:减少共享(降到达率)> 缩短临界区(降服务时间)> 优化锁(降单次开销)

并发目标不是线程更多,而是等待更少。

并发模型的统一分类

并发模型看似繁多,实则由两条正交的设计轴决定:怎么通信安全从哪来。两轴定义一个坐标平面,所有模型都是其中的坐标点。

第一维:通信机制

模型协作方式特点代表
共享内存我修改,你直接看到灵活、高性能、容易失控Java Threads、pthreads
消息传递我修改,告诉你,你再处理隔离性强、易扩展、延迟更高Actor、CSP

第二维:安全保证来源

约束时机特点代表
动态约束运行时发现错误灵活、容易遗漏锁、CAS、STM
静态约束编译期证明安全强约束、更安全Rust Ownership、线性类型系统

两维坐标:模型的统一定位

把典型模型放进 (通信 × 安全) 平面,分类才真正"统一":

动态约束(运行时)静态约束(编译期)
共享内存锁、STMOwnership
消息传递Actor、CSP会话类型(Session Types,前沿)

坐标暴露两条规律:

典型并发模型:各自的取舍

定位之后看细节——每个模型都不消灭复杂度,只转移复杂度:

模型坐标本质消除的问题转移出的复杂度
锁模型共享 / 动态控制共享数据竞争死锁
Actor消息 / 动态封装状态共享问题消息一致性
CSP消息 / 动态Channel 协作显式锁通道阻塞
STM共享 / 动态内存事务锁管理回滚成本
Ownership共享 / 静态类型隔离数据竞争生命周期复杂度

"转移出的复杂度"列揭示一条守恒律:选型不是挑"最好的模型",而是选愿意承受哪一类复杂度

现代并发演化

并发的根本困难是共享状态 + 时间不确定性。看似纷繁的现代技术,实则沿四条同向矢量演进——每条都在削弱这两个困难之一:

矢量演进攻击的困难实例消除的痛点
结构化隐式 → 显式时间不确定性Async/Await(控制流)、Structured Concurrency(生命周期)回调地狱、孤儿任务、资源泄漏
去共享共享 → 隔离/不可变共享状态Actor、不可变数据(见核心矛盾)数据竞争
静态化运行时 → 编译期时间不确定性Rust Ownership、会话类型(见两维坐标的右移)错误发现太晚
无阻塞挂起等待 → 流式推进时间不确定性Lock-Free、Reactive 背压(见性能与伸缩性章)线程挂起传播、伸缩性瓶颈

四条矢量殊途同归:要么消除共享,要么把时间交错变得显式、可证、不阻塞——与开篇"共享 vs 隔离"的核心矛盾首尾呼应。

并发设计模式

设计模式是前述原理的落地实例——"问题 → 解法 → 代价"的复用模板。三类模式分别对应三种诉求:安全(去共享,消除型控制)、协调(共享受限下的协作协议)、伸缩(突破单点容量)。每个模式都在转移而非消灭复杂度。

安全性模式

去共享 / 去可变,从根源消除竞争——"消除型"控制的实例。

模式解决的问题机制转移出的代价
Immutable共享可变状态引发竞争状态不可改,构造后永不迁移每次"修改"需新建对象(内存 / GC 压力)
Thread Confinement对象被多线程共享对象限定在单一执行单元内,不发布跨线程传递需显式拷贝或移交
Copy-On-Write读多写少时读写互斥开销大写时复制副本,读旧本无锁写放大、内存翻倍、读可能拿到旧快照
ThreadLocal全局状态被并发访问每个执行单元持有私有副本内存随线程数增长,线程池下需清理(泄漏 / 串数据)

协调性模式

共享不可避免时,用结构化协议管理访问顺序——"协调型"控制的实例。

模式解决的问题机制转移出的代价
Producer-Consumer生产 / 消费速率不匹配队列缓冲,解耦两端节奏队列容量与背压(满 / 空时阻塞)
Guarded Suspension前置条件未满足时如何等待条件不成立则挂起,成立再唤醒虚假唤醒,须 while 循环复检
Reader-Writer读多写少时统一锁粒度过粗读共享、写独占写饥饿(读不断则写等不到)
Two-Phase Termination如何安全停止运行中的任务先发停止信号,再等清理完成需可中断点,清理顺序敏感

可伸缩模式

突破单点容量与速率瓶颈——多在分布式层落地。

模式解决的问题机制转移出的代价
Worker Pool无界任务并发导致资源耗尽固定 worker 数 + 任务队列,复用执行单元队列积压、拒绝策略,任务间不可相互依赖
MQ瞬时流量超过处理能力,上下游强耦合异步消息缓冲,削峰填谷 + 解耦延迟增加,消息重复 / 顺序 / 一致性
Sharding单点状态成为热点与容量瓶颈按 key 拆分到多分片,并行处理跨分片操作复杂,再平衡成本
Consistent Hash分片增减时数据大规模迁移哈希环,节点变动只影响相邻区间负载不均(需虚拟节点),实现复杂

并发测试哲学

根因:时间是不可控的隐藏输入

普通测试是"固定输入 → 固定输出";并发的输入除了数据,还有交错顺序,而它由运行时决定、每次不同(即前文的时间不确定性)。于是同样输入这次通过、下次崩溃——并发 Bug 时序相关、概率触发、难以复现。

范式转变:从"验证输出"到"探索交错"

既然时间是输入,测一次只覆盖了一种交错,等于只测了一个输入点。并发测试的核心因此转变:

不是验证"这次跑对了",而是控制、扰动或穷尽交错空间,逼出坏交错或证明其不存在。

所有并发测试手段都是这一目标的不同强度实现。

对策:两条路线

路线思路手段边界
提高触发概率多跑、乱跑、扰动调度,让罕见坏交错显形压力 / 随机重复、延迟注入(jcstress、CHESS)、竞争检测器(TSan、-race只能"发现存在",不能"证明不存在"
穷尽或证明把交错空间系统化覆盖或形式化推理确定性重放 / 模拟、模型检测(TLA+、SPIN)、不变式 / 属性断言状态爆炸,规模受限

竞争检测器是性价比最高的一档:它查的是数据竞争这一根因,不依赖 Bug 是否恰好显形。

测试重点

测什么,对应前文"错误统一抽象"的四类失效——关键是每一维如何在交错中检验:

维度核心怎么测
正确性不变式始终成立高并发下持续断言不变式(如余额 >= 0)
安全性状态不被破坏竞争检测器 + 随机交错压力
活跃性系统持续推进死锁检测、超时探针、活锁观测(CPU 高但无进展)
性能延迟与吞吐递增并发压测,观察 ρ 趋近 1 时的拐点(见性能章)

并发误区与反模式

认知误区

观念层面的错误判断——多源于把直觉套用到并发:

误区真相
并发 = 并行(多线程必加速)并发管等待、并行才加速;CPU 密集的加速受 Amdahl 限制(见性能章)
锁 = 安全锁只护它圈住的范围,跨多步的业务不变式仍可能被破坏(见错误抽象)
volatile = 轻量锁volatile 只保证可见性 / 有序性,不保证原子性(见内存模型)
响应式无需同步消除了显式回调,但 operator 间状态、订阅生命周期、线程切换边界仍需协调
协程没有并发问题单线程协程在 await 让出点仍会被交错 / 重入;多线程协程更叠加数据竞争
死锁是偶然死锁是结构缺陷——满足 Coffman 四条件就必然发生(见活跃性)

真正的反模式

做法层面的坏实践——看似可行却有害:

反模式危害正确做法
忙等待 / 自旋空转烧 CPU 却无进展用条件变量 / 阻塞队列挂起等待
锁内做 I/O 或调用外部代码持锁时间不可控,利用率 ρ 飙升(见性能章)I/O 移出临界区,锁内只护状态
嵌套锁、加锁顺序不一致循环等待 → 死锁统一全局锁序 / 一次性获取 / tryLock 超时
全局粗粒度锁一切串行化,伸缩性归零锁分段 / 缩小临界区 / 改无锁结构
用 sleep 凑同步既慢又不可靠,竞态依旧存在显式同步原语(latch、条件变量)
双重检查锁定漏 volatile可能读到半初始化对象(见安全发布)字段加 volatile,或用静态 holder / 枚举单例

并发设计哲学总结

全文可收束为一条因果链:一个矛盾派生两类问题,两类问题对应两条对策,对策落地为各章内容。

一个矛盾      共享状态 × 时间不确定性                ↓ 派生两类问题      安全性(状态协调失效) + 活跃性(进度协调失效)                ↓ 对策两条主线      消除共享          驯服时间              (隔离 / 不可变)   (结构化 / 静态化 / 无阻塞)                ↓ 落地各章展开      控制哲学 → 模型坐标 → 设计模式 → 测试范式

两条主线对应全文的两组章节:

主线攻击的困难展开于
消除共享共享状态控制哲学(消除型)、安全发布、安全性模式
驯服时间时间不确定性内存模型(happens-before)、现代演化(结构化/静态化/无阻塞)、测试范式

由此得到全文唯一的最高原则——按"消除 > 协调"排序:

不共享 > 不可变 > 消息通信 > 乐观 > 互斥

并发的真正难点不是线程、锁或 API,而是:

如何在不确定时间中维持状态一致性。 并发编程,本质上是"状态协调工程学"。

关联内容