在不改变程序执行结果的前提下,尽可能提高并行度。
Java 内存模型
JMM 就是 Java 内存模型,由于 Java 默认支持多线程操作,每个线程中都有自己的缓存,即下图中的本地内存(本地内存并不真实存在)。
JMM 通过控制主内存与每个线程的本地内存之间的交互,实现内存可见性的保证。
指令重排
重排序分为三种类型,分别为编译器优化的重排序、指令级并行的重排序和内存系统的重排序。只有第一种属于编译器重排序,剩下的都为处理器重排序。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令时,插入特定类型内存屏障指令来禁止特定类型的处理器重排序。
数据正确性
并发编程最重要的核心就是数据正确,JMM 一定程度上保证了数据可见性,内存屏障又保证了指令执行顺序的正确性,这一切都是为了最后的数据正确。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中又有一个为写操作,此时这两个操作之间就存在数据依赖性。分为 写后读、写后写和读后写三种情况。
如果对这三种情况进行重排序,执行结果就会出现错误。编译器和处理器在重排序时会遵守数据依赖性。但不同处理器和不同线程之间的数据依赖性不被保护。
as-if-serial 语义:不管如何重排序,(单线程)程序的执行结果不能被改变。编译器、处理器都遵守 as-if-serial 语义。
重排序对多线程的影响
当编译器和处理器对代码进行重排序时,可能会导致执行结果错误。
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 先执行操作 2,线程 B 正常执行的情况下,线程 B 中的 i 的结果就为 0,多线程程序的语义被重排序破坏了。
操作依赖性
上面代码中,操作 3 和操作 4 存在控制依赖关系。当代码存在控制依赖关系时,会影响执行序列执行的并行度。编译器和处理器会采取猜测(Speculation)执行来克服控制相关性对并行度影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到名为重排序缓冲(Reorder Buffer,ROB)的硬件缓存中。当判断条件为真时,再把计算记过写入变量 i 中。
顺序一致性
顺序一致性模型是理想化的参考模型,有以下两点特性:
- 一个线程中的所有操作必须按照程序的顺序来执行。
- 无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序(即所有线程所看到的操作执行顺序一致)。在顺序一致性内存模型中,每个操作必须原子执行且立刻对所有线程可见。
但 JMM 中无法保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。
正确的多线程同步程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。
JMM 对数据安全性的保证
JMM 中在获取锁和释放锁的临界区内,可以进行重排序,但锁之间无法重排序。
对于未同步或未正确同步的多线程程序,JMM 只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值。JMM 保证线程读操作读取到的值不会无中生有。
|