静态工厂方法中单例的延迟加载技术
在用Java实际开发的时候我们经常要用到单例模式,一般来说,单例类的实例只能通过静态工厂方法来创建,我们也许会这样写:
public final class Singleton { private static Singleton singObj = new Singleton(); private Singleton(){ } public static Singleton getSingleInstance(){ return singObj; } }
这种方法可能会带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。因此我们希望在用到它的时候再加载它,这种设计思想就是延迟加载(Lazy-load Singleton)。
public final class LazySingleton { private static LazySingleton singObj = null; private LazySingleton(){ } public static LazySingleton getSingleInstance(){ if(null == singObj ) //A线程执行
singObj = new LazySingleton(); //B线程执行 return singObj; } }
上面的写法就保证了在对象使用之前是不会被初始化的。这种方式对于单线程来说是没什么问题,但是在多线程的环境下却是不安全的。假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。如何避免这个问题,答案很简单,在那个方法前面加一个Synchronized就OK了。
public final class ThreadSafeSingleton { private static ThreadSafeSingleton singObj = null; private ThreadSafeSingleton(){ } public static Synchronized ThreadSafeSingleton getSingleInstance(){ if(null == singObj ) singObj = new ThreadSafeSingleton(); return singObj; } }
但是众所周知,同步对性能是有影响的。由于对getInstance()做了同步处理,synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用,将会导致程序执行性能的下降。那么有没有什么方法,一方面是线程安全的,有可以有很高的并发度呢?我们的能想到的对策是降低同步的粒度,因此,人们想出了一个“聪明”的技巧:双重检查锁定(double-checked locking)。
public final class DoubleCheckedSingleton { private static DoubleCheckedSingletonsingObj = null; private DoubleCheckedSingleton(){ } public static DoubleCheckedSingleton getSingleInstance(){ if(null == singObj ) { //第一次检查 Synchronized(DoubleCheckedSingleton.class){ //加锁 if(null == singObj) //第二次检查 singObj = new DoubleCheckedSingleton(); } } return singObj; } }
如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。这看起来似乎两全其美,但是这是一个错误的优化!原因在于singObj = new DoubleCheckedSingleton()这一句。
以上有问题的这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
在一些编译器中会对代码进行优化而对语句进行重排序,这很常见,对于上面的三行伪代码,2和3可能会被重排,结果如下:
memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 //注意,此时对象还没有被初始化! ctorInstance(memory); //2:初始化对象
所以在多线程的环境下线程A可能执行到3,这时候对象在内存中已经有地址了,但是还未被完全初始化,这时候一旦线程B执行,在第一次检查的时候(null == singObj)返回false,线程B以为对象已经存在,接下来就可以使用了。实际上线程B可能使用了一个没有被完全初始化的对象,运行结果不得而知。
对于这种问题解决方法有两种,“基于volatile的双重检查锁定的解决方案”和“基于类初始化的解决方案”,在双重检查锁定与延迟初始化这篇文章中有详细介绍。我们来说下第二种解决方案。这种解决方案也就是Initialization On Demand Holder idiom,这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。
public class Singleton { private static class SingletonHolder { public final static Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }
内部类的初始化是延迟的,外部类初始化时不会初始化内部类,只有在使用的时候才会初始化内部类。而Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。也就是说,SingletonHolder在各个线程初始化的时候是同步执行的,且全权由JVM承包了。
Initialization On Demand Holder idiom的实现探讨中分析了单例的几种描述符(private static final / public static final / static
final
)之间的合理性,其推荐使用最后一种描述符方式更为合理。
参考资料:
http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
http://ifeve.com/initialization-on-demand-holder-idiom/
http://blog.sina.com.cn/s/blog_75247c770100yxpb.html