volatile关键字与Java内存模型(JMM)

时间:2021-03-08 20:50:11

Java内存模型(JMM)

JMM用来屏蔽不同硬件和操作系统的内存访问差异,期望Java程序在各种平台上都能实现一致的内存访问效果;


volatile关键字与Java内存模型(JMM)
 
JMM规定里多线程之间的共享变量存储在主存中, 每个线程单独拥有一个本地内存( 逻辑概念 ),本地内存存储线程操作的共享变量副本;
  • JMM中的变量指的是线程共享变量(实例变量,static字段和数组元素),不包括线程私有变量(局部变量和方法参数);
  • JMM规定线程对变量的写操作都在自己的本地内存对副本进行,不能直接写主存中的对应变量;
  • 多线程间变量传递通过主存完成(Java线程通信通过共享内存),线程修改变量后通过本地内存写回主存,从主存读取变量,彼此不允许直接通信(本地内存私有原因);
综上,JMM通过控制主存和每个线程的本地内存的数据交互,保证 一致的内存 可见性;

JMM特征

原子性:原子性是指多线程一起执行时,一个线程操作开始后不会被其他线程干扰,操作不可被中断;
  • synchronizd临界区执行具有原子性;
  • volatile仅仅保证对单个volatile变量的操作具有原子性;
可见性:一个线程修改共享变量时,其他线程能够立即知道这个修改;
  • 单线程:不存在内存可见性问题;
  • 多线程:Java通过volatile, synchronized, final关键字实现可见性;
    • volatilevalatile变量保证变量新值立即被同步回主存,每次读取valtile变量都立即从主存刷新;
    • synchronized:对变量进行解锁前,将对应变量同步回内存;
    • final:final字段一旦初始化完毕,并且this引用没有发生逃逸,其他线程立即看到final字段值;
有序性:线程内操作有序进行,线程间操作有序进行;
  • Java通过volatilesynchronized保证线程间操作的有序性
    • volatile通过禁止重排序实现有序性;
    • synchronized通过声明临界区,保证线程互斥访问,实现有序性;

synchronized与volatile辨析

  • volatile是线程同步的轻量级实现,只用于修饰变量,synchronized用于修饰方法和语句块;
  • 多线程访问volatile不会发生阻塞,但是synchronized会发生阻塞;
  • volatile保证数据的可见性,不保证原子性;synchronized保证数据的可见性和原子性;
  • volatile强调共享变量在多线程之间的可见性,synchronized强调多线程访问资源的同步性;


重排序与happen-before规则

volatile关键字与Java内存模型(JMM)

影响多线程有序性:重排序

  • 编译器重排序:编译器保证不改变单线程执行结果的前提下,可以调整多线程语句执行顺序;
  • 处理器重排序如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
JMM通过happen-before规则,底层禁止特定类型的编译器重排序和处理器重排序,保证内存的可见性和有序性

happen-before规则

JMM为所有操作定义了一个偏序关系,称之为happen-before。 在JMM中,如果A操作对B操作存在happen-before关系;A操作的执行结果全部对B操作可见; 因此不同操作的时间顺序和先行发生规则没有关系,happen-before强调前者修改结果全部对后者可见如果A,B操作不存在happen-before关系,JVM会对它们进行任意重排序;
JMM默认 happen-before 规则:
  • 程序顺序规则:一个线程的每个操作,先于该线程其他后续操作执行;
    • 线程的start()方法先于线程内其他方法执行,线程所有操作先于线程的终结操作;
  • 锁规则:对一个monitor的解锁必然先于对该monitor的加锁;
  • volatile变量规则:对volatile的写操作先于读操作;
  • 传递性:A先于B,B先于C,必然A先于C;

内存屏障指令(memory barriers)

内存屏障指令是一组处理指令,用来限制内存操作的顺序;

volatile关键字

★ volatile实现原理 

volatile变量写,汇编指令会多出 Lock前缀,Lock前缀在多核处理器下的作用:
  • 将当前处理器缓存行的数据写回主存;
  • 令其他CPU里缓存该内存地址的数据无效;
针对编译器重排序: JMM针对编译器指定了volatile重排序规则表,规定哪些先后操作不能进行编译器重排序:
volatile关键字与Java内存模型(JMM)
针对处理器重排序:编译器在生成字节码指令时,通过在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序,以 实现volatile内存语义 volatile底层通过内存屏障指令实现
volatile关键字与Java内存模型(JMM)
  • 在每个volatile变量写操作之前插入StoreStore屏障,之后插入StoreLoad屏障;
    • 之前插入StoreStore屏障:禁止volatile写之前的写操作与其重排序,保证之前的所有写操作都写回主存,对volatile写可见
    • 之后插入StoreLoad屏障:禁止volatile写之后的读写操作与其重排序,实现volatile写结果对后续操作可见
  • 在每个volatile变量读操作之后,接连插入LoadLoad屏障,LoadStore屏障;
    • 插入LoadLoad屏障:禁止volatile变量读之后的读操作与其重排序;
    • 插入LoadStore屏障:禁止volatile变量读之后的写操作与其重排序;
    • 通过插入两次内存屏障,实现volatile读结果对后续操作可见
JMM通过上述内存屏障插入策略,保证在任意平台上 volatile 的内存语义一致;

volatile关键字语义

volatile用来修饰共享变量(成员变量,static变量)表明:
  • volatile变量:当写一个volatile变量时,JMM会把所有线程本地内存的对应变量副本刷新回主存;
    • volatile写和解锁内存语义相同;
  • volatile变量:当读一个volatile变量时,JMM会设置该线程的volatile变量副本(本地内存中)无效,线程只能从主存中读取该变量;
    • 保证了volatile变量读,总能看见对该volatile变量最后的修改;
    • volatile变量读和加锁内存语义相同;
通过上述机制, volatile 保证共享变量一旦被修改,新值对所有线程可见;