什么是Java内存模型
结合我上一篇文章(如果还没阅读的可以先看看https://www.cnblogs.com/bolbo/p/10593810.html)已经总结出并发的原因的其中两个原因就是缓存导致的可见性和编译优化导致的有序性,那么解决这两个问题的最直接方法就是禁用缓存和编译优化
但是这样就违背了设计这两个功能目的了,所以合理的方法就是按需禁用缓存和编译优化。那么具体是怎么个按需法呢?
我们知道并发编程主要出现的问题就是对共享资源的处理,所以我们只要针对共享资源进行按需禁用即可,换句话说只有程序员才知道哪些是共享资源,什么时候需要禁用缓存和编译优化,所以只需要提供给程序员方法即可,那么具体是什么方法呢?
这些方法包括volatile
、synchronized
、final
三个关键字以及Happens-Before规则
下面我们重点讲解这些方法
volatile
关键字
被 volatile
修饰的变量,线程在每次使用变量的时候,都会告诉编译器,不能使用CPU缓存,必须从内存中读取或者写入到内存中
Happens-Before 规则
前面一个操作的结果对后续的操作是可见的,约束了编译器优化行为,允许编译器优化,但是要求编译器优化后一定要遵守这个规则
-
程序的顺序性规则
按照程序顺序,前面的操作Happens-Before于后续的任意操作
下面的代码先执行write
方法,然后执行reader
方法class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里 x 会是多少呢? } } }
这里如果没有规则,那么x的值有可能是0,也有可能是42;基于这个规则x=42的这个操作是HB于v=true的,所以在下面的判断中,x一定是等于42的
-
volatile规则
这个规则是指一个被volatile
修饰的变量的写操作,Happens-Before于后续对这个变量的读操作 -
传递性
如果AHappens-Before
于B,同时BHappens-Before
于C,那么AHappens-Before
与 C -
管程中的锁规则
对一个锁的解锁Happens-Before
于后续对这个锁的加锁
这里解释一下管程,在Java中管程就是synchronized
,synchronized
就是Java里对管程的实现 -
线程start()规则
主线程A启动子线程B,子线程B可以看到主线程A启动的子线程B前的操作Thread B = new Thread(()->{ // 主线程调用 B.start() 之前 // 所有对共享变量的修改,此处皆可见 // 此例中,var==77 }); // 此处对共享变量 var 修改 var = 77; // 主线程启动子线程 B.start();
-
线程join()规则
主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现的),当子线程B完成后(主线程A的join()方法完成),主线程可以看到子线程B的操作Thread B = new Thread(()->{ // 此处对共享变量 var 修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程 B 可见 // 主线程启动子线程 B.start(); B.join() // 子线程所有对共享变量的修改 // 在主线程调用 B.join() 之后皆可见 // 此例中,var==66
final
关键字
这个变量生而不变,很容易给我们引起误解
比如下面这个例子
// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲 this 逸出,
global.obj = this;
}
这个例子中,构造函数把this赋值给了global.obj,这里会因为编译器的重排序,出现this还没有初始化完成就已经复制给global.obj了