提起volatile,大家有什么印象呢?大多数的时候对他印象不深,作为安卓程序猿,我唯一的印象是在双检索单例的实现里,用到了volatile
public class Singleton {
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;
}
}
那这里用volatile干嘛呢?一句话概括:禁止重排序,增加内存屏障,以保证有序性和可见性
什么是可见性
通俗点讲就是你可以一直看到这个变量,换句话说,如果不满足可见性,那么你就有可能暂时看不到这个变量,就像猴子开了w,短时间内你看到的可能是假猴。
为什么Java里会出现这种问题,我们拿Java内存模型(JMM)举例便知。
根据JMM描述,每个线程有自己的本地内存,里面存放着共享变量的副本,当线程A对共享变量做改动时,此时会先放到本地内存中,如果此时没有将变量写回到主内存中,那么线程b从自己的本地内存拿到的变量则是过时的,不是最新的,就会引发问题。
所以,加了volatile,当你修改了共享变量后,他会及时帮你把变量写回到主内存中,来避免上述问题。
什么是有序性
有序性,就是保证你的代码在多线程中是顺序执行的,换句话说,你的代码可能在多线程中不是顺序执行的,这个又怎么理解呢?这是因为你的代码在编译期、运行时都可能被优化,打乱你原本的代码执行顺序,从而提升性能,在单线程中执行结果不会受到影响,而对于多线程中,重排序就可能引发意外情况。
重排序分为三种:
-
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句 的执行顺序。
-
指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序
不满足有序性就有可能破坏可见性,volatile这里通过禁止重排序,保证有序性,并以此保证可见性。
volatile怎么实现的?
禁止重排序。
-
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保
-
volatile写之前的操作不会被编译器重排序到volatile写之后。
-
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
-
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
上面的东西不用记,仅需要知道可以禁止重排序即可。
那怎么实现上述列表的禁止重排序操作呢?这里就提出了JVM的内存屏障,并在编译期在volatile前面或后面增加内存屏障,来实现禁止重排序。
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的后面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的处理。
回到双检索单例
public class Singleton {
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;
}
}
BUG举例:这里我们用两个线程Thread-0和Thread-1来描述下面发生的过程:
- Thread-0 调用getSington(), 进入sync块执行
- Thread-0 进入synchronized块执行
singleton = new Singleton();,他会被分成三步来执行- 第一步:分配内存空间,用来初始化对象,此时我们得到一段空间 address
- 第二步:将该address的空间初始化为singleton对象
- 第三步:将singleton指针指向初始化好后的address,此时singleton就不为null了
(注意这里的第二步第三步,在没有volatile关键字时,他俩的顺序是有可能互换的)
- 此时Thread-1调用getSingleton,发现singleton != null,便直接返回了singleton对象。
这里如果第二步第三步发生互换,然后Thread-1刚好在第二步后发生,那么此时Thread-1返回的对象则是一个还没有被初始化的地址。
总结
volatile干了什么这下就清楚多了~