1. jmm屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致性的内存访问效果。jmm解决的是一个线程修改一个变量,何时对其他线程可见的问题。涉及的关键字有volatile、final、锁,通过这些可以实现java的内存可见性。
2. jmm定义的内存模型如图:
其实jvm并没有本地内存、主内存的说法,只不过为了让人们更加理解jmm,屏蔽内部实现的复杂性而抽象出来的模型。
3. happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。两个操作既可以是一个线程中的,也可以是两个不同的线程中的。Happends-before的规则如下:
A 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
B 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
C volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
D 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
A可以这么理解,单线程中如果需要可见性的要求,即写操作对之后读操作的可见性,那必然出现了数据依赖关系,根据as-if-serial语义,不会出现重排序。
如果遵循这些规则,就能保证变量的可见性了。
对于java程序员来说,happens-before规则简单易懂,它避免java程序员为了理解JMM提供的内存可见性保证而去学习复制的重排序规则以及这些规则的具体实现。
重点注意:对两个线程来说,为了正确的设置happens-before关系,访问相同的volatile变量是很重要的。以下的结论是不正确的:当线程A写volatile字段f的时候,线程A可见的所有东西,在线程B读取volatile的字段g之后,变得对线程B可见了。释放操作和获取操作必须匹配(也就是在同一个volatile字段上面完成)。
4. as-if-serial
不管怎么重排序(编译器和处理器为了提高并行度),程序的执行结果不能被改变,编译器,runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
名称 | 代码示例 | 说明 |
写后读 | a = 1;b = a; | 写一个变量之后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量之后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量之后,再写这个变量。 |
5. volatile的含义
volatile的特性
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这个种符合操作不具有 原子性。
有序性:加入内存屏障,防止重排序。
volatile写的内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile的内存语义实现是通过内存屏障来实现的。
下面是JMM针对编译器制定的volatile重排序规则表:
是否能重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 |
|
| NO |
volatile读 | NO | NO | NO |
volatile写 |
| NO | NO |
从上表我们可以看出:
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile写操作的后面插入一个StoreLoad屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:
屏障类型 | 指令示例 | 说明 |
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
内存屏障有两个作用:
1)确保一些特定操作执行的顺序
2)影响一些数据的可见性
为什么doublecheck需要volitale
public class DoubleCheckedLocking { //1
private static Instance instance; //2
public static Instance getInstance() { //3
if (instance == null) { //4:第一次检查
synchronized (DoubleCheckedLocking.class) { //5:加锁
if (instance == null) //6:第二次检查
instance = new Instance(); //7:问题的根源出在这里
} //8
} //9
return instance; //10
} //11
}
前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
由于步骤2与步骤3没有数据依赖关系 可能发生重排序,synchronized关键字并能保证内部不会发生重排序,所以另外一个线程可能在第四步检查不为空,但实际对象还没有初始化成功,只是分配了内存空间,并install指向了内存空间的地址。
Instance设置成valotile后会禁止instance = new Singleton()内部的重排序。
我认为instance不为volatile的话,还会出现其他的不稳定的因素。在第四步进行检查的时候有可能不是最新的值,因为普通变量不能保证读取到其他线程最后一次写入的值。
6. final
对于final域,编译器和处理器要遵守两个重排序规则:
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域的重排序规则
写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:
1)JMM禁止编译器把final域的写重排序到构造函数之外。
2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。
对于final域只保证在构造函数中初始化的安全性,不保证后续对final引用的对象的修改的安全性。
注意:构造对象的引用不能提前在构造器中溢出,对其他线程可见,因为final域可能还没有初始化
7. 锁
锁释放和获取的内存语义:
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量
AQS分公平锁和非公平锁。公平锁是通过获取锁时读取volatile变量,释放锁时写入volatile变量实现可见性的;非公平锁获取锁跟跟公平锁不一样,通过cas实现获取锁,cas具有volatile相同的语义,释放锁跟公平锁一样。
参考 http://www.infoq.com/cn/author/%E7%A8%8B%E6%99%93%E6%98%8E