Volatile 如何保证可见性
在前面我们提到volatile关键字可以保证多个线程运行时的可见性问题。在单核CPU的情况下,是不存在可见性问题的,如果是多核CPU,可见性问题就会暴露出来。
我们知道线程中运行的代码最终都是交给CPU执行的,而代码执行时所需使用到的数据来自于内存(或者称之为主存)。但是CPU是不会直接操作内存的,每个CPU都会有自己的缓存,操作缓存的速度比操作主存更快。
因此当某个线程需要修改一个数据时,事实上步骤是如下的:
- 将主存中的数据加载到缓存中
- CPU对缓存中的数据进行修改
- 将修改后的值刷新到内存中
第一步:线程1、线程2、线程3操作的是主存中的同一个变量,并且分别交由CPU1、CPU2、CPU3处理。
第二步:3个CPU分别将主存中变量加载到缓存中
第三步:各自将修改后的值刷新到主存总
问题就出现在第二步,因为每个CPU操作的是各自的缓存,所以不同的CPU之间是无法感知其他CPU对这个变量的修改的,最终就可能导致结果与我们的预期不符。
而使用了volatile关键字之后,情况就有所不同,volatile关键字有两层语义:
1、立即将缓存中数据写会到内存中
2、其他处理器通过嗅探总线上传播过来了数据监测自己缓存的值是不是过期了,如果过期了,就会对应的缓存中的数据置为无效。而当处理器对这个数据进行修改时,会重新从内存中把数据读取到缓存中进行处理。
在这种情况下,不同的CPU之间就可以感知其他CPU对变量的修改,并重新从内存中加载更新后的值,因此可以解决可见性问题。
如何拥有可见性?
先介绍一下Java内存模型中定义的8种工作内存与主内存之间的原子操作
- lock( 锁定 ):作用于主内存的变量,把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,把一个处于锁定的变量释放出来,释放变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存种的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的值放入主内存的变量中。