JMM
前言
上一节讲到 Java 并发问题的源头可归结为可见性、原子性、有序性的问题。Java 内存模型可以解决可见性和有序性的问题。
Java 虚拟机规范中定义了 Java 内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果,JMM 规范了 Java 虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
Java 内存模型在 Java1.5 时被重新修订。
CPU 缓存导致可见性,编译优化导致有序性,所以可以按需禁用缓存和编译优化来解决可见性和有序性问题。JMM 即规范了 JVM 如何提供按需禁用缓存和编译优化的方法。这些方法包括:
volatile
synchronized
final
Happens-Before 规则
volatile
C 语言里面就有 volatile 关键字,在 C 里面的原始意义就是禁用 CPU 缓存,对 volatile 变量读写不能使用 CPU 缓存,必须从内存中读写。
如下代码,假设线程 A 执行 writer()
,然后线程 B 执行 reader()
,那么线程 B 看到 x 是多少呢?
若 JDK 低于1.5,则 x 等于0或42,这是因为 x 可能被 CPU 缓存导致可见性问题。
若 JDK 高于1.5,则 x 一定为42,这是因为 JMM 在 Java1.5 时被重新修订,即有了 Happens-Before 原则。
Happens-Before
Happens-Before 很多翻译都不对,它真正的意义是:前一个操作的结果对后续操作是可见的。
程序顺序性规则
一个线程中的每个操作,Happens-Before 于该线程中的任意后续操作。即:单线程中,程序前面对某个变量的修改一定是对后续操作可见的。
volatile 变量规则
对一个volatile
变量的写操作, Happens-Before 于后续对这个 volatile
变量的读操作。
注意,这个不像第一点,没有限定在单线程中。
传递性
如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
现在我们使用前面三条规则来分析上面的代码,x=42 Happens-Before v=true, 线程 A 写 v Happens-Before 线程 B读 v,再根据传递性,所以线程 B 读到的 x 一定是 42
监视器锁规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
线程start()
规则
start()
规则主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
线程join()
规则
join()
规则主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join()
方法返回),主线程能够看到子线程的操作。
final
被final
修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this
传递出去(this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final
字段的值(无须同步)。
案例
下面代码运行后,会一直运行,CPU 一直使用率很高,8 个线程一直处于 while 循环,控制台没有任何输出。原因是 get 方法无法看见 set 方法的结果,不能从 Happens-Before 原则中导出 get 能看到 set 对 result 的修改。
需要解决上面问题,仅需要把 result 增加 volatile 修饰,甚至 set 方法的 synchronized 可以去掉。
Reference
Last updated