Java 并发编程
并发编程的本质问题
并发编程是关于在不确定时序下,通过某种协调机制达成正确行为的问题。
并发的三个永恒问题
大多数并发 Bug,本质上都可以归结为以下三类之一:
可见性(Visibility)一个线程对状态的修改,是否能被其他线程观察到。
原子性(Atomicity)一个操作是否不可被中断、不可被拆分。
有序性(Ordering)程序执行顺序在不同线程观察下是否一致。
可见性由内存屏障(实现 Happens-Before)保证,原子性由锁/CAS 保证,有序性由重排约束(满足 Happens-Before)保证
并发解决方案的层次结构
应用层:并发设计模式 ↓抽象层:Lock、Semaphore、Barrier ↓基础设施层:等待-通知(Object.wait/notify, Condition.await/signal) ↓硬件层:内存屏障、CPU 缓存一致性并发控制的核心思想
并发控制的核心是管理协调的不确定性,共享是协调的一种形式。
不共享是最高级的并发安全
并发问题的根源在于共享,因此:
不共享 = 天然线程安全
线程封闭
- 对象只在单一线程内使用
- 不需要任何同步机制
常见形式:
- 栈封闭(方法内对象)
- 任务私有对象
线程封闭的本质是用"隔离"换"安全",代价是数据交换和内存开销。只有在线程间无共享需求时,线程封闭才是最优解
只读共享:不变性
如果共享不可避免,下一优先级是:
共享但不可变
不变对象的并发安全来自其数学属性,而非同步机制:
- 不变对象 ≈ 数学中的常量
- 操作不变对象 ≈ 应用纯函数
不变性的三个条件:
- 状态创建后不可修改
- 所有字段为 final
- 构造期间 this 不逸出
不变性是并发世界中最强的确定性来源。
受控共享:同步与协作
当对象既需要共享,又必须可变:
并发的核心任务 = 协调对状态的访问顺序
这引出了同步、锁、条件等待等机制。
对象共享与发布模型
对象生命周期视角
并发安全问题,本质是对象生命周期与线程生命周期不一致。
关键问题:
- 对象何时创建?
- 何时对其他线程可见?
- 由谁负责修改?
发布与逸出
- **发布**:对象从私有域进入共享域
- **逸出**:对象在未准备好时被发布
构造期间逸出是最危险的并发错误之一。
安全发布
安全发布不是”是否加锁”,而是建立可见性与有序性保证。
通用安全发布策略:
- 静态初始化
- final 字段语义
- volatile / 原子引用
- 锁保护的发布
没有安全发布,线程安全无从谈起。
并发类的设计模式
实例封闭
将线程不安全对象封装在一个受控的并发边界内。
- 并发策略集中
- 对外暴露安全接口
这是最常见、也是最稳健的并发类设计方式。
线程安全委托
将并发安全责任交给更底层的线程安全组件。
前提:
- 不引入新的复合状态依赖
委托失败,往往源于”多个原子操作组合后不再原子”。
状态依赖操作
并发系统中最复杂的不是互斥,而是:
操作是否依赖于状态是否满足某个条件
这类操作需要:
- 条件检查
- 等待
- 被唤醒后重新校验
并发系统的活跃性困境
死锁(Deadlock)
线程循环等待资源,程序永久停滞。
Coffman 四条件:互斥 | 占有且等待 | 不可剥夺 | 循环等待
预防:固定加锁顺序、一次性申请、超时放弃(tryLock)、缩小临界区。
活锁(Livelock)
线程持续行动但无进展,消耗 CPU 但空转。
| 区别 | 死锁 | 活锁 |
|---|---|---|
| CPU 消耗 | 无 | 有 |
| 自愈可能 | 不能 | 可能 |
预防:引入随机延迟、限制重试次数、引入协调者。
饥饿(Starvation)
线程长期无法获得资源,但最终能获得(与死锁"永远不能"不同)。
预防:公平锁(ReentrantLock(true))、避免优先级反转、动态优先级提升。
预防优于检测,设计优于补救。
核心原则:不共享 > 高层工具 > 最小化锁粒度 > 避免嵌套锁循环依赖。
等待、通知与协作机制
等待-通知的本质模型
等待不是”睡眠”,而是:
在条件未满足时,不用浪费 CPU 空转,主动让出执行权,并等待条件变化的通知
核心原则:
- 等待必须释放锁
- 被唤醒 ≠ 条件满足
- 条件检查必须在循环中
条件队列与显式条件
条件队列解决的是:
- 多条件等待
- 精准唤醒
- 可中断 / 可超时
它体现的是状态机式并发设计思想。
取消、关闭与线程生命周期管理
取消不是强制终止
并发系统中:
取消是一种协作协议,而非控制命令
线程必须自行决定:
- 是否响应取消
- 如何清理资源
中断的语义
中断不是异常,而是:
一种跨线程的协作信号
设计原则:
- 不知道中断策略,不要中断
- 阻塞方法要么响应中断,要么明确不可中断
基于任务的取消
Future、Executor 的意义在于:
- 将线程管理权从业务逻辑中剥离
- 提供统一的生命周期控制
性能与伸缩性的并发视角
并发的成本模型
并发不是免费的:
- 上下文切换
- 同步开销
- 阻塞等待
并发的目标不是”线程更多”,而是”等待更少”。并发的开销可能抵消并发带来的收益。
锁竞争的本质
竞争强度取决于:
- 请求频率
- 持锁时间
优化方向:
- 减少共享
- 缩短临界区
- 降低热点
当竞争强度较大时,增加线程数并不能带来性能提升。
JVM 层面的并发优化
- 偏向锁
- 轻量级锁
- 自旋与自适应自旋
- 锁消除与锁粗化
这些优化的前提是:
并发设计本身是合理的
并发程序的测试哲学
并发测试的困难
并发 Bug 是:
- 时序相关的
- 不可复现的
- 概率性的
测试关注点
- 正确性(不变性、后验条件)
- 安全性(无越界、无破坏)
- 活跃性(无死锁、无饥饿)
- 性能特性(吞吐、延迟)
并发编程反模式与误区
并发错误的根源往往不是语法或API的误用,而是认知框架的偏差。
核心原则
最小共享原则:不共享最安全。共享范围越小、频率越低,出现并发问题的概率越小。
不可变优先原则:状态不可变则无需同步。优先设计不可变对象,而非为可变对象添加锁。
角色清晰原则:明确谁拥有锁、谁等待锁、谁释放锁。死锁几乎总是角色定义模糊的系统性缺陷,而非运气不好。
常见认知陷阱
| 误区 | 真相 |
|---|---|
| 并发=加速 | 并发解决资源利用率和响应延迟,非无条件加速 |
| 加锁=安全 | 锁只提供互斥与可见性,不保证业务逻辑原子性 |
| volatile=轻量锁 | volatile保证可见性和有序性,不保证原子性 |
| 响应式无需同步 | 响应式解决调用栈阻塞,状态共享仍需同步 |
| 死锁是运气不好 | 死锁是锁设计缺陷的表征,破坏四要素之一可预防 |
关联内容(自动生成)
- [/编程语言/JAVA/JAVA并发编程/基础概念.html](/编程语言/JAVA/JAVA并发编程/基础概念.html) 深入了解Java并发编程的基础概念,包括线程生命周期、并发问题等核心知识点
- [/编程语言/JAVA/JAVA并发编程/线程.html](/编程语言/JAVA/JAVA并发编程/线程.html) 详细探讨Java中线程的创建、管理和控制机制,与本文档中的线程生命周期管理内容密切相关
- [/编程语言/JAVA/JAVA并发编程/线程池.html](/编程语言/JAVA/JAVA并发编程/线程池.html) 线程池是实现高效并发的关键组件,提供了线程生命周期管理的高级抽象
- [/编程语言/JAVA/JAVA并发编程/并发工具类.html](/编程语言/JAVA/JAVA并发编程/并发工具类.html) Java并发包提供了丰富的同步工具类,是实现本文档所述并发控制思想的具体手段
- [/编程语言/JAVA/JAVA并发编程/并发集合.html](/编程语言/JAVA/JAVA并发编程/并发集合.html) 并发集合是线程安全的数据结构,体现了本文档中提到的实例封闭和线程安全委托设计模式
- [/编程语言/JAVA/JVM/JAVA内存模型.html](/编程语言/JAVA/JVM/JAVA内存模型.html) Java内存模型定义了多线程环境下的内存访问规则,是理解可见性、原子性和有序性问题的基础
- [/操作系统/进程与线程.html](/操作系统/进程与线程.html) 操作系统层面的进程与线程概念是理解Java并发编程模型的基础,有助于深入理解线程调度和同步机制
- [/软件工程/架构/系统设计/高并发.html](/软件工程/架构/系统设计/高并发.html) 高并发系统设计涉及大量并发编程原理的应用,是本文档理论知识的实际应用场景
- [/编程语言/并发模型.html](/编程语言/并发模型.html) 不同编程语言的并发模型比较,有助于理解Java并发编程模型的特点和优势
- [/计算机网络/IO模型.html](/计算机网络/IO模型.html) IO模型与并发编程密切相关,特别是在处理高并发网络请求时,需要结合IO模型选择合适的并发策略