Java内存模型与Volatile,Happen-Before原则等

将相本无种,男儿当自强。这篇文章主要讲述Java内存模型与Volatile,Happen-Before原则等相关的知识,希望能为你提供帮助。
  java的内存模型
Java内存模型(JMM)是一个抽象的模型。决定了线程主要定义了线程和内存间的抽象关系:主内存存放的是线程共享变量,每个线程有自己的工作内存,存放变量的副本,只能对副本进行读写,副本的变量再刷新到主内存中。具体体现为多核CPU,每核有一个高速缓存,每个核的线程对高速缓存读写,并且有共同的主存。
主内存与工作线程交互的操作有以下八种:
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
unlock(解锁):作用于主内存的变量,释放锁定状态的变量
read(读取):作用于主内存的变量,把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign(赋值):作用于工作内存的变量,把一个从执行引擎收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作
store(存储):作用于工作内存的变量,把工作内存的一个变量值传送到主内存,以便随后的write操作使用
write(写入):作用于主内存的变量,把store操作从工作内存得到的变量的值放入主内存变量中
上述八种操作均是原子操作。

Java内存模型与Volatile,Happen-Before原则等

文章图片

如上图,若A和B两个线程同时去主存读写变量C,就会存在线程安全问题(可见性和有序性)。
 
Happens-Before原则
在JMM中,两个线程操作之间存在happens-before关系,则前一个操作的结果对后续操作可见。即使编译器进行指令重排序的优化,如果结果和重排序前一致,也是允许的。
因为允许指令重排序,这也说明  happens-before并不代表操作的时间顺序。
有如下8条规则:
1.程序次序规则:单线程内,按照程序顺序,书写在前面的操作  happens-before  于书写在后面的操作;
2.volatile变量规则:对一个变量的写操作  happens-before  于后面对这个变量的读操作(保证了volatile变量的可见型);
3.传递规则:如果 A  happens-before  B,而B  happens-before  C,则A  happens-before  C;
4.锁定规则:一个unLock操作happens-before  后面对同一个锁的lock操作;
5.线程启动规则:A线程调用B线程的B.start()方法和调用之前的操作  happens-before  于B线程中的任意操作;
6.线程终结规则:线程中所有的操作都  happens-before  于线程的终止检测,如B线程的操作都happens-before 于B.join();
7.线程中断规则:对线程interrupt()方法的调用  happens-before  于被中断线程的代码检测到中断事件的发生;
8.对象终结规则:一个对象的初始化完成  happens-before  他的finalize()方法的开始
 
Volatile关键词
java1.5之后,通过happen-before原则增强了volatile关键词。volatile关键词是轻量的实现线程安全的方法,保证了volatile变量的有序性和可见性。
1.volatile保证了变量在线程间的可见性(  MESI 协议):
当写 volatile 变量时,JMM 会立即该线程对应的本地内存中的共享变量值刷新到主内存。
当读 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile 保证内存可见性,其实是用到了 CPU 保证缓存一致性的 MESI 协议。当某线程对 volatile 变量的修改会立即回写到主存中,并且导致其他线程的缓存行失效,强制其他线程再使用变量时,需要从主存中读取。
 
2.volatile保证了有序性(内存屏障):
volatile变量在读写操作前后添加内存屏障,禁止了指令的重排序,保证了有序性。
内存屏障有LoadLoad屏障,StoreStore屏障,LoadStore屏障,StoreLoad屏障。比如loadload屏障,加在Load1; Load2两个原子操作之间 ,保证在Load2及后续的读操作读取之前,Load1已经读取。其他同理。
在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad
在每个volatile读取之前,插入LoadLoad,之后插入LoadStore。
禁止重排序规则如下图。
Java内存模型与Volatile,Happen-Before原则等

文章图片

 
3.volatile无法保证原子性,只能保证自身读写为原子操作。
对volatile变量的读写 相当与给读写方法加了synchronized关键词,但volatile更轻量级。
 
例子
【Java内存模型与Volatile,Happen-Before原则等】最经典的例子就是单例模式的双重检查锁模式
 
 
Java内存模型与Volatile,Happen-Before原则等

文章图片

这里instance变量需要设置为volatile类型,这样可以保证它的new操作不会被重排序。
因为这段代码可能在重排序后分为下列三步执行:
  1. 为 instance分配内存空间
  2. 将 instance指向分配的内存地址
  3. 初始化  instance 
这样在执行2时,别的线程就会判断instance不为null,直接返回还未实例化完全的instance。
 
并且可以知道volatile修饰的instance变量的new操作并不是原子操作,否则也就不需要synchronized加锁来保证原子性了。
总结:
JMM内存模型定义了线程和内存间的抽象关系,实际的例子就是cpu核线程,高速缓存和主存间的关系;
happens-before原则的规定保证了操作间的可见性。
volatile变量保证了有序性和可见性。主要是内存屏障和MESI 协议实现的。volatile变量的读写操作为原子操作,本身其他操作(如自增)是非原子操作。

    推荐阅读