为什么要有 Java内存模型?
并发编程的3个源头问题分别是:
- 可见性,由缓存导致的可见性问题
- 有序性,由编译优化导致的有序性问题
- 原子性,由线程切换导致的原子性问题
Java内存模型就是为了解决可见性和有序性问题。
什么是 Java 内存模型?
缓存会导致可见性问题,编译优化会导致有序性问题。如果要避免这两个问题,最简单的方法不就是禁用缓存和编译优化。这样就丢掉了优化程序性能的有利武器,显然是不可取的。
合理的方案应该是按需禁用缓存以及编译优化。什么叫按需禁用缓存以及编译优化呢?指的就是程序员在写代码的过程中,对有可能出现并发问题的代码禁用缓存和编译优化。
**Java内存模型就是禁用缓存和编译优化的一种规范,它规范了 JVM 如何提供按需禁用缓存和编译优化的方法。**现在提到的 Java 内存模型,一般指的是 JDK 5 开始使用内存模型,遵循的是 JSR-133 描述的规范。
JSR133
Java内存模型是一个雄心勃勃的计划,它是编程语言规范第一次尝试合并一个能够在各种处理器架构中为并发提供一致语义的内存模型。不过,定义一个既一致又直观的内存模型远比想象要更难。JSR133 为Java语言定义了一个新的内存模型,它修复了早期内存模型中的缺陷。为了实现 JSR133,final和volatile的语义需要重新定义。
完整的语义见:http://www.cs.umd.edu/users/pugh/java/memoryModel,但是正式的语义不是小心翼翼的,它是令人惊讶和清醒的,目的是让人意识到一些看似简单的概念(如同步)其实有多复杂。幸运的是,你不需要懂得这些正式语义的细节——JSR133的目的是创建一组正式语义,这些正式语义提供了volatile、synchronzied和final如何工作的直观框架。
Java内存模型主要分成两部分,一部分面向并发编程的开发人员,一部分面向JVM开发人员,我们需要关注的是前者。前者主要包括 volatile
、synchronized
和 final
三个关键字,以及六项 Happens-Before
规则。
Happens-Before 规则
Java内存模型是按需禁用缓存和编译优化的规则。Happens-Before 表示前面一个操作对后面一个操作是可见的,它约束了编译器的优化行为,编译器可以优化,但是需要遵循 Happens-Before 规则。
有这样 6 条和可见性相关的规范:
- 程序的顺序性规则:按照程序顺序,前面的操作 Happens-before 于后面的操作。
- volatile 变量规则:对 volatile 变量的写操作,Happens-before 于对该 volatile 变量的读操作。
- 传递性:若 A Happens-Before B,B Happens-Before C,则 A Happens-Before C。
- 管程中的锁规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
- 线程 start() 规则:主线程 A 启动子线程 B,则子线程 B 能看到主线程 A 在启动子线程 B 之前的操作。
- 线程 join() 规则:主线程 A 等待子线程 B 完成后,主线程能看到子线程 B 操作
Happens-Before规则对 volatile 语义的增强
前面提到过,现在所说的 Java 内存模型一般指的是 Java1.5 之后的内存模型,它遵循 JSR-133 描述的规范。采用新的规范的原因是旧的 Java 内存模型存在问题,比如:对 final 修饰的变量进行过度的编译优化,以及 volatile 的语义问题。
// 以下代码来源于【参考1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
上面这段代码,变量 v 是一个 volatile 类型的变量,则会禁用缓存,所有线程对 v 的操作都是直接从内存中读取。此时,线程 A 执行 writer() 方法,则 v = true 会被写入内存;同时线程 B 执行 reader() 方法,显然会通过 if 判断,那么此时 x 的值会是多少?
在 Java1.5 之前,x 的值可能是 0,也有可能是 42。因为 x 有可能被 CPU 缓存起来,导致可见性问题。这显然不是我们期望的情况,所以在新的 Java 内存模型中对 volatile 的语音进行了增强,就是依靠 Happens-Before 中的 volatile 变量规则和传递性规则。
如上图所示,线程 A 执行 writer() 方法,线程 B 执行 reader() 方法:
- 根据顺序性规则,x=42 happens-before 于写变量 v;读变量 v happens-before 于读变量 x
- 根据volatile变量规则,对 volatile 变量的写操作 happens-before 于对该变量的读操作
- 根据传递性规则,x=42 happens-before 于读变量 x
此时,通过 Happens-Before 规则对 volatile 变量的语义增强,线程 B 读到的 x 就一定是 42,而不会存在是 0 的情况,符合我们对程序的期待。
相关文章
JSR 133 (Java Memory Model) FAQ