指令重排

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。
举一个伪代码:

  1. int a=10 ;//1
  2. int b=20 ;//2
  3. int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是: 1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3
可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。
可能这里还看不出有什么问题,那看下一段伪代码:

  1. private static Map<String,String> value ;
  2. private static volatile boolean flag = fasle ;
  3. //以下方法发生在线程 A 中 初始化 Map
  4. public void initMap(){
  5. //耗时操作
  6. value = getMapValue() ;//1
  7. flag = true ;//2
  8. }
  9. //发生在线程 B中 等到 Map 初始化成功进行其他操作
  10. public void doSomeThing(){
  11. while(!flag){
  12. sleep() ;
  13. }
  14. //dosomething
  15. doSomeThing(value);
  16. }

这里就能看出问题了,当 flag 没有被 volatile 修饰时, JVM 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。
所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

  1. public class Singleton {
  2. private static volatile Singleton singleton;
  3. private Singleton() {
  4. }
  5. public static Singleton getInstance() {
  6. if (singleton == null) {
  7. synchronized (Singleton.class) {
  8. if (singleton == null) {
  9. //防止指令重排
  10. singleton = new Singleton();
  11. }
  12. }
  13. }
  14. return singleton;
  15. }
  16. }

这里的 volatile 关键字主要是为了防止指令重排。
如果不用 , singleton=newSingleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • singleton 对象指向分配的内存地址。(3)

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。