Java并发:volatile详解

101 阅读4分钟

提起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)举例便知。

image.png

根据JMM描述,每个线程有自己的本地内存,里面存放着共享变量的副本,当线程A对共享变量做改动时,此时会先放到本地内存中,如果此时没有将变量写回到主内存中,那么线程b从自己的本地内存拿到的变量则是过时的,不是最新的,就会引发问题。

所以,加了volatile,当你修改了共享变量后,他会及时帮你把变量写回到主内存中,来避免上述问题。

什么是有序性

有序性,就是保证你的代码在多线程中是顺序执行的,换句话说,你的代码可能在多线程中不是顺序执行的,这个又怎么理解呢?这是因为你的代码在编译期、运行时都可能被优化,打乱你原本的代码执行顺序,从而提升性能,在单线程中执行结果不会受到影响,而对于多线程中,重排序就可能引发意外情况。

重排序分为三种:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句 的执行顺序。

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。

  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序

image.png

不满足有序性就有可能破坏可见性,volatile这里通过禁止重排序,保证有序性,并以此保证可见性。

volatile怎么实现的?

禁止重排序。

image.png

  • 当第二个操作是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来描述下面发生的过程:

  1. Thread-0 调用getSington(), 进入sync块执行
  2. Thread-0 进入synchronized块执行singleton = new Singleton();,他会被分成三步来执行
    • 第一步:分配内存空间,用来初始化对象,此时我们得到一段空间 address
    • 第二步:将该address的空间初始化为singleton对象
    • 第三步:将singleton指针指向初始化好后的address,此时singleton就不为null了

(注意这里的第二步第三步,在没有volatile关键字时,他俩的顺序是有可能互换的)

  1. 此时Thread-1调用getSingleton,发现singleton != null,便直接返回了singleton对象。

这里如果第二步第三步发生互换,然后Thread-1刚好在第二步后发生,那么此时Thread-1返回的对象则是一个还没有被初始化的地址。

总结

volatile干了什么这下就清楚多了~