深刻理解Volatile

转自:https://juejin.im/editor/drafts/5acda6976fb9a028d937821f


一旦一个共享变量(类的成员变量、 类的静态成员变量) 被 volatile 修饰以后, 那么就具有了两层语义:

  1. 保证了不一样线程对这个变量进行读取时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。 (volatile 解决了线程间共享变量的可见性问题)。编程

  2. 禁止进行指令重排序, 阻止编译器对代码的优化。缓存

内存可见性:

  • 第一: 使用 volatile 关键字会强制将修改的值当即写入主存;bash

  • 第二: 使用 volatile 关键字的话, 当线程 2 进行修改时, 会致使线程 1 的工做内存中缓存变量 stop 的缓存行无效(反映到硬件层的话, 就是 CPU 的 L1或者 L2 缓存中对应的缓存行无效) ;多线程

  • 第三: 因为线程 1 的工做内存中缓存变量 stop 的缓存行无效, 因此线程 1再次读取变量 stop 的值时会去主存读取。ide

那么, 在线程 2 修改 stop 值时(固然这里包括 2 个操做, 修改线程 2 工做内存中的值, 而后将修改后的值写入内存) , 会使得线程 1 的工做内存中缓存变量 stop 的缓存行无效, 而后线程 1 读取时, 发现本身的缓存行无效, 它会等待缓存行对应的主存地址被更新以后, 而后去对应的主存读取最新的值。性能

具体内容参考个人另一篇博客:优化

禁止重排序:

volatile 关键字禁止指令重排序有两层意思:ui

  • 当程序执行到 volatile 变量的读操做或者写操做时, 在其前面的操做的更改确定所有已经进行, 且结果已经对后面的操做可见; 在其后面的操做确定尚未进行this

  • 在进行指令优化时, 不能把 volatile 变量前面的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。spa

为了实现 volatile 的内存语义, 加入 volatile 关键字时, 编译器在生成字节码时,会在指令序列中插入内存屏障, 会多出一个 lock 前缀指令。 内存屏障是一组处理器指令, 解决禁止指令重排序和内存可见性的问题。 编译器和 CPU 能够在保证输出结果同样的状况下对指令重排序, 使性能获得优化。 处理器在进行重排序时是会考虑指令之间的数据依赖性。

内存屏障, 有 2 个做用:

  • 1.先于这个内存屏障的指令必须先执行, 后于这个内存屏障的指令必须后执行。

  • 2.使得内存可见性。 因此, 若是你的字段是 volatile, 在读指令前插入读屏障, 可让高速缓存中的数据失效, 从新从主内存加载数据。 在写指令以后插入写屏障, 能让写入缓存的最新数据写回到主内存。

Lock 前缀指令在多核处理器下会引起了两件事情:

  1. 将当前处理器中这个变量所在缓存行的数据会写回到系统内存。 这个写回内存的操做会引发在其余 CPU 里缓存了该内存地址的数据无效。 可是就算写回到内存, 若是其余处理器缓存的值仍是旧的, 再执行计算操做就会有问题, 因此在多处理器下, 为了保证各个处理器的缓存是一致的, 就会实现缓存一致性协议, 每一个处理器经过嗅探在总线上传播的数据来检查本身缓存的值是否是过时了, 当处理器发现本身缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态, 当处理器要对这个数据进行修改操做的时候, 会强制从新从系统内存里把数据读处处理器缓存里。

  2. 它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置, 也不会把前面的指令排到内存屏障的后面; 即在执行到内存屏障这句指令时, 在它前面的操做已经所有完成。

深刻理解缓存一致性问题:

当程序在运行过程当中, 会将运算须要的数据从主存复制一份到 CPU 的高速缓存当中, 那么 CPU 进行计算时就能够直接从它的高速缓存读取数据和向其中写入数据, 当运算结束以后, 再将高速缓存中的数据刷新到主存当中。 举个简单的例子, 好比下面的这段代码:

i = i+1复制代码

当线程执行这个语句时, 会先从主存当中读取 i 的值, 而后复制一份到高速缓存当中, 而后 CPU 执行指令对 i 进行加 1 操做, 而后将数据写入高速缓存,最后将高速缓存中 i 最新的值刷新到主存当中。这个代码在单线程中运行是没有任何问题的, 可是在多线程中运行就会有问题了。 在多核 CPU 中, 每条线程可能运行于不一样的 CPU 中, 所以每一个线程运行时有本身的高速缓存(对单核 CPU 来讲, 其实也会出现这种问题, 只不过是以线程调度的形式来分别执行的) 。

本文咱们以多核 CPU 为例好比同时有 2 个线程执行这段代码, 假如初始时 i 的值为 0, 那么咱们但愿两个线程执行完以后 i 的值变为 2。 可是事实会是这样吗?

可能存在下面一种状况: 初始时, 两个线程分别读取 i 的值存入各自所在的CPU 的高速缓存当中, 而后线程 1 进行加 1 操做, 而后把 i 的最新值 1 写入到内存。 此时线程 2 的高速缓存当中 i 的值仍是 0, 进行加 1 操做以后, i 的值为1, 而后线程 2 把 i 的值写入内存。最终结果 i 的值是 1, 而不是 2。 这就是著名的缓存一致性问题。 一般称这种被多个线程访问的变量为共享变量。也就是说, 若是一个变量在多个 CPU 中都存在缓存(通常在多线程编程时才会出现) , 那么就可能存在缓存不一致的问题。

如何解决缓存一致性的问题:为了解决缓存不一致性问题, 一般来讲有如下 2 种解决方法:1) 经过在总线加 LOCK#锁的方式2) 经过缓存一致性协议

经过在总线加 LOCK#锁的方式:在早期的 CPU 当中, 是经过在总线上加 LOCK#锁的形式来解决缓存不一致的问题。 由于 CPU 和其余部件进行通讯都是经过总线来进行的, 若是对总线加 LOCK#锁的话, 也就是说阻塞了其余 CPU 对其余部件访问(如内存) ,从而使得只能有一个 CPU 能使用这个变量的内存。 好比上面例子中 若是一个线程在执行 i = i +1, 若是在执行这段代码的过程当中, 在总线上发出了 LCOK#锁的信号, 那么只有等待这段代码彻底执行完毕以后, 其余 CPU 才能从变量 i所在的内存读取变量, 而后进行相应的操做。 这样就解决了缓存不一致的问题。可是上面的方式会有一个问题, 因为在锁住总线期间, 其余 CPU 没法访问内存, 致使效率下。

可是上面的方式会有一个问题, 因为在锁住总线期间, 其余 CPU 没法访问内存, 致使效率低下。

经过缓存一致性协议:因此就出现了缓存一致性协议。 该协议保证了每一个缓存中使用的共享变量的副本是一致的。

它核心的思想是: 当 CPU 向内存写入数据时, 若是发现操做的变量是共享变量, 即在其余 CPU 中也存在该变量的副本, 会发出信号通知其余 CPU 将该变量的缓存行置为无效状态, 所以当其余 CPU 须要读取这个变量时, 发现本身缓存中缓存该变量的缓存行是无效的, 那么它就会从内存从新读取。

内存屏障能够被分为如下几种类型:

  • LoadLoad 屏障: 对于这样的语句 Load1; LoadLoad; Load2, 在 Load2 及后续读取操做要读取的数据被访问前, 保证 Load1 要读取的数据被读取完毕。

  • StoreStore 屏障: 对于这样的语句 Store1; StoreStore; Store2, 在 Store2 及后续写入操做执行前, 保证 Store1 的写入操做对其它处理器可见。

  • LoadStore 屏障: 对于这样的语句 Load1; LoadStore; Store2, 在 Store2 及后续写入操做被刷出前, 保证 Load1 要读取的数据被读取完毕。

  • StoreLoad 屏障: 对于这样的语句 Store1; StoreLoad; Load2, 在 Load2 及后续全部读取操做执行前, 保证 Store1 的写入对全部处理器可见。 它的开销是四种屏障中最大的。 在大多数处理器的实现中, 这个屏障是个万能屏障, 兼具其它三种内存屏障的功能。

这里用一张图来详细分析指令的执行顺序:


StoreStore屏障能够保证在volatile写以前,其前面的全部普通写操做已经对任意处理器可见了,由于StoreStore屏障将保障上面全部的普通写在volatile写以前刷新到主内存

x86处理器仅仅会对写-读操做作重排序

所以会省略掉读-读、读-写和写-写操做作重排序的内存屏障

在x86中,JMM仅需在volatile后面插入一个StoreLoad屏障便可正确实现volatile写-读的内存语义

这意味着在x86处理器中,volatile写的开销比volatile读的大,由于StoreLoad屏障开销比较大

对于Volatile的使用,附上代码讲解:

/* 
 * 1、volatile 关键字:当多个线程进行操做共享数据时,能够保证内存中的数据可见。 
 * 相较于 synchronized 是一种较为轻量级的同步策略。 
 * 注意: 
 * 1. volatile 不具有“互斥性” 
 * 2. volatile 不能保证变量的“原子性” 
 */  
 public class TestVolatile {//main线程和ThreadDemo线程对flag属性的可见性问题  
​
    public static void main(String[] args) {  
        ThreadDemo td = new ThreadDemo();  
        new Thread(td).start();  
​
        while(true){  
            if(td.isFlag()){  
                System.out.println("------------------");  
                break;  
            }  
        }  
​
    }  
​
}  
​
class ThreadDemo implements Runnable {  
​
    //不添加volatile将不会输出--------------------  
    private volatile boolean flag = false;  
      
    @Override  
    public void run() {  
      
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException e) {  
        }  
      
        flag = true;  
      
        System.out.println("flag=" + isFlag());  
      
    }  
      
    public boolean isFlag() {  
        return flag;  
    }  
      
    public void setFlag(boolean flag) {  
        this.flag = flag;  
    }  
}  复制代码
相关文章
相关标签/搜索