设计模式相关文章,用于整理网络中对应的设计模式的一些解读。

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

实例应用

以下实例来源于微信公众号(Java知音)文章,设计模式是什么鬼(单例)

单例,顾名思义,整个系统其实就只有一个实例存在,不能再多,否则就不叫单例。那我们把整个宇宙看做是一个庞大的系统,这宇宙里有各种对象存在,人啊,动物啊,植物啊不胜枚举,这些都是实例,丰富多彩的世界是美好的。然而,持续几千年的战争给世界带来了巨大灾难,尤其是宗教战争最为残忍,各个信仰间存在极大的世界观价值观冲突。

神

单印度一个国家就有几百个神,人们各信各的,风俗各异,各邦文化冲突不断,语言不通,办事效率极低。

插图

为了让幸福美好洒满人间,那我们就定义一位神吧,独一无二的神。

插图

我们先写一个God类吧,类中空空如也,世界如此清净,虚无缥缈。

public class God {

}

首先我们得保证任何人都不能去创建神的实例,否则如:new God(),这样世界又要陷入战争的灾难,各种造神运动,或是某天又出来个什么神棍先知告诉信徒说他们肚子里有个轮子。那就不写构造方法吧?不行,因为有默认的无参构造器!那就把构造方法改成private吧,也就是神可以自己创造自己,但别人不能。

public class God {
private God(){}//构造方法私有化
}

God类里面封装一个God自己,对,一切都是神创造的,包括我们人类。有人开始质疑,那神是谁?神自己是谁造的?这是个哲学问题。神说“I am who I am.” 我是我所是,我就是我,自有永有,超越时空。很逆天吧? 好吧,谁也不能造上帝,神自己造自己。

public class God {
private static final God god = new God();//自有永有的神单例
private God(){}//构造方法私有化
}

以上private关键字保证了上帝的私有性,不可见性,不可访问性,我想没有活人见过上帝吧?static关键字保证上帝的静态性,他与类同在,不依赖于类的实例化就自有永有,他将在内存中永生,GC垃圾回收器也回收不了他。final关键字则保证这位神是和常量,衡量,他是终极上帝,不能再改。

正如同静态方法main(),不需要实例化类就能运行的入口,同样我们需要一个静态方法getInstance()来请神,方法体内我们就返回这个在唯一的真神,当然方法它必须是public公开的,不然谁都访问不了。

public class God {
private static final God god = new God();//自有永有的神单例
private God(){}//构造方法私有化
public static God getInstance(){//请神方法公开化
return god;
}
}

以上的神类雏形已经写好了,当然你还可以加其他的功能方法,比如说创世纪神造了光,造了世界、动物、人、亚当夏娃等等功能,我们这里就不在赘述了。那对于外部来说只要调用God.getInstance();就可以拿到神了,而且不管谁拿,拿几次,都是同一个神,这样就保证了整个系统中神的唯一性,不可伪造性,至于其他先知那也只是神的代理人,只能帮请神而已。

好了,其实我们已经学会了单例模式的“痴汉模式(Eager load)”,代码第一行一开始就造出了神(new God那一句),已经准备好了随时给你请神,这样就有了一个问题,如果没人请神那不是白造了?提前备货如果价格跌了不是很惨?反应在系统中的问题就是占用了内存空间。于是又有了“懒汉模式(Lazy load)”

public class God {
private static God god;//这里不进行实例化
private God(){}
public static God getInstance() {
if (god == null) {//如果无神才造神
god = new God();
}
return god;
}
}

这我们看到一开始就没有造神,只有某人第一次求神时才实例化,之后再求的就直接返回了。这样的好处是省了一段时间的内存(无求神期间),坏处是第一次请神的时候速度相较之前的痴汉模式会慢,因为要消耗CPU去造神。

其实这么写是在多线程模式下是有陷阱的,试想多人同时并发请神的话,依然会造成多神,好吧我们再来改良一下,把请神方法加上synchronized,声明为同步方法,某线程调用前必须获取同步锁,调用完后会释放锁给其他线程用,也就是请神的必须排队,大家一个一个按顺序来。

public class God {
private static God god;//这里不进行实例化
private God(){}
public static synchronized God getInstance() {//此处加入同步
if (god == null) {//如果无神才造神
god = new God();
}
return god;
}
}

然而,这样做是要付出代价的,还没进庙呢不管三七二十一请神的直接给加锁排队,结果队伍从北边的庙排到了南天门,人们都要来一个一个拜佛求神,这造成了巨大时间浪费,没有充分利用CPU资源并发优势(特别是多核情况)。好吧,那还是让人们抢好了,但依然得保证单例神的情况下。

插图

这里我们去掉方法上的同步关键字,换到方法体内部做同步,整个方法开放并发大家都可以同时入庙,当然起早贪黑的虔诚信徒们要抢头香是必须要入堂排队的。一旦头香诞生,那其他抢香的都白早起,白排队了。再之后的事情我们都可以预见了,头注香被抢后堂内排队再无必要来了,大家可以在堂外同时并发拜佛求神,这就极大的利用了CPU资源。简而言之:只有第一批抢头香的在排队,之后大家都不必排队了,代码如下。

public class God {
private volatile static God god;
private God(){}
public static God getInstance() {//庙是开放的不用排队进入
if (god == null) {//如果头柱香未产生,这批抢香人进入堂内排队。
synchronized(God.class){
if (god == null) {//只有头香造了神,其他抢香的白排队了
god = new God();
}
}
}
//此处头柱香产生后不必再排队
return god;
}
}

其实在这之上还发展出了各种各样的单例模式变种,我们这里只讲了最基础的两种,其实他们都各有优缺,我们要做到灵活运用,各取所需。对于我个人来讲倾向于痴汉模式,现在内存成本根本不算问题,况且迟早要被实例化占用内存,加锁解锁更是一种浪费,还有同步效率低等问题,如果上帝不是很占空间那就没必要去懒汉延迟加载,越复杂问题越多,风险越大。

单例模式的几种实现方式

懒汉式,线程不安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

实例:

public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉式,线程安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

实例:

public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

饿汉式

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

实例:

public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5

是否 Lazy 初始化:

是否多线程安全:

实现难度:较复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

实例:

public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

登记式/静态内部类

是否 Lazy 初始化:

是否多线程安全:

实现难度:一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

实例:

public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举

JDK 版本:JDK1.5

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

实例:

public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。