前提
volatile相关的字节码如下:
...................... // 给volatile修饰的变量赋值
lock xxx // 相比较非volatile变量多出来的字节码
...................... // 赋值后的字节码
给volatile修饰的变量赋值,赋值后,字节码会多了一个“lock xxxxx”指令。下面的一些讲解需要了解这个前提。
原理
对声明了volatile的变量进行写操作,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回内存,如果其他处理器缓存的值还是旧的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
1.Lock前缀指令会引起处理器缓存回写到内存。
2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
一、volatile的使用场景
1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。
二、volatile的两个语义
1.内存可见性
当一个线程修改了volatile变量的值,新值对于其他线程来说是可以立即得知的。
普通变量的值在线程间传递时均需要通过主内存来完成。比如,线程A修改了一个普通变量的值,然后向主内存进行回写,另外一个线程B在线程A回写完成之后再对主内存进行读取操作,新变量值才会对线程B可见。
2.禁止指令重排序优化
普通变量仅能保证在方法的执行过程中所有依赖赋值结果的地方能获取到正确的结果,但不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
例如一段代码如下:
volatile boolean initFlag= false; // 行1
...... // 行2,执行init初始化
initFlag= true; // 行3
if(initFlag){ // 行4
...... // 行5,依赖行2初始化的一些操作
} // 行6
如果没有使用volatile 修饰变量initFlag,上述代码指令重排序优化后,可能行3在行2之前执行,此时并未执行初始化,但initFlag是true,就会执行行5,此时就会有问题。使用volatile 关键字就会避免此类情况的发生。
三、JMM对volatile变量定义的规则
1.load和use两个动作必须连续
每次使用volatile变量前都必须先从主内存刷新到最新的值,用于保证能看到其他线程对volatile变量做出的修改。
2.assign和store两个动作必须连续
每次修改volatile变量后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对volatile变量所做的修改。
3.volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同
四、volatile如何禁止指令重排序优化
volatile写的内存语义如下:
当写一个volatile变量时,JVM会把该线程对应的本地内存中的共享变量刷新到主内存。
volatile读的内存语义如下:
当读一个volatile变量时,JVM会把该线程对应的本地内存置为无效。线程接下来从主存中读取共享变量。
对volatile写和volatile读的内存语义做个总结:
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
编译器不会对volatile读与volatile读后面的任意内存操作重排序;
编译器不会对volatile写与volatile写前面的任意内存操作重排序。
组合这两个条件,意味着为了同时实现volatile读与volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。