是什么
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁还要更加方便。如果一个字段被声明程volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
为什么
1.在并发编程中会存在多个线程操作同一个共享变量的情况,因为是并发编程,所以是多个线程同时执行
2.那么为什么要用它呢?
volatile是轻量级的synchronized,它在多处理器并发中保证了共享变量的“可见性”。可见性的意识是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换调度。
实现原理
Java代码如下:
instance=new Singleton(); //instance是volatile变量
转变成汇编代码,如下:
0x01a3deld:movb $0*0,0*1104800(%esi);0x01a3de24;lock addl $0*0,(%esp);
解析:
从上面的汇编代码可以看到,存在lock前缀
lock前缀的指令在多核处理器下会引发两件事情:
(1)将当前处理器缓存行的数据写回到系统内存
(2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效---------因为无效所以其他CPU想要使用这个数据时就需要从内存中重新拉取数据
操作步骤如图所示:
解释:
1.给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值
2.线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值(缓存一致性协议)
使用示例----单例模式为什么要用volatile关键字
线程安全的单例模式常见写法是双重检查加锁,代码如下:
class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized(Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
其他具体的这里就不做解释了,下面一起来看一下为什么要加volatile呢?
首先理解new Singleton()做了什么,步骤如下:
1.看class对象是否加载到内存中,如果没有就先加载class对象
2.分配内存空间,初始化实例
3.调用构造函数
4.返回地址给引用
而CPU为了优化程序,可能会进行指令重排序,如果打乱3,4这两个步骤,导致实例内存还没分配,就被使用了,那么会有什么bug吗?
其实如果是单线程执行是没有问题的,但是如果变成多线程执行就会有问题,问题如下:
线程A执行到new Singleton(),开始实例化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数,这是时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,那么加上volatile之后,就保证new不会被指令重排序,具体原理稍后做解释!
volatile重排序规则表
所以实例的第四个步骤为:返回地址给引用 ,此时是对volatile变量进行写操作,所以根据上表,第一操作为任何操作都不可以进行重排序,所以3 4两个步骤不进行重排序,所以多线程情况下就不会出现问题了!