本文主要从volatile关键字的两大特性来总结。
1、保证线程可见性;
2、禁止指令重排序。
一、保证线程可见性
可见性是什么,我们看之前的一篇文章:多线程与高并发(2)——synchronized用法详解。这里我们直接拿过来看:
1、线程可见性
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
从上图可以看到,java内存模型中,是依赖主内存来实现可见性,修改时新值同步到主内存,读取也是从主内存刷新变量值。无论是普通变量还是volatile变量都是如此,但是两者的区别是:volatile的特殊规则能够保证新值立即同步到主内存,以及每次使用前立即从主内存刷新。
实现可见性的关键字有:volatile,synchronized,final。
synchronized实现可见性的规则: 对一个变量执行unlock之前,必须先把此变量同步回主内存中。
final的可见性: 被final修饰的变量,不管是常量还是变量,其值或者引用是不可变的,在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸:其他线程可能通过这个引用访问到“初始化一半”的对象),那么其他线程就能看见final字段的值。
2、缓存一致性协议之MESI
volatile保持可见性的原理是使用了缓存一致性协议,描述如下:
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
由于volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值,会引发总线风暴,所以不要大量使用volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
二、禁止指令重排序
1、什么是指令重排序
变量赋值操作的顺序与程序代码中的执行顺序是不一致的。
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。具体的可以参考:什么是重排序
JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑。
2、怎么禁止指令重排序
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。 为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
3、DCL单例模式(双重检验锁)
单例模式直接看我之前的一篇文章:java(面向对象)的23种设计模式(2)——单例模式详解
这里我们直接看DCL的代码:
/**
* volatile 关键字,禁止指令重排序
*/
private static volatile Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//先做为空判断
if (singleton == null) {
synchronized (Singleton.class) {
//加锁之后再做一次为空判断
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
为什么要这么写呢,首先看以下代码:
public class Singleton {
int a = 5;//考虑指令重排序的问题
}
singleton = new Singleton()的字节码如下:
0: new #2 // class com/reasearch/Singleton
3: dup
4: invokespecial #3 // Method com/reasearch/Singleton."<init>":()V
7: astore_1
字节码执行过程如下:
1、new 分配空间,a=0
2、invokespecial 构造方法 a=5
3、astore_1将对象赋给singleton
这是理想的状态,2和3语义和逻辑上没有什么关联,因此jvm可以允许这些指令乱序执行,即先执行3再执行2 。
回到DCL代码中,如果不加volatile关键字的话,先执行3再执行2会发现,Singleton不为空,但a也不为5,但其实这个对象是一个问题对象,是一个半初始化的对象,即a=0。所以加上volatile关键字,禁止指令重排序就能避免这个问题。
三、volatile不能保证原子性
假如说线程A在做了i++,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经不再需要i的值了(读取过程已经结束,不会再去主内存中取值了),而是直接交给处理器去做++的操作,于是当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile不能保证原子性。
解决原子性问题的方法:Synchronized或Lock都可以实现,还有一种方式就是CAS(Compare And Swap)。