Java 内存模型
看Java 如何解决可见性和有序性问题
并发编程bug之源:
- 可见性
- 原子性
- 有序性
先看如何解决可见性和有序性问题–Java内存模型
什么是Java内存模型?
- 缓存导致可见性
- 编译优化导致有序性
那么解决可见性,有序性最直接的办法就是禁用缓存和禁用编译优化,但这样问题虽然解决了,但程序就有性能问题了。
合理的方案是按需禁用缓存以及编译优化。
那么,怎么做到“按需禁用”呢?所谓“按需禁用”,其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。
Java内存模型可以从不同的视角来解读,站在程序员的视角,本质上可以理解为,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。
具体来说包括以下方法:
- volatile
- synchronized
- final
- 六项 Happens-Before 规则
使用volatile的困惑
volatile关键字不是Java语言的特产,C语言中也有,volatile表示不稳定的,变化的。它最原始的意义就是禁用CPU缓存。
如:我们声明一个 volatile 变量:
volatile int x = 0
它表达的是:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或写入。
Java内存模型在1.5版本对 volatile 语义进行了增加。怎么增加的呢?答案就是一项 Happens-Before 规则。
Happens-Before 规则
Happens-Before 并不是说前面一个操作发生在后续操作的前面,它真正要表达的是:前面一个操作的结果对后续操作是可见的。
Happends-Before 规则就是要保证线程之间的这种“心灵感应”,一个人心之所想,另一个人都看得到。
正式的说法是: Happends-Before 约束了编译器的优化行为,虽然允许编译器优化,但是要求编译器优化后一定要遵守 Happends-Before 规则。
1. 程序的顺序性规则
这条规则指一个线程中,按照程序顺序:程序前面对某个变量的修改一定是对后续操作可见的。
这是比较符合单线程里面的思维的。
2. volatile 变量规则
这条规则是指对一个 volatile 变量的写操作,Happens-Before 于后续对这个 volatile 变量的读操作。
3. 传递性
这条规则指:如果 A Happends-Before B,且 B Happends-Before C,那么 A Happends-Before C。
4. 管程中锁的规则
这条规则指:对一个锁的解锁 Happends-Before 于后续对这个锁的加锁。
要理解这个规则,首先要了解“管程指的是什么”。
管程是一种能用的同步原语,在Java 中指的就是 synchronized,synchronized 是Java 里对管程的实现。
管程中的锁在Java 里是隐式实现的,例如下面的代码,在进入同步块之前,会自动加锁,而在代码块执行完,会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。
synchronized(this){ // 此处自动加锁
// x是共享变量,初始值 = 10
if(this.x < 12){
this.x = 12;
}
} // 此处自动解锁
5.线程 start() 规则
这条是关于线程启动的。它是指主线程A启动子线程B后,子线程B能看到主线程在启动子线程B之前的操作。
Thread B = new Thread(()->{
// 主线程调用 B.start() 之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();
6. 线程 join() 规则
这条是关于线程等待的。它是指主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
Thread B = new Thread(()->{
// 此处对共享变量 var 修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join() // 主线程等待子线程B执行完成
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66
被我们忽视的final
前面讲 volatile 为的是禁用缓存以及编译优化,我们再从另一方面来看,有没有办法告诉编译器优化的更好一点呢?就是使用final 关键字了。
final修饰变量时,初衷是告诉编译器:这个变量是永远不变的,可以做死的优化。