A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

关于这个关键字的使用,无敌推荐一篇文章: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中对应的缓存行无效。


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马