先说单例模式:
经典的单例模式实现:
饿汉式:
public class Singleton {private static Singleton instance = new Singleton();public static Singleton getInstance(){return instance;} }
懒汉式:
public class Singleton {private static Singleton instance = null;synchronized public static Singleton getInstance(){if(instance ==null){instance = new Singleton();}return instance;} }
这两种都是可以安全运行在多线程下的。但是每一个都有点缺点,对于第一种如果这个单例的初始化需要很多内存和时间,我们希望用到时在初始化,没有用到就不初始化。对于第二种我们,其实只需要在第一次初始化时需要避免线程冲突,其他时候都可以直接返回的,而第二种的实现则变成了完全的串行(因为每一个操作都需要获得对象锁),非常大的降低了并发度。
我们尝试以下改进:
一个好的方式是DCL(double-checked locking),这种方式的实现如下:
public class Singleton {private static Singleton instance = null;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null)instance = new Singleton();}}return instance;} }
这样看起来是完美的解决方案,确实DCL是一个很好的解决思想,在C下其能很好的运行,但在Java下就会有问题了。这全怪Java的JMM(Java内存模型)。
在执行到instance = new Singleton()这里时,由于Java内存的“无序写入”,
可能的执行顺序是这样的:
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passinginstance.
即为instance分配内存,标记instance不为空,初始化instance。
这样会导致,一个线程刚标记完,还没有初始化赋值给instance,就释放了锁,然后另一个线程进入锁,判断不为空,释放锁,返回instance,这时显然是错的。这里出现这中错误的原因是instance = new instance();并没有真正的执行完,就释放了锁,我实在不能理解这样设计的原因,但很好的是在JDK1.5之后,已不存在这种问题了DCL这个可以很好的运行。但我们还是有必要继续讨论JDK1.5之前如何实现的。
可以在instance返回之前加一个步奏,确定其确实初始化了。
public class Singleton {private static Singleton instance = null;public static Singleton getInstance() {if (instance == null) {Singleton temp = instance;synchronized (Singleton.class) {if (temp == null)instance = new Singleton();}instance = temp;}return instance;} }
这样就很好的解决这个问题了。但是代码量和可阅读性已经陡然上升了,那么有没有更好的方法呢?是有的,利用类加载机制来实现,延迟初始化。
public class Singleton {private Singleton() {}private static class SingletonHolder {private static Singleton instance = new Singleton();}public static Singleton getInstance() {return SingletonHolder.instance;} }
这里补充一点类加载的知识,类加载分为一个步骤,加载->验证->准备->解析->初始化->使用->卸载。
Java中对何时初始化一个类有严格的说明,这里涉及到的一个规则是当类的静态域,或静态方法被引用的时候,必须对声明这个静态域或方法的类进行初始化。至于说明时候对类进行加载,这个有两种形式:饿加载(只要有其他类引用了它就加载),懒加载(初始化的时候才加载)。具体JVM对这点的实现不同。
所以当懒汉式能保证用到的时候才进行初始化,而饿汉试则是在加载时就初始化了。