volatile 关键字

一旦一个共享变量(包括类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么该变量就 具备了两层含义:

1. 保证了不同线程对这个变量进行读取时的可见性,即一个线程修改了某个变量的值,这个新值对 其他线程是立即可见的。

volatile 解决了线程间共享变量的可见性问题。

第一,使用 volatile 关键会强制将修改的值立即写入主存。

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

第三,由于线程 1 的工作内存中缓存变量的缓存行无效,所以线程 1 再次读取变量的值会去主存中读取。

那么,在线程 2 修改变量的值时(此处的修改包括两个操作,修改线程 2 工作内存中的值,然后将修改后的值 写入内存),会使得线程 1 的工作内存中缓存变量的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效, 它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。因此,线程 1 读取到的就是最新 的正确的(期望的)值。

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

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

(1) 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行, 且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

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

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

内存屏障,有两个作用:

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

(2) 使得内存可见性。 所以,如果字段被 volatile 修饰,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新 从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

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

(1) 将当前处理器中这个变量所在缓存行的数据会写回到系统内存。 这个写会内存的操作会引起其它 CPU 中缓存的该内存地址的数据无效。但是就算写回到内存, 如果其它处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证 各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据 来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将 当前处理器的缓存行设置为无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存中 把数据读到处理器缓存中。

(2) 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到 内存屏障后面,即在执行到内存屏障这条指令时,在它前面的操作已经全部完成。

内存屏障可以被分为以下几种类型:

LoadLoad 屏障 对于如下形式的语句,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。

Load1;
LoadLoad;
Load2;

StoreStore 屏障 对于如下形式的语句,在 Store2 及后续写入操作之前,保证 Store1 的写入操作对其它处理器可见。

Store1;
StoreStore;
Store2;

LoadStore 屏障 对于如下形式的语句,在 Store2 及后续写操作被刷出前,保证 Load1 要读取的数据被读取完毕。

Load1;
LoadStore;
Store2;

StoreLoad 屏障 对于如下形式的语句,在 Load2 及后续写入操作被刷出前,保证 Load1 要读取的数据被读取完毕。 它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是万能屏障,兼具其它三种内存屏障 的功能。

Store1;
StoreLoad;
Load2;

results matching ""

    No results matching ""