volatile关键字的作用是保证多线程执行的同步性。
在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile变量有什么不同:
1、对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
2、对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。
volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。
程序运行时需要CPU、高速缓存、内存三个部件协同工作,线程A把内存中的变量x读到了高速缓存,线程B现在修改了变量x。内存中的x虽然变了,但是线程A依旧认为高速缓存中的x是最新的。这就影响了线程之间的同步。
volatile关键字的作用就是:如果线程B修改了变量x,那么线程A的高缓中的x就失效了,需要从内存中重新加载。单例模式中使用volatile更稳妥,这种单例实现方式叫做“double check”,在进入synchronized区域之前检查一次,进入之后再检查一次。外层的if能够避免所有线程都进入synchronized区域,从而避免了线程同时等待一个锁,内层的if用于准确判断,免得其它线程已经创建了单例。
class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { syschronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
另外一种依赖类加载机制的单例模式实现方法
public class Singleton { static class SingletonHolder { static Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingletonHolder.instance; } }