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 保证了顺序不一定一致,但结果一定一致(重排前和重排后指令执行得到的结果一致)。
但在多线程环境下,重排序则会引起很大的问题,这又涉及了线程安全的要素:有序性
有序性是指程序执行的顺序应当按照代码的先后顺序执行。
为了更好地理解有序性,下面通过一个例子来分析:
//成员变量 i
int i = 0;
//线程一的执行代码
Thread.sleep(10);
i++;
f = true;
//线程二的执行代码
while(!f){
System.out.println(i);
}
理想的结果应该是:线程二不停地打印 0,最后打印一个1,终止。
在线程一里,f 和 i 没有依赖性,如果发生了指令重排,那么 f = true 发生在 i++ 之前,就有可能导致线程二在终止循环前输出的全部是 0。
需要注意的是,这种情况并不常见,再次运行并不一定能重现,正因为如此,很可能会导致出现一些莫名的问题。
如果修改上方代码中 i 的定义为使用 volatile 关键字来修饰,那么就可以保证最后的输出结果符合预期。
这是因为,被 volatile 修饰的变量,CPU 不会对它做重排序优化,所以也就保证了有序性。
volatile 不保证操作的原子性
原子性:一个或多个操作,要么全部连续执行且不会被任何因素中断,要么就都不执行。
一眼看上去,这个概念和数据库概念里的事务(Transaction)很类似,没错,事务就是一种原子性操作。
原子性、可见性和有序性,是线程安全的三要素。
需要特别注意的是,volatile 保证可见性和有序性,但是不保证操作的原子性,下面的代码将会证明这一点: