多线程之volatile关键字理解

273 阅读3分钟

前言

在明白volatile是什么之前,首先要知道volatile并不保证原子性,它只是java内存模型中线程协同数据的手段,可以解决并发编程中的可见性问题。

volatile的两个语义

volatile关键字有两个语义,第一个是保证可见性,第二个是保证有序性。

保证可见性

我们知道,在Java内存模型中,有主内存和工作内存,工作内存是Java线程私有的,而主内存是线程公有的,数据存储在主内存中,线程想要读取某个变量,会先从主内存去读然后加载进自身线程的工作内存里。

那么这样就会有个可见性问题:线程1和线程2读取了主内存某个变量data的值,然后线程1修改data的值,但是线程2没有跟着更新,保存的还是data的旧值。

这个问题可以通过volatile关键字解决,它起了两个作用:

第一:一旦在变量前面加了volatile修饰,那么只有有任何线程读取该变量并且在自身工作内存中修改了该变量的值,那么就会强制将这个变量最新的值刷回主内存中,使得主内存的变量值也变为最新值。

第二:有了第一个作用还是不够的,所以第二个作用是会使其他线程的工作内存存的变量副本直接失效过期,不能再使用,所以其他线程如果还想使用该变量,就会重新到主内存去加载读取。

保证有序性 这个语义是通过禁止指令重排序实现的,涉及到较为复杂的指令重排和内存屏障的概念,只是简单介绍下。

(1)何为指令重排序: 从硬件结构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给相应电路单元处理。但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。简单点说,CPU在执行指令时并不是按照代码顺序去执行指令,在内部CPU可能会将代码顺序进行调整以便更适于指令 的执行。但是,不管怎么调整,指令的执行最后结果都会与代码顺序执行的结果相符,所以从外部看起来重排序依然是有序的。

(2)volatile之所以能禁止指令重排序是因为它多设置了一个内存屏障操作,这个内存屏障操作指令为“lock addl $0x0,(%esp)”(这条指令是个空操作),有了这个指令,重排序时不能把后面的指令重排序到内存屏障之前的位置。而这条指令的lock前缀是使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,这种操作相当于对Cache中的变量做了一次“store和write“操作,因为volatile变量要求工作内存中发生变化之后,必须马上回写到主内存。volatile变量的修改对其他CPU立即可见也正是通过这个空操作指令。

lock addl $0x0,(%esp)指令将修改同步到内存时,意味着所有之前的操作都已经执行完毕,这样便形成了“指令重排序无法越过内存屏障”的效果。

volatile关键字的读操作性能消耗与普通变量几乎没什么差别,但是写操作可能会慢一些,因为它需要再本地代码中插入内存屏障指令来保证处理器不发生乱序执行