Java Memory Model

对JMM以及happens-before的一些理解

我们写的程序所要操作的变量都保存在内存中,但同时CPU又有自己的高速缓存寄存器等保存它要处理的数据。举例来说,在多核CPU环境下,每个CPU都有自己的高速缓存(L1 Cache,L2 Cache),那么在CPU Core1上执行的Thread A对共享变量a赋值1的操作,该值被写入了CPU Core1的L1 Cache里面,那么在CPU Core2上同时执行的Thread B是否能访问到变量a的最新值?这就需要缓存一致性协议(Cache Conherence)来保证。而不同硬件平台上的实现是不同的。

JMM(Java Memory Model)是一个抽象的语言级别的内存模型,用来屏蔽底层硬件或者操作系统同步协议的差异。

在JMM中,没有寄存器和CPU高速缓存的概念,只有工作内存(local memory,包含了CPU缓存,寄存器等)和主内存(main memory)。工作内存中保存了主内存中共享变量的副本,Thread操作这些变量的副本,然后再同步回主内存当中。

可见性问题(Visibility)

一个线程A设置了a=1,那么另外一个线程B如何能保证看到变量a的最新赋值? JMM定义了volatilesynchronizedfinal的语义来保证共享变量在不同Thread之间的可见性。

比如一个Thread退出synchronized代码块时,JMM需要其将local memory中的共享变量值flush到主内存中,下一个进入该synchrounized代码块的Thread需要首先将自己工作区中的共享变量副本标记为非法,以便从主内存中得到共享变量的最新副本。

volatile

考虑如下程序:

1
2
3
4
5
6
7
8
9
10
11
GlobalConfig config;
boolean loaded = false;

//Thread A
config = loadConfig();
loaded = true;

//Thread B
if(loaded){
doSomethings();
}

如果考虑到编译器或者指令级的重排序,Thread B在执行时loaded的值可能已经为true,但是config变量可能并未初始化,导致doSomethings中如果依赖于config变量的值将造成错误。这时候就需要将loaded变量声明为volatile的。

在JSR133(JDK5)之前,volatile定义的变量仅能保证不同的线程读写自己的时候的可见性,所以上面的代码依旧是不能保证的。JSR133重新定义了volatile的语义,保证在包含volatile变量的Thread中操作变量的所有动作不会被重排序。

happens-before

我们知道,出于性能的考虑,我们编写的程序指令会被CPU或者编译器以及JVM运行时进行重排序,happens-before是JMM呈献给程序猿的一套规则,阐述了在其规定的情形下一些重排序是禁止的。

其中的一条规则是:

“Program order rule. Each action in a thread happens before every action in that thread that comes later in the program order.”

其中的action指的是读/写变量,获取/释放锁监视器,开始/join一个线程等。

1
2
3
4
5
6
7
8
9
int foo, bar;

//Thread A
foo = 1;//------1
bar = 2;//------2
if (foo == 1) System.out.println(bar);//------3

//Thread B
if (foo == 1) System.out.println(bar);//------4

上面列出的那条规则能够保证在标注3处打印的结果是2,但标注4处的打印结果仍然可能为0, why?
因为对于Thread A来说,1处和2处的指令重排序并不影响3处的结果,so对Thread A来说是无所谓的。

Resources