Java并发学习二:编译优化带来的有序性问题导致的并发Bug

Java并发学习系列文章:Java并发学习-博客专栏


今天在学习极客时间专栏:《Java并发编程实战》
第一讲01 | 可见性、原子性和有序性问题:并发编程Bug的源头中提到:

编译器及解释器的优化可能导致意想不到的 Bug

双重检验创建单例代码如下:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

“you look,当有多有线程调用getInstance()方法的时候,不管三七二十一,先让他们进来。如果instance 实例不为空,那最好了,直接return实例instance ,跟synchronized一点都扯不上关系,所以也不会影响到性能。这是双重检验中的第一次检验。”
“oh,I know,如果instance 是null的,就进入synchronized语句块,在synchronized语句块里面初始化对象。但为什么在synchronized语句中需要再次检查instance 实例是否为null?”
“这就是第二次检验了,当有多个线程通过第一次检验时,假设线程拿到锁进入synchronized语句块,对instance 实例进行初始化,释放instance .class锁之后,线程二持有这个锁进入synchronized语句块,此时又对instance 对象就行初始化。所以在这里进行第二次检验防止这种意外发生。”

但上面的代码在多线程下可能会出现Bug,是因为编译器对new做了优化:

默认的new操作
1.分配一块内存 M;
2.在内存 M 上初始化 Singleton 对象;
3.然后 M 的地址赋值给 instance 变量。

但是,当我们编译的时候,编译器在生成汇编代码的时候会对流程顺序进行优化。优化的结果是有可能按照1-2-3顺序执行,也可能按照1-3-2顺序执行。
我们知道,执行完3的时候就instance 对象就已经不为空了,如果是按照1-3-2的顺序执行,恰巧在执行到3的时候(还没执行2),突然跑来了一个线程,进来getInstance()方法之后判断instance 不为空就返回了instance 实例。
此时instance 实例虽不为空,但它还没执行构造方法进行初始化。又恰巧构造方法里面需要对某些参数进行初始化。后来闯进来的线程糊里糊涂对那些需要初始化的参数进行操作就有可能报错奔溃了。”


解决方法是加上volatile关键字,volatile关键字可以禁止指令重排序


这个例子让我想起了很久之前在码农翻身上看到的一篇文章:《Java帝国之单例设计模式》

你可能感兴趣的:(Java并发,java)