并发编程:从共享状态到系统协调的统一理论
并发的本质
并发系统模型
并发真正要解决的问题是:多个执行主体在不确定时间顺序下,对状态进行协作的问题。
并发系统 = 执行单元(谁在运行) + 状态(谁被修改) + 时间交错(顺序是否确定) + 协调机制(如何避免冲突)并发的核心矛盾
共享(协同)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 / ThreadLocal |
| 乐观 | 假设冲突少 | CAS / MVCC |
| 互斥 | 同时只能一个执行者 | 锁 |
设计优先级:隔离 > 乐观 > 互斥
因为:共享越少,复杂度越低。
悲观与乐观的本质差异
悲观并发
假设:冲突一定发生。
先限制,再执行典型:Mutex、synchronized、数据库锁
代价:阻塞、上下文切换、死锁风险
乐观并发
假设:冲突是小概率事件。
实现方式:
乐观并发├── CAS系:先执行,失败重试(atomicInteger)└── 版本系:读写分离,快照读取(MVCC、Copy-On-Write)代价:重试、ABA 问题、自旋消耗(针对 CAS 系)
不共享:最高级的并发安全
锁的本质:共享之后的补救。隔离:从根源消灭问题。
最好的锁 = 不存在的锁线程封闭:对象仅属于单执行单元(栈变量、协程局部状态、Actor 内部状态),无需同步、无需可见性、无需协调。
不变性:共享不可避免时,共享不可变状态。状态永不迁移,因此不存在竞争、不需要同步、没有时间问题。
不变对象的条件:状态不可修改(消除竞争)、final 字段(禁止重排序)、构造期间不逸出(防止半初始化)。
对象生命周期与安全发布
并发问题很多不是"锁问题",而是生命周期失控问题——对象生命周期 ≠ 执行单元生命周期。
发布:对象从私有域进入共享域(放入缓存、注册监听器、返回给其他线程)。
逸出:对象在未准备好前被其他执行单元访问。最危险情况:构造期间 this 逸出,导致 final 语义失效、对象状态不完整。
安全发布的本质不是"有没有加锁",而是是否建立了 happens-before。
安全发布策略:
| 方式 | 本质 |
|---|---|
| 静态初始化 | 类加载屏障 |
| final 字段 | 初始化有序性 |
| volatile | 可见性 |
| 锁保护发布 | 建立 happen-before 关系 |
状态机:现代并发系统的核心
并发本质上是状态协调,线程只是实现细节。真正核心是:事件 → 状态迁移 → 行为触发。
所有并发系统都可抽象为:当前状态 + 事件 → 下一个状态。
| 系统 | 状态机体现 |
|---|---|
| Actor | 消息驱动状态迁移 |
| Reactor | 事件循环 |
| 工作流 | 状态流转 |
| Saga | 分布式补偿 |
| Stream | 数据流状态转换 |
并发真正难的不是执行,而是状态一致性。状态机提供:可推导性、可验证性、可恢复性。
性能与伸缩性的本质
并发不是免费的。成本:上下文切换(调度)、缓存失效(CPU cache)、同步开销(Fence)、锁竞争(串行化)。并发目标不是线程更多,而是等待更少。
锁竞争强度:请求频率 × 持锁时间。优化核心:减少共享 > 缩短临界区 > 优化锁。
真正限制伸缩性的不是 CPU,而是共享热点。现代系统演化方向:共享最小化 + 局部性最大化。
并发模型的统一分类
第一维:通信机制
共享内存模型
我修改你直接看到特点:
- 灵活
- 高性能
- 容易失控
代表:
- Java Threads
- pthreads
消息传递模型
我修改告诉你你再处理特点:
- 隔离性强
- 易扩展
- 延迟更高
代表:
- Actor
- CSP
第二维:安全保证来源
动态约束
运行时发现错误。
代表:
- 锁
- CAS
- STM
特点:
- 灵活
- 容易遗漏
静态约束
编译期证明安全。
代表:
- Rust Ownership
- 线性类型系统
特点:
- 强约束
- 更安全
典型并发模型
| 模型 | 本质 | 消除的问题 | 转移出的复杂度 |
|---|---|---|---|
| 锁模型 | 控制共享 | 数据竞争 | 死锁 |
| Actor | 封装状态 | 共享问题 | 消息一致性 |
| CSP | Channel 协作 | 显式锁 | 通道阻塞 |
| STM | 内存事务 | 锁管理 | 回滚成本 |
| Ownership | 类型隔离 | 数据竞争 | 生命周期复杂度 |
现代并发演化
Async/Await
本质:
控制流结构化。
解决:
- 回调地狱
- 状态分裂
Structured Concurrency
本质:
生命周期结构化。
目标:
任务树=生命周期树避免:
- 孤儿任务
- 资源泄漏
Reactive
本质:
数据流驱动系统。
核心:
- 背压(Backpressure)
- 流式传播
Lock-Free
本质:
消除阻塞等待。
目标:
- 提高可伸缩性
- 避免线程挂起传播
Rust Ownership
本质:
在编译期消除共享错误。
核心思想:
共享必须显式证明安全并发设计模式
安全性模式
| 模式 | 本质 |
|---|---|
| Immutable | 静态确定性 |
| Thread Confinement | 隔离 |
| Copy-On-Write | 读写分离 |
| ThreadLocal | 执行单元私有化 |
协调性模式
| 模式 | 本质 |
|---|---|
| Producer-Consumer | 解耦 |
| Guarded Suspension | 条件等待 |
| Reader-Writer | 读写优化 |
| Two-Phase Termination | 优雅关闭 |
可伸缩模式
| 模式 | 本质 |
|---|---|
| Worker Pool | 执行复用 |
| MQ | 异步削峰 |
| Sharding | 热点拆分 |
| Consistent Hash | 稳定分布 |
并发测试哲学
为什么并发 Bug 难测
因为并发 Bug:
- 时序相关
- 概率触发
- 不稳定复现
本质:
输入不仅是数据,还有时间。
测试重点
| 维度 | 核心 |
|---|---|
| 正确性 | 不变式 |
| 安全性 | 不破坏状态 |
| 活跃性 | 系统持续推进 |
| 性能 | 延迟与吞吐 |
并发反模式
| 误区 | 真相 |
|---|---|
| 并发 = 加速 | 并发主要解决等待 |
| 锁 = 安全 | 锁不保证业务不变式 |
| volatile = 轻量锁 | 不保证原子性 |
| 响应式无需同步 | 状态共享仍然存在 |
| 协程没有并发问题 | 协程仍有共享状态 |
| 死锁是偶然 | 本质是设计缺陷 |
并发设计哲学总结
并发系统的核心矛盾
共享状态vs时间不确定性并发演化的本质方向
整个并发技术的发展:
本质都在朝:
减少共享增强隔离提升确定性方向演化。
并发设计的最高原则
不共享>不可变>消息通信>乐观协调>互斥锁最终结论
并发的真正难点:
不是线程。
不是锁。
不是 API。
而是:
如何在不确定时间中维持状态一致性。
因此:
并发编程,本质上是"状态协调工程学"。
关联内容
- [/编程语言/并发模型.html](/编程语言/并发模型.html) 不同编程语言的并发模型比较
- [/操作系统/进程与线程.html](/操作系统/进程与线程.html) 操作系统层面的进程与线程概念
- [/操作系统/死锁.html](/操作系统/死锁.html) 死锁是并发编程中的经典问题
- [/软件工程/架构/系统设计/高并发.html](/软件工程/架构/系统设计/高并发.html) 高并发系统设计
- [/计算机网络/IO模型.html](/计算机网络/IO模型.html) IO模型与并发编程密切相关