转自:https://juejin.im/editor/drafts/5acda6976fb9a028d937821f
一旦一个共享变量(类的成员变量、 类的静态成员变量) 被 volatile 修饰之后, 那么就具备了两层语义:
保证了不同线程对这个变量进行读取时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 (volatile 解决了线程间共享变量的可见性问题)。
禁止进行指令重排序, 阻止编译器对代码的优化。
内存可见性:
第一: 使用 volatile 关键字会强制将修改的值立即写入主存;
第二: 使用 volatile 关键字的话, 当线程 2 进行修改时, 会导致线程 1 的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话, 就是 CPU 的 L1或者 L2 缓存中对应的缓存行无效) ;
第三: 由于线程 1 的工作内存中缓存变量 stop 的缓存行无效, 所以线程 1再次读取变量 stop 的值时会去主存读取。
那么, 在线程 2 修改 stop 值时(当然这里包括 2 个操作, 修改线程 2 工作内存中的值, 然后将修改后的值写入内存) , 会使得线程 1 的工作内存中缓存变量 stop 的缓存行无效, 然后线程 1 读取时, 发现自己的缓存行无效, 它会等待缓存行对应的主存地址被更新之后, 然后去对应的主存读取最新的值。
禁止重排序:
volatile 关键字禁止指令重排序有两层意思:
当程序执行到 volatile 变量的读操作或者写操作时, 在其前面的操作的更改肯定全部已经进行, 且结果已经对后面的操作可见; 在其后面的操作肯定还没有进行
在进行指令优化时, 不能把 volatile 变量前面的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
为了实现 volatile 的内存语义, 加入 volatile 关键字时, 编译器在生成字节码时,会在指令序列中插入内存屏障, 会多出一个 lock 前缀指令。 内存屏障是一组处理器指令, 解决禁止指令重排序和内存可见性的问题。 编译器和 CPU 可以在保证输出结果一样的情况下对指令重排序, 使性能得到优化。 处理器在进行重排序时是会考虑指令之间的数据依赖性。
1.先于这个内存屏障的指令必须先执行, 后于这个内存屏障的指令必须后执行。
2.使得内存可见性。 所以, 如果你的字段是 volatile, 在读指令前插入读屏障, 可以让高速缓存中的数据失效, 重新从主内存加载数据。 在写指令之后插入写屏障, 能让写入缓存的最新数据写回到主内存。
将当前处理器中这个变量所在缓存行的数据会写回到系统内存。 这个写回内存的操作会引起在其他 CPU 里缓存了该内存地址的数据无效。 但是就算写回到内存, 如果其他处理器缓存的值还是旧的, 再执行计算操作就会有问题, 所以在多处理器下, 为了保证各个处理器的缓存是一致的, 就会实现缓存一致性协议, 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了, 当处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态, 当处理器要对这个数据进行修改操作的时候, 会强制重新从系统内存里把数据读到处理器缓存里。
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 也不会把前面的指令排到内存屏障的后面; 即在执行到内存屏障这句指令时, 在它前面的操作已经全部完成。
深入理解缓存一致性问题:
当程序在运行过程中, 会将运算需要的数据从主存复制一份到 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 需要读取这个变量时, 发现自己缓存中缓存该变量的缓存行是无效的, 那么它就会从内存重新读取。
内存屏障可以被分为以下几种类型:
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中,JMM仅需在volatile后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义
这意味着在x86处理器中,volatile写的开销比volatile读的大,因为StoreLoad屏障开销比较大
对于Volatile的使用,附上代码讲解:
/*
* 一、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;
}
}