volatile原理及使用场景
以前一直困惑于volatile到底有什么功能、怎么使用。多方搜索资料,终于理清了头绪,记录一下。
首先,volatile有两个作用:(1)保证变量的可见性(2)防止指令重排序。这两个作用都是通过设置内存屏障实现的。
防止指令重排序这个很好理解,对一个变量可能有多种操作,如果使用操作在赋值操作之前发生,就会导致数据错误甚至空指针等问题。
一直以来最困惑的还是第(1)个作用,volatile是怎么保证变量的可见性的,可见性又是什么呢,为什么需要有可见性?
首先,说一下为什么变量会不可见?
Java每个线程工作都有一个工作空间,需要的变量都是从主存中加载进来的。Java内存模型如下(JMM):
线程访问一个共享的变量时,都需要先从主存中加载一个副本到自己的工作内存中,经过自己修改后再更新到主存中去。在这个过程中可能出现这种情况:线程A在工作内存中修改了变量1的值,但是还没有写入主存,这档口线程B将变量1加载到自己工作内存中。显然,线程B拿到的不是变量1的最新值了。
变量可见性就是: 这个变量被任何一个线程修改了,其他线程都能“看见”,也就是能取到变量最新的值。
volatile实现可见性的原理
JVM线程工作时的原子性指令有:
read: 从主存读取一个变量的值的副本到线程的工作内存。
load:read读来的值,把这个值填充到线程使用的变量中,然后就可以使用了。
use:要使用一个变量,先发出这个指令。
assign:赋值,给变量一个新值。
write:将变量的新值运送到主存中。
store:将写到主存的新值存到那个变量中。
上述操作必定是顺序执行的,但可不一定连续,中间可能插入其他指令。为了保证可见性:关键就是保证load、use的执行顺序不被打乱(保证使用变量前一定进行了load操作,从主存拿最新值来),assign、wirte的执行顺序不被打乱(保证赋值后马上就是把值写到主存)。
所以使用内存屏障, CPU指令,可以禁止指令执行乱序:插入一个内存屏障, 相当于告诉CPU和编译器指令顺序先于这个指令的必须先执行,后于这个命令的必须后执行。
解决第一个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时在读指令use之前插入一条读屏障指令重新从主存加载最新值进来,保证了load、use指令的执行顺序不乱;写操作时在写指令assign之后插入一条写屏障指令,将工作内存变量的最新值立刻写入主存变量。
解决第二个因素(指令重排): 由于读写数据时会在之前/后插入一条内存屏障指令,因此volatile可以禁止指令重排序。
volatile使用场景
只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
文章参考: