Singleton模式可以是很简单的,它的全部只需要一个类就可以完成。但是如果在“对象创建的次数以及何时被创建”这两点上较真起来,Singleton模式可以相当的复杂。
结构是简单的,只是我们还有一些小小的要求如下:
1.最基本要求:每次从getInstance()都能返回一个且唯一的一个对象。
2.稍微高一点的要求:希望这个方法能适应多线程并发访问。
3.再提高一点的要求:方法性能尽可能高。
4.最后一点要求是:希望实现懒加载(Lazy Load),在需要的时候才被构造。
目的:希望对象只创建一个实例,并且提供一个全局的访问点。
我们第一次写的单例模式是下面这个样子的:
public class SingletonKerriganA { /** * 单例对象实例 */ private static SingletonKerriganA instance = null; public static SingletonKerriganA getInstance() { if (instance == null) { //line A instance = new SingletonKerriganA(); //line B } return instance; } }
紧接着,我们做单例模式的第二次尝试:
public class SingletonKerriganB { /** * 单例对象实例 */ private static SingletonKerriganB instance = null; public synchronized static SingletonKerriganB getInstance() { if (instance == null) { instance = new SingletonKerriganB(); } return instance; } }
那继续把代码改成下面的样子:
public class SingletonKerriganC { /** * 单例对象实例 */ private static SingletonKerriganC instance = null; public static SingletonKerriganC getInstance() { synchronized (SingletonKerriganC.class) { if (instance == null) { instance = new SingletonKerriganC(); } } return instance; } }
public class SingletonKerriganD { /** * 单例对象实例 */ private static SingletonKerriganD instance = null; public static SingletonKerriganD getInstance() { if (instance == null) { synchronized (SingletonKerriganD.class) { if (instance == null) { instance = new SingletonKerriganD(); } } } return instance; } }
我们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:
1.给Kerrigan的实例分配内存。
2.初始化Kerrigan的构造器
3.将instance对象指向分配的内存空间(注意到这步instance就非null了)
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。
DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。
一个类在一个ClassLoader中只会被初始化一次,这点是JVM本身保证的,那就把初始化实例的事情扔给JVM好了,代码被改成这样:
public class SingletonKerriganE { /** * 单例对象实例 */ private static SingletonKerriganE instance = new SingletonKerriganE(); public static SingletonKerriganE getInstance() { return instance; } }
public class SingletonKerriganF { private static class SingletonHolder { /** * 单例对象实例 */ static final SingletonKerriganF INSTANCE = new SingletonKerriganF(); } public static SingletonKerriganF getInstance() { return SingletonHolder.INSTANCE; } }
通过一次次的的尝试,去了解单例模式各种实现方案的优缺点。对双锁锁定检测进行了简单的讨论,相信大家能从各种尝试的演化过程中,明白为何单例模式是最简单而又最复杂的一种构造模式。
最后我们比较一下懒汉模式和饿汉模式的优缺点:
懒汉模式,它的特点是运行时获得对象的速度比较慢,但加载类的时候比较快。它在整个应用的生命周期只有一部分时间在占用资源。
饿汉模式,它的特点是加载类的时候比较慢,但运行时获得对象的速度比较快。它从加载到应用结束会一直占用资源。
这两种模式对于初始化较快,占用资源少的轻量级对象来说,没有多大的性能差异,选择懒汉式还是饿汉式都没有问题。但是对于初始化慢,占用资源多的重量级对象来说,就会有比较明显的差别了。所以,对重量级对象应用饿汉模式,类加载时速度慢,但运行时速度快;懒汉模式则与之相反,类加载时速度快,但运行时第一次获得对象的速度慢。
从用户体验的角度来说,我们应该首选饿汉模式。我们愿意等待某个程序花较长的时间初始化,却不喜欢在程序运行时等待太久,给人一种反应迟钝的感觉,所以对于有重量级对象参与的单例模式,我们推荐使用饿汉模式。
原文地址:http://blog.csdn.net/victor_cindy1/article/details/44872789