关于DCL双重锁失效及解决方案
Double Check Lock (DCL)实现单例
DCL 方式实现单例的优点是既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance方法不进行同步锁。代码如下:
本程序的 亮点自然在getInstance方法上面,可以看到该方法对instance进行了两次判空:第一层主要是为了避免不必要的同步,第二层判断则是为了在null情况下才创建实例。这是什么意思呢?是不是有点摸不着头脑,下面就一起来分析一下。
假设线程A执行到singleton03 = new Singleton03()语句,看起来只有一行代码,但实际上它并不是原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事:
1)给singleton03的实例分配内存。
2)调用Singleton03()构造函数,初始化成员字段。
3)将singleton03对象指向分配的内存空间(此时singleton03就不是null了)。
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM中的Cache、寄存器到主内存回写顺序的规定,上面的2和3的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候singleton03因为已经在线程A内执行过了3,singleton03已经是非空了,所以,线程B直接取走singleton03,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的错误可能会隐藏很久。
在JDK1.5之后,SUN官方已经注意到这种问题,调整了JVM,具体化了volatile关键字,因此,如果JDK1.5或之后的版本,只需要将singleton03的定义改成private volatile static Singleton03 singleton = null就可以保证singleton03对象每次都是从主内存中读取,就可以使用DCL的写法来完成单例模式。当然,volatile或多或少也会影响到性能,但考虑到程序的正确性,这点牺牲也是值得的。
DCL优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载稍慢,也由于JMM的原因导致偶尔会失败。在高并发环境下也有一定的缺陷,虽然发送概率很小。DCL模式是使用最多的单例实现方式,它能够在需要时才实例化对象,并且能在绝大多数场景下保证对象的唯一性,除非你的代码在并发场景比较复杂或低于JDK1.6版本下使用,否则,这种方式一般能够满足要求。