Volatile

来自姬鸿昌的知识库
跳到导航 跳到搜索

volatile 的使用是为了线程安全,但 volatile 不保证线程安全。

线程安全有三个要素:可见性、有序性和原子性。

线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改发生与预期不符的情况。

volatile 的作用

volatile 有以下三个作用:

volatile 用于解决多核 CPU cache(高速缓存)导致的变量不同步

这本质上是个硬件问题,其根源在于:CPU 的高速缓存的读取速度远远快于主存(物理内存)。

所以,CPU 在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显然提高了读取的性能。

而多核 CPU 有多个这样的缓存。这就带来了问题,当某个 CPU(例如 CPU1)修改了这个变量(比如把 a 的值从 1 修改为 2 ),但是其他的 CPU(例如 CPU2)在修改前已经把 a=1 读取到自己的缓存了,当 CPU2 再次读取数据的时候,它仍然会去自己的缓存区中读取,此时读取到的值仍然是 1,但是实际上这个值已经变成 2 了。

这里,就涉及了线程安全的要素:可见性

可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能立即看到修改后的值。

volatile 的实现原理是内存屏障(Memory Barrier),其原理为:当 CPU 写数据时,如果发现一个变量在其他 CPU 中存有副本,那么会发出信号通知其他 CPU 将该副本对应的缓存行置为无效状态,当其他 CPU 读取到 变量副本的时候,会发现该缓存行是无效的,然后,它会从主存重新读取变量。




volatile 可以解决指令重排序的问题

一般情况下,程序是按照顺序执行的,例如下面的代码:

1        int i = 0;
2        i++;
3        boolean f = false;
4        f = true;

如果 i++ 发生在 int i = 0 之前,那么会不可避免地出错,CPU 在执行代码对应指令的时候,会认为 1、2 两行是具备依赖性的,因此,CPU 一定会安排行 1 早于行 2 执行。

那么,int i = 0 一定会早于 boolean f = false 吗?

并不一定,CPU 在运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。

在单线程执行的情况下,发生重排是没有问题的,CPU 保证了顺序不一定一致,但结果一定一致(重排前和重排后指令执行得到的结果一致)。

但在多线程环境下,重排序则会引起很大的问题,这又涉及了线程安全的要素:有序性

有序性是指程序执行的顺序应当按照代码的先后顺序执行。

为了更好地理解有序性,可以看看多线程情况下指令重排对程序执行造成的影响:

Java 重排序对多线程的影响