相信面向对象程序员都对单例模式比较熟悉,而对于单例模式却有着各种各样的写法,今天我专门针对一种称为双重加锁的写法进行分析。我们先来看下这种写法。
/** * 单例双重加锁Demo * */public class DoubleCheckLock { private static DoubleCheckLock instance ; private DoubleCheckLock(){ } public static DoubleCheckLock getInstance(){ if(instance == null){ synchronized (DoubleCheckLock.class) { if(instance == null) instance = new DoubleCheckLock() ; } } return instance; }}
这种写法相信很多人都见过,但是你认为这种写法是正确的吗?或者更准确的来说,这种写法在并发的环境下是否还能表现出正确的行为呢。
之所以有这种所谓的双重加锁,一方面是因为延迟初始化可以提高性能,另一方面通过使用内置锁sychronized来防止并发,其原理是首先检查是否在没有同步的情况下进行了初始化,如果没有的话,在进行同步,然后再次检查是否对其(instance)进行了初始化,如果没有那么则初始化DoubleCheckLock。
这种写法表面看起来既提高了性能,又保证了线程安全。但实际上却并不是如此,我只从线程安全上来分析这种写法的对错。
在这,首先应该注意的是使用内置锁加锁的是DoubleCheckLock.class,并不是instance,也就是说没有在instance实现同步,那么在这种情况下,当有两个线程同时进行到synchronized代码块时,只有一个线程可以进入,然后初始化了instance,但是这仅仅只能保证的是两个线程在访问上的独占性,也就是说两个线程在此一定是一先一后进行访问,但是不能保证的是instance的内存可见性,原因很简单,因为同步的对象并不是instance,而是DoubleCheckLock.class(可以保证内存可见性)。不能保证内存可见性的后果就是当第一个线程初始化instance之后,第二个线程并不能马上看见instance被初始化,或者更准确的来说,第二个线程看到的可能只是被部分构造的instance。因此,这种造成的后果是第二个线程读取到了错误的instance的状态,有可能instance会被再次实例化。
那么如何解决这个问题呢,最简单的方式是对instance加上关键词volatile,volatile可以保证变量的内存可见性,同时volatile同步的消耗也非常小,这么做到话,可以保证线程安全。
上述解决问题的方式固然是可以,但是实质上我感觉很繁琐其代码阅读效果也不好,就单例而言,我推荐一下的写法。
public class Single { private Single(){} private static class SingleHolder{ public static Single instance = new Single(); } public static Single getInstance(){ return SingleHolder.instance; }}
这种写法相对而言比较简单,而且处理了两个问题:1.线程安全问题。2.延迟初始化(初始化在调用getInstance的时候才会去静态内部类中初始化instance)。而且相对而言,有着更加良好的代码可读性。
对于双重加锁的这种写法就先分析到这,等后面说到Happens-Before之后我会再来分下下双重加锁。