volatile关键字
- volatile关键字是java虚拟机提供的轻量级的同步机制,它保证了不同线程对共享变量操作的内存可见性,但不保证原子性,禁止指令重排。
- 把加入volatile关键字的代码和未加入的代码的都生成汇编代码,会发现加入volatile关键字的代码多出一个lock前缀的指令。
- lock前缀指令相当于一个内存屏障,提供了以下功能:1. 重排序时不能把后面的指令重排序到内存屏障之前的位置。2.使得本CPU的Cache写入内存。3. 写入动作会使其他处理器的缓存无效,让新写入的值对别的线程可见。
- 如果对声明了volatile的变量进行写操作,jvm会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到主内存中,为了保证各个处理器缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的数值是否过期,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前的缓存行设置成无效状态,当处理器再次读取这个数值时,会重新从主内存中把数据读取到缓存里。
JMM内存模型
-
Java 内存模型规定所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行而不能直接读写主内存中的变量,不同线程之间无法相互直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。
-
Java 内存模型对主内存与工作内存之间的具体交互协议定义了八种操作,具体如下:
- lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):作用于工作内存变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时执行此操作。
- assign(赋值):作用于工作内存变量,把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个需要给变量进行赋值的字节码指令时执行此操作。
- store(存储):作用于工作内存变量,把工作内存中一个变量的值传递到主内存中,以便后续 write 操作。
- write(写入):作用于主内存变量,把 store 操作从工作内存中得到的值放入主内存变量中。
-
在线程执行时,首先会从主存中read变量值,再load到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。
-
JMM 可以保证并发编程场景中的原子性、可见性和有序性。
JMM三大特性
- 原子性:一个操作不可被中断,要么执行,要么不执行。类似事务操作,Java 基本类型数据的访问大都是原子操作,long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作。在64位JVM中是原子操作。
- 可见性:当一个线程对共享变量进行了修改操作,其他线程可以立即感知到共享变量的改变。
- 有序性:是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。另外,JMM具备一些先天的有序性,即不需要通过任何手段就可以保证的有序性,通常称为happens-before原则。
- 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁
- volatile变量规则: 对一个volatile域的写,happens-before于后续对这个volatile域的读
传递性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C - start()规则: 如果线程A执行操作ThreadB_start()(启动线程B) , 那么A线程的ThreadB_start()happens-before 于B中的任意操作
- join()原则: 如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- interrupt()原则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,7. 可以通过Thread.interrupted()方法检测是否有中断发生
- finalize()原则:一个对象的初始化完成先行发生于它的finalize()方法的开始
volatile关键字如何满足并发编程的三大特性的
- volatile变量规则:对于volatile域的写,happens-before对volatile域的读。其实就是如果一个变量声明成是volatile的,那么当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值。也就是说volatile关键字可以保证可见性以及有序性。
- 从内存语义上看:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
- 不能保证原子性,要是说能保证,也只是对单个volatile变量的读/写具有原子性,但是对于类似volatile++这样的复合操作就无能为力了。例如线程A进行a++,线程B进行a++,线程A修改完值后同步到主内存,这时B已经做了部分累加操作,但会使线程B的缓存行无效,因此虽然进行了两次自加操作,但是实际上进行了一次。
volatile的应用
-
状态量标记,这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见。比synchronized,Lock有一定的效率提升。
int a = 0; volatile bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
-
单例模式的实现,典型的双重检查锁定(DCL)
class Singleton{ private volatile static Singleton instance = null; //把默认的构造方法私有化 private Singleton() { } public static Singleton getInstance() { if(instance==null) {//如果第一次检测不为空,那么就不需要加锁和初始化,降低开销 synchronized (Singleton.class) {//加锁,线程A执行,线程B等待 //线程A初始化成功,线程B开始执行 if(instance==null) instance = new Singleton(); } } return instance; } }
-
创建一个对象可以分3步,1. 分配对象空间;2初始化对象;3设置instance指向刚分配的内存地址;
假设在线程A创建对象的时候指令重排(1–>3–>2),在线程A刚执行完3之后,线程C进来,这时线程C判断instance是否为空,不为空直接返回,但是这时还没有初始化对象。
参考 :https://juejin.im/post/5a2b53b7f265da432a7b821c
参考 :https://juejin.im/post/5d9c8ab4518825094e372706