学 Java 并发,过不去 volatile 和 synchronized ,既然过不去,那就不过了,踏踏实实把它搞懂,踩在脚下.
这篇文章先搞定 volatile ,后面我再写另外一篇文章关于 synchronized 和锁的.
以下,正文开始:
在 Java 中, volatile 主要有两个功能:
接下来一一来看这两个功能,以及是怎么实现的
如果要谈 volatile 保证了变量的内存可见性,那就需要了解什么是内存可见性
所谓内存可见性是说,当一个线程对 volatile
修饰的变量进行 写操作 时, JMM 会立即将该线程对应的本地内存中的共享变量的值刷新到主内存中;当一个线程对 volatile
修饰的变量进行 读操作 时, JMM 会立即将该线程对应的本地内存设置为无效,然后从主内存中读取共享变量的值
在 JSR-133 之前的旧的 Java 内存模型中,是允许 volatile 变量与普通变量重排序的.
也就是说,虽然 volatile 变量能够保证内存可见性,但是可能程序执行的结果依旧不是你想要的.
如果直接使用锁的话,又会让整个程序变得比较重量级,基于以上考虑, JSR-133 专家组决定增强 volatile 的内存语义: 严格限制编译器和处理器对 volatile 变量与普通变量的排序
俗话说,说得容易,做起来就比较难.定义了严格限制 volatile 变量与普通变量的排序,那是拿什么来做保证的呢? JVM 在处理器层面是通过 内存屏障 来实现的.
强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效.
这里的缓存主要是指: CPU 缓存,如 L1 , L2 等
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序.
在这里编译器选择了一个比较保守的 JMM 内存屏障插入策略,保守的好处就是,可以保证在任何处理器平台,任何程序中都能得到正确的 volatile 内存语义.这个保守策略就是( Load 代表读操作, Store 代表写操作):
Store1 ; StoreStore ; Store2
语句,在 Store2 及后续写入操作执行前,要保证 Store1 的写入操作对其他处理器可见 Store1 ; StoreLoad ; Load2
语句,在 Load2 及后续所有读取操作之前,要保证 Store1 的写入对所有处理器可见 Load1 ; LoadLoad ; Load2
,在 Load2 及后续读取操作要读取的数据被访问前,要保证 Load1 要读取的数据读取完毕 Load1 ; LoadStore ; Store2
在 Store2 及后续写入操作被刷出前,要保证 Load1 读取的数据读取完毕 是不是有点儿懵?别急,我这里画了两张图,可以看着理解一下
写到这里了,就顺便介绍一下 volatile 和普通变量的重排序规则:
可以发现,针对 volatile 写操作来说,是比较严格的,但是如果第一个是普通变量的读,第二个是 volatile 的读,我可不可以重排序呢?可以
看到这里,应该就能知道, volatile 保证了内存可见性以及禁止重排序.
在保证内存可见性这一点上,可以说 volatile 和锁有着相同的意义,所以 volatile 可以作为一个”轻量级”锁来使用.
volatile 的本质其实就是告诉 JVM ,我修饰的这个变量在寄存器中的值是不确定的,如果需要的话,不能直接从本地内存中读取,需要从主存中去拿,所以 volatile 它改变的只是变量的可见性,但是不保证原子性.
基于此,就需要搞清楚,在什么情况下使用 volatile 比较好.
对于 volatile 关键字来说,当且仅当满足以下所有条件时,才可以使用:
我觉得上面的条件,就是为了保证操作是原子性操作,因为 volatile 不保证原子性,那为了安全,就要保证你本身的操作就是原子性操作,相当于直接从源头上就把不是原子性操作给排除掉.
这样的话,就比较容易搞清楚 volatile 这个变量使用在什么场景下了:
boolean flag
这种 参考:
深入理解 JVM
Java 理论与实践:正确使用 Volatile 变量
并发关键字 volatile(重排序和内存屏障)
JMM——volatile与内存屏障以上,感谢您的阅读哇