转载自:https://javadoop.com/post/java-memory-model#toc10
volatile 的作用,记住两点:内存可见性和禁止指令重排序。
volatile 的内存可见性
我们还是用 JMM 的主内存和本地内存抽象来描述,这样比较准确。还有,并不是只有 Java 语言才有 volatile 关键字,所以后面的描述一定要建立在 Java 跨平台以后抽象出了内存模型的这个大环境下。
还记得 synchronized 的语义吗?进入 synchronized 时,使得本地缓存失效,synchronized 块中对共享变量的读取必须从主内存读取;退出 synchronized 时,会将进入 synchronized 块之前和 synchronized 块中的写操作刷入到主存中。
volatile 有类似的语义,读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile 属性会立即刷入到主内存。所以,volatile 读和 monitorenter 有相同的语义,volatile 写和 monitorexit 有相同的语义。
volatile 的禁止重排序
大家看下面的双重检查的单例模式,加个 volatile 能解决问题。其实就是利用了 volatile 的禁止重排序功能。
public static Singleton getInstance() { if (instance == null) { // Singleton temp; synchronized (Singleton.class) { // temp = instance; if (temp == null) { // synchronized (Singleton.class) { // 内嵌一个 synchronized 块 temp = new Singleton(); } instance = temp; // } } } return instance; }
synchronized 在退出的时候,能保证 synchronized 块中对于共享变量的写入一定会刷入到主内存中。也就是说,上述代码中,内嵌的 synchronized 结束的时候,temp 一定是完整构造出来的,然后再赋给 instance 的值一定是好的。
可是,synchronized 保证了释放监视器锁之前的代码一定会在释放锁之前被执行(如 temp 的初始化一定会在释放锁之前执行完 ),但是没有任何规则规定了,释放锁之后的代码不可以在释放锁之前先执行。
也就是说,代码中释放锁之后的行为 instance = temp
完全可以被提前到前面的 synchronized 代码块中执行,那么重排序问题就又出现了。
volatile 的禁止重排序并不局限于两个 volatile 的属性操作不能重排序,而且是 volatile 属性操作和它周围的普通属性的操作也不能重排序。
之前 instance = new Singleton() 中,如果 instance 是 volatile 的,那么对于 instance 的赋值操作(赋一个引用给 instance 变量)就不会和构造函数中的属性赋值发生重排序,能保证构造方法结束后,才将此对象引用赋值给 instance。
根据 volatile 的内存可见性和禁止重排序,那么我们不难得出一个推论:线程 a 如果写入一个 volatile 变量,此时线程 b 再读取这个变量,那么此时对于线程 a 可见的所有属性对于线程 b 都是可见的。
volatile 小结
1.volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值。在并发包的源码中,它使用得非常多。
2.volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
3.volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序。
4.volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
5.volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 happens-before 所有其他线程后续对 v 的读操作。
6.volatile 可以使得 long 和 double 的赋值是原子的。