1. jmm屏蔽各类硬件和操做系统的内存访问差别,以实现让java程序在各类平台下都能达到一致性的内存访问效果。jmm解决的是一个线程修改一个变量,什么时候对其余线程可见的问题。涉及的关键字有volatile、final、锁,经过这些能够实现java的内存可见性。java
2. jmm定义的内存模型如图:程序员
其实jvm并无本地内存、主内存的说法,只不过为了让人们更加理解jmm,屏蔽内部实现的复杂性而抽象出来的模型。安全
3. happens-beforeapp
在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关系。两个操做既能够是一个线程中的,也能够是两个不一样的线程中的。Happends-before的规则以下:jvm
A 程序顺序规则:一个线程中的每一个操做,happens- before 于该线程中的任意后续操做。函数
B 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。学习
C volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。spa
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