volatile基本原理

742 阅读5分钟

JVM之volatile:JVM提供的轻量级的同步机制

一、volatile作用:volatile通过内存屏障和缓存一致性协议实现了变量在多核心之间的一致性

  1. 保证线程的可见性:变量加了volatile关键字之后,一定会保证assign之后,会用storewrite操作,将这个值刷回主存中;同时基于MESI一致性协议,其他线程会感知到这个值已经被修改,使得其他线程中的该变量失效。如果这个时候其他线程需要使用这个变量,会重新从主存中加载变量。 (1)操作use之前必须先执行readload操作 (2)操作assign之后必须执行storewrite操作:无论方法内改行代码后面还有多少代码,都是直接重新写回主内存中
  2. 保证有序性(防止指令重排序):volatile保证有序性主要是依靠内存屏障来禁⽌指令重排序的。
  3. 不能保证原子性。如果多线程并发执行Thread1对象里面的addOne()方法,num的最终值会跟理想值有偏差,因为num++底层实际上是3条指令((1)从主内存取值、(2)加1、(3)把计算后的值写回主内存),有可能线程在执行最后一条指令之前让出了CPU资源(线程挂起),这时候该线程无法获取到其他线程的修改值,导致值的计算有误。
class Thread1 {
    volatile int num = 0;
    public void addOne(){
        num++;
    }
}

拓展1:assign、store和write是什么?JMM规定的工作内存与主内存之间交互8种原子操作中的几种,具体有如下8种:

  • lock(加锁):将主内存中的变量锁定,为一个线程所独占。
  • unclock(解锁):将lock加的锁定解除,此时其它的线程可以有机会访问此变量。
  • read(读取):将主内存中的变量值读到工作内存当中。
  • load(载入):将read读取的值保存到工作内存中的变量副本中。
  • use(使用):将值传递给线程的代码执行引擎。
  • assign(赋值):将执行引擎处理返回的值重新赋值给变量副本。
  • store(存储):将变量副本的值存储到主内存中。
  • write(写入):将store存储的值写入到主内存的共享变量当中。

拓展2:

  • lock和unlock操作并不直接开放给用户使用,而是提供给像Synchronize关键字指定monitorenter和monitorexit隐式使用。关于Synchronize的监听器锁monitor,javac编译后会在作用的方法前后增加monitorenter和monitorexit指令,详细的可以查看Synchronize原理。

拓展3:缓存一致性协议MEIS:

二、指令重排序

源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令

指令重排序是编译器和处理器为了提高程序运行效率,在不影响单线程程序执行结果的前提下,将指令重排序,尽可能地提高并行度。注意是单线程。多线程的情况下指令重排序就会给程序带来问题。

  • JMM中定义一个先行发生原则(happens-before),语义是如果 A happens-before B,那么A的结果对B是可见的,满足八条原则,可以保证代码执行的顺序(www.cnblogs.com/niuyourou/p…
  • 比喻:单例DCL(double-check-lock)模式中new一个对象的过程
/**
 * 懒汉模式:需要的时候才去加载
 * 线程安全
 */
public class SingletonC {
 private static SingletonC instance = null;
 private SingletonC() {
 }

 public static SingletonC getInstance() {
  if (instance == null) {
   synchronized (SingletonC.class) {
    if (instance == null) {
     instance = new SingletonC();
    }
   }
  }
  return instance;
 }
}

instance = new SingletonC(); 主要做了3个事情:

(1) java虚拟机为对象分配一块内存x;

(2) 在内存x上为对象进行初始化;

(3) 将内存x的地址赋值给instance变量。

因为上述步骤2和3没有强依赖性,编译器有可能会将指令重排为:

(1)java虚拟机为对象分配一块内存x;

(2)将内存x的地址赋值给instance变量;

(3)在内存x上为对象进行初始化。

三、内存屏障:它是一个CPU指令

volatile保证有序性主要是依靠内存屏障来禁⽌指令重排序的。

  • LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1的数据装载先于Load2后所有的状态指令。Load1和Load2对应的代码,是不能指令重排的。
  • StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据⼀定刷回主存,对其他cpu可⻅,先于Store2以及后续指令
  • LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令
  • StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据⼀定刷回主存,对其他cpu可⻅,先于Load2以及后续指令的数据装载

对于volatile的读写操作,都会加⼊内存屏障:

  • 每个voliate写操作之前都会加⼊StoreStore内存屏障,禁⽌前⾯的普通写和他重排,每个volatile写操作后⾯都会加⼊StoreLoad屏障,禁⽌跟后⾯的volatile读写重排。
  • 每个voliate读操作之前都会加⼊LoadLoad内存屏障,禁⽌后⾯普通读和voliate读重排。每个volatile读操作之后都会加⼊LoadStore屏障,禁⽌后面的普通写和voliate读重排。

四、补充

并行和并发

  1. 并发:是指多线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程是在同时执行的,但其实并发只是一种逻辑上的同时执行;
  2. 并行:是指多个程序任务在不同CPU上同时执行,是真正意义上的同时执行。

synchronized和volatile的有序性比较

  1. synchronized的有序性:是持有锁的同步块只能串行的进入,但是内部的同步代码还是会发生重排序;
  2. volatile的有序性:是通过插入内存屏障来保证指令按照顺序执行。