volatile的两大特性
1. 保证可见性
因为在线程之间的交互可以有两种方式进行,一种是共享内存,还有一种是通信(例如线程中的wait,notify方法)
但是如果有多个线程进行竞争时共享内存的变量可能会得不到同步的更新,就是两个线程都在自己的工作内存中对这个成员变量a=0进行更新,假设线程1把成员变量a=1,然后线程1先刷新变量到共享内存中a=1,此时线程2是不知道a值得变化(会继续读取工作内存的值),线程2输出a的值,会发现还是输出的0。
先简单的介绍一下jmm的知识
该图片引用
java代码会先被编译成class文件,在运行时被JVM编译成按顺序的CPU指令,然后CPU逐条执行,也就是说有段代码在运行时会先按照代码生成指令,两个线程在运行前都会先把共享内存中数据刷一遍到自己线程的工作内存中
加入volatile之后
假设双核CPU的电脑,我们知道CPU的速度比内存要快得多,为了弥补这个性能差异,CPU内核都会有自己的高速缓存区,当内核运行的线程执行一段代码时,首先将这段代码的指令集进行缓存行填充到高速缓存,如果非volatil变量当CPU执行修改了此变量之后,会将修改后的值回写到高速缓存,然后再刷新到内存中。如果在刷新会内存之前,由于是共享变量,那么CORE2中的线程执行的代码也用到了这个变量,这是变量的值依然是旧的。
那么假如了volatile之后
volatile关键字就会解决这个问题的,如何解决呢,首先被volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以lock为前缀的指令,当CPU发现这个指令时,立即做两件事:
1.将当前内核高速缓存行的数据立刻回写到内存;
2.使在其他内核里缓存了该内存地址的数据无效。
即在两个CPU的情况下,一个CPU刷新到共享内存中,发现是共享变量会
告诉其他CPU这个变量已经设置为无效,然后在其他CPU读取这个变量是,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。
但是上面可能会发生更新不到最新的数据的情况,因为在CPU1刷新这个共享变量前,CPU2执行的线程已经使用了之前刷新了共享内存到工作内存中已存在的数据,此时没有嗅探操作,导致了脏读。所以这个可见性是不可以保证线程安全的。
2.防止指令重排
- 在单线程中只要进行指令重排不会对结果造成影响的话,会对指令排序进行优化。
- 在正确同步的多线程中,在不影响结果的话,会根据happens-before的规则对部分指令进行重排
volatile的内存语序
如果使用了volatile修饰,那么会在volatile前后加上内存屏障。
在每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
参考:https://www.jianshu.com/p/157279e6efdb#comment-56517188
https://www.jianshu.com/p/aa7958f1c827