volatile关键字简单分析

1. 处理器的内存模型

因为程序运行时数据是放在主存(物理内存)的,但是主存的读取速度通常跟不上CPU处理的速度,因此为了不影响效率,每个CPU都会有自己的高速缓存,通常CPU会先读取数据到高速缓存,修改数据,再定期把修改同步到主存,所以如果有多个线程同时修改一个数据就有可能会有缓存一致性问题。

解决方法:

(1)在总线加锁:同一时刻只允许一个线程访问共享数据,因此效率很低

(2)通过缓存一致性协议:例如intel的MESI协议,其核心思想是:如果一个CPU想要修改某个数据并发现它是共享数据,那么他就会给其他使用该数据的CPU一个信息:关于该变量的缓存是无效的,如果要使用它,你需要重新从内存中读取。

2. java内存模型

要想并发程序正确的执行,原子性,可见性,有序性 这三个条件缺一不可,在java中对这三个方面的设定如下:

(1)原子性

在java中只有简单的读取、赋值(而且必须是直接将数字赋给某个变量,而非把变量赋给另一个变量)是原子操作。

因此:

i=3;

j=i;

i++;

j=j+1中只有第一个是原子操作。

(2)可见性

如果采用volatile关键字那么它会保证被修改的变量的值会立即同步到内存,并且其他线程使用时会从内存中取。

synchronized和lock也能保证可见性,不过是通过加锁来实现(即保证两个操作该变量的线程不会同时执行,且执行完后会及时更新主存中变量的值)。

(3)有序性

在java内存模型中允许对指令重排序,但是并不是说排序后的指令顺序无法预见,有一些原则是在重排序的时候一定会保证的,这个原则称为happens-before原则,如果指令的顺序无法从这个原则推导出来,那么他们之间的执行顺序就是无法保证的:

程序次序规则:一个线程内,书写在前的代码操作会先行发生于书写在后面的代码的操作;

管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;

volatile变量规则:对一个volatile变量的写操作先行发生于读操作;

线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;

线程终止规则:线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程以及终止执行。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

3. volatile关键字

(1)volatile的两层语义:

保证变量的可见性,避免缓存一致性问题

禁止指令重排序

关于禁止指令重排序,使用volatile关键字可以保证:

首先:所有对volatile变量的操作都是有序的,即:在操作volatile变量时,其前面的操作对变量的更改都是已经完成了的,且其后面对变量的操作一定是没有进行的。

其次:对于同一个线程中的语句,对volatile变量进行操作的语句和其他语句的相对顺序是不会改变的,但其他语句之间的相对顺序不做保证。

注意:volatile不能保证变量操作的原子性(比如i++),要保证原子性可以使用juc剥下的atomic相关类。

(2)实现原理:

观察字节码发现加入volatile关键字后会多出一个lock前缀指令,它相当于一个内存屏障,有三个功能:

在进行指令排序时不能跨过屏障

强制对缓存的修改操作立即写入内存

在对变量进行写操作时,会导致其他CPU中对应的缓存失效

4. 使用场景

就是在你能保证变量的原子性的时候就可以使用,具体来说是

(1)对变量的写操作不依赖于当前值

(2)该变量不在包含其他变量的不变式中。


参考:

https://www.cnblogs.com/dolphin0520/p/3920373.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容