关于这个关键字的使用,无敌推荐一篇文章:https://www.cnblogs.com/dolphin0520/p/3920373.html#
这里只是简单总结一下:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,也就是说每个线程所读的同一个变量的值都应该是一致的。
2)禁止进行指令重排序。
简单说明一下:
可见性:
cpu在处理一个数据时并不是马上会写到内存中的,而是先存到高速内存cache然后再写到主存。
在多线程的环境下,如果多个线程被不同的cpu执行,某一个变量可能在不同的cpu上的cache里的值是不一致的,因为cache没有及时写到内存中。而使用volatile关键字,会让这个变量在更新后立即写入内存,保证了多个线程对于某个变量的可见性。
重排序:
cpu对于代码执行的顺序不是按照程序写好的顺序,假如代码所涉及的变量之间前后没有必需的排序关系(实际上cpu会误判),cpu可能会重新排列它们的顺序来提高效率。例如:int a = 1;a++;int b = 2;b++;a++;b++;a++;b++;那么cpu肯定会先对a执行完再去操作b这样效率更高(这里只是假设)。
场景:
保证可见性:
public class Test {
public volatile int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
假如不添加volatile,inc的值将会小于10000,因为线程之间可能会产生不可见现象,某个线程某一时刻读取到的可能是旧值
(测试没有达到预期效果,但是理论是正确的)
保证不被重排序:
经典的双重检查锁的懒汉校检模式
//懒汉式(线程安全,高效双重检查锁)
class Singleton4{
private Singleton4() { };
private volatile static Singleton4 singleton4 = null;
public static Singleton4 getInstance(){
if(singleton4 == null){
synchronized (Singleton4.class){
if(singleton4 == null){
singleton4 = new Singleton4();
}
}
}
return singleton4;
}
}
singleton4 = new Singleton4();
对于这样一条语句,实际上它并不是原子操作,而是一共有三步:
(1)分配内存空间。
(2)初始化对象。
(3)将内存空间的地址赋值给对应的引用。
假如singleton4没有使用volatile关键字,发生了重排序,将内存空间的地址赋值给singleton4这一个操作在对象初始化之前发生了(1->3->2),那么此时singleton4便不指向null了,但此时它还没调用构造函数初始化。
假设线程1进入到singleton4 = new Singleton4()这行代码,然后在给singleton4指定内存空间之后马上被线程2抢占了,那么线程2判断到singleton4不为null,那么它就会去返回singleton4这个不完整的未初始化的对象。
而使用了volatile禁止了重排序,那么给singleton4指定内存地址这个代码永远是最后执行(在初始化之后执行),就不会有问题了。
volatile的原理和实现机制:
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到 内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
|
|