Singleton Pattern -- 单例模式
单例模式是用来创建一个只能又一个实例的对象。
单例模式类图如下。
单例模式有两大好处:
(1)对于频繁使用的对象,可以省略创建对象所话费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
(2)由于new操作的次数减少,因而对系统内存的使用频率页会降低,这将减轻GC压力,缩短GC停顿时间。
因此对于系统的关键组件和被频繁使用的对象,使用单例模式便可以有效地改善系统的性能。
这种单例的实现方式非常简单,而且十分可靠,它唯一的不足仅是无法对instance实例做延迟加载。假如单例的创建过程很慢,而由于instance成员变量
是static定义的,因此在JVM加载单例类时,单例对象就会被建立,如果此时,这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会
初始化这个单例变量,而不管是否被用到。比如单例类作为String工厂,用于创建一些字符串(该类即用于创建单例Singleton,又用于创建String对象):
单例模式通用代码(饿汉模式):
当使用Singleton.createString()执行任务时,程序输出:
可以看到,虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员所不愿意看到的。为了解决这个问题,并以此提高系统在相关函数调用是的
反应速度,就需要引入延时加载机制。
延时单例模式(懒汉模式)
首先,对于静态成员变量instance初始化赋值null,确保系统启动时没有额外的负载;其次,在getInstance()工厂方法中,判断当前单例是否已经存在,
若存在则返回,不存在则再建立单例。这里尤其还要注意,getInstance()方法必须是同步的,否则再多线程的环境下,当一个线程A执行到instance = new
Singleton(),但还没有获得对象(对象初始化时需要时间的),在此同时第二个线程B也在执行,执行到(instance == null)判断,那么线程B判断的结果
也为真。于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现了两个对象!
如下:
开启了5个线程同时完成以上代码的运行,使用第1种类型的单例耗时0ms,而使用LazySingleton,解决了线程安全问题却相对耗时约390ms。性能至
少相差2个数量级。
为了使用延时加载引入的同步关键字反而降低了系统性能,有点得不偿失,我们继续改进:
在这个实现中,单例模式是用来内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会被初始化,故可以确保
StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。
同时,由于实例的建立是在类加载时完成,故天生对多线程友好,getInstance()方法也不需要使用同步关键字。因此,这种实现方式同时
兼备以上两种实现的优点。
通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。
(现在已经标准的实现了单例模式方式有(单检查锁,双检查锁,枚举),但是如果我们通过反射机制调用,是否会产生多个实例,即破坏了单例模式。)
反射模式,调用方法是不会破坏单例模式,因为反射方法同样受锁&逻辑的保护。
调用构造函数会破坏单例模式,因为构造方法只是private 修饰,防止外部类访问,但是反射方法访问,不受限制。
还有一种破坏次单例模式的方法:对象克隆。这个现象是我在看原型模式时发现。
如果类实现了Clonable接口,那么在得到单例模式对象可以通过clone方法生成单例对象。
所以写单例模式时不要继承或间接继承Clonable接口。