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;