Java 内存模型与并发编程
一、并发编程模型分类
- 关键问题:线程通信与同步。
- 通信机制:
- 共享内存:线程通过读写共享状态隐式通信(如 Java)。
- 消息传递:线程通过显式消息通信,无共享状态。
- 同步机制:
- 共享内存:显式同步,程序员需指定互斥代码。
- 消息传递:隐式同步,发送先于接收。
Java 特点:采用共享内存模型,通信隐式,易引发可见性问题。
二、Java 内存模型(JMM)抽象
- 存储位置:
- 共享变量(实例域、静态域、数组元素):存储在堆中,线程共享。
- 私有变量(局部变量、方法参数、异常参数):存储在栈中,线程隔离,无可见性问题。
- 抽象模型:
- 主内存:存储共享变量。
- 本地内存:线程私有,保存共享变量副本(抽象概念,涵盖缓存、寄存器等)。
- 通信步骤:
- 线程更新本地内存中的共享变量副本。
- 将更新刷新到主内存。
- 其他线程从主内存读取更新。
- 目的:通过控制主内存与本地内存交互,保证内存可见性。
三、重排序
- 类型:
- 编译器重排序:优化语句顺序,不改变单线程语义。
- 指令级并行重排序:处理器重叠执行指令,无数据依赖可调整顺序。
- 内存系统重排序:缓存和写缓冲区导致操作乱序。
- 过程:源代码 → 编译器重排序 → 指令重排序 → 内存重排序 → 执行序列。
- 问题:多线程下可能破坏可见性。
- JMM 对策:
- 禁止特定编译器重排序。
- 通过内存屏障禁止处理器重排序。
四、处理器重排序与内存屏障
- 写缓冲区影响:
- 写操作暂存缓冲区,仅当前处理器可见。
- 可能导致写-读操作重排序。
- 处理器重排序类型(常见处理器):
- Store-Load:普遍允许。
- 数据依赖:禁止重排序。
- 内存屏障:
- LoadLoad:确保加载顺序。
- StoreStore:确保存储顺序。
- LoadStore:加载先于存储。
- StoreLoad:存储刷新后加载,全能型,开销大。
- 作用:JMM 插入屏障,限制重排序,保证可见性。
五、Happens-Before
- 定义(JSR-133):
- 一个操作结果对后续操作可见,且顺序在前。
- 不要求严格时间顺序。
- 规则:
- 程序顺序:线程内操作顺序。
- 监视器锁:解锁 → 加锁。
- volatile:写 → 读。
- 传递性:A → B,B → C,则 A → C。
- 与 JMM:对应重排序规则,简化程序员理解。
六、数据依赖性与 As-If-Serial
- 数据依赖:
- 写后读、写后写、读后写:重排序改变结果,禁止。
- 仅限单线程或单处理器。
- As-If-Serial:
- 单线程执行结果不变。
- 无数据依赖可重排序。
- 示例:
pi * r * r
,pi
和r
可重排,但计算依赖不可。
七、重排序对多线程影响
- 示例:
java class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; flag = true; } public void reader() { if (flag) int i = a * a; } }
- 无同步:
a = 1
和flag = true
可重排序。- 线程 B 可能读到
flag = true
但a = 0
。 - 控制依赖:
if (flag)
和a * a
重排序,猜测执行破坏语义。
八、顺序一致性
- 定义:
- 线程按程序顺序执行。
- 操作原子且立即对所有线程可见。
- 数据竞争:
- 无同步的读写操作。
- 导致不可预测结果。
- JMM 保证:
- 正确同步程序具有顺序一致性。
九、同步与未同步程序
- 同步程序:
- 使用
synchronized
:java class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { a = 1; flag = true; } public synchronized void reader() { if (flag) int i = a; } }
- 顺序一致性:临界区内重排序不影响结果。
- 未同步程序:
- 最小安全性:读取值为默认值或之前写入值。
- 不保证顺序一致性。
- 64 位变量读写可能非原子。
十、处理器内存模型
- 类型:
- TSO:允许写-读重排序。
- PSO:加允许写-写。
- RMO/PowerPC:更宽松。
- 与 JMM:
- JMM 通过屏障适配不同处理器。
- 比顺序一致性弱,优化性能。
十一、JMM 设计
- 平衡:
- 程序员:强可见性,简单易用。
- 编译器/处理器:弱约束,优化空间。
- 策略:
- 禁止改变结果的重排序。
- 允许不影响结果的重排序。
- 内存可见性:
- 单线程:As-If-Serial。
- 正确同步:顺序一致性。
- 未同步:最小安全性。
十二、JSR-133 改进
- Volatile:禁止与普通变量重排序,写-读如锁。
- Final:初始化安全性,禁止重排序。
总结
- JMM 核心:通过 Happens-Before 和屏障,平衡可见性与性能。
- 并发基础:理解通信、同步与重排序是编写正确多线程程序的关键。