浅谈设计模式之单例模式

最近学习了Java的几种常规的设计模式,内容较多,思维方式多种多样,故将所学整理一下,写成博客,分享并加深自己的理解与记忆。

首先我们看一下什么是设计模式?

设计模式(Design pattern)

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

设计模式分为三大类:

创建型模式

共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式

共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式

共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

其实还有两类:并发型模式和线程池模式。

更多关于设计模式的设计原则与内容,会在陆续出完设计模式系列最后汇总一下。

接下来,进入本篇的主题——单例模式(Singleton)

单例模式(Singleton)

在我们日常的工作中经常需要在应用程序中保持一个唯一的实例,如:IO处理,数据库操作,配置文件,工具类,线程池,缓存,日志对象等,由于这些对象都要占用重要的系统资源,所以我们必须限制这些实例的创建或始终使用一个公用的实例,如果创造出来多个实例,就会导致许多问题,比如占用过多资源,不一致的结果等。这就是我们今天要介绍的——单例模式(Singleton)。

UML

java中单例模式是一种常见的设计模式,单例模式分两种:懒汉式单例、饿汉式单例
单例模式有以下特点:

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

饿汉式单例(Eager initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
//1.将构造方法私有化,不允许外部直接创建对象
private Singleton(){
}
//2.创建类的唯一实例,使用private static修饰
private static Singleton instance=new Singleton();
//3.提供一个用于获取实例的方法,使用public static修饰
public static Singleton getInstance(){
return instance;
}
}

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

下面提供饿汉式的变种:

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}

表面上看起来差别挺大,其实和之前差不多,都是在类初始化即实例化instance。

总结一下饿汉式的优点:

  • The static initializer is run when the class is initialized, after class loading but before the class is used by any thread.

  • There is no need to synchronize the getInstance() method, meaning all threads will see the same instance and no (expensive) locking is required.

  • The final keyword means that the instance cannot be redefined, ensuring that one (and only one) instance ever exists.

懒汉式单例(Lazy initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* 懒汉模式
* 区别:饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全
* 懒汉模式的特点是加载类时比较快,但运行时获取对象的速度比较慢,线程不安全
*/
public class Singleton {
//1.将构造方式私有化,不允许外边直接创建对象
private Singleton(){
}
//2.声明类的唯一实例,使用private static修饰
private static Singleton instance;
//3.提供一个用于获取实例的方法,使用public static修饰
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}

这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
其实懒汉式也是可以写成线程安全的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static SingletongetInstance() {
if (instance == null ) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

写博客的时候,发现了WIKI提供了一个更加牛的懒汉式升级版——双重校验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

双重校验锁(Double-checked locking),俗称双重检查锁定。

其他单例的实现

登记式单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
static{
Singleton3 single = new Singleton3();
map.put(single.getClass().getName(), single);
}
//保护的默认构造子
protected Singleton3(){}
//静态工厂方法,返还此类惟一的实例
public static Singleton3 getInstance(String name) {
if(name == null) {
name = Singleton3.class.getName();
System.out.println("name == null"+"--->name="+name);
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton3) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一个示意性的商业方法
public String about() {
return "Hello, I am RegSingleton.";
}
public static void main(String[] args) {
Singleton3 single3 = Singleton3.getInstance(null);
System.out.println(single3.about());
}
}

登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。

枚举(The enum way)

1
2
3
4
5
6
public enum Singleton {
INSTANCE;
public void execute (String arg) {
// Perform operation here
}
}

居然用枚举!!看上去好牛逼,通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。这种方式是《Effective Java》作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊。

默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。

这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。

静态内部类(Static block initialization)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
private static final Singleton instance;
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("Darn, an error occurred!", e);
}
}
public static Singleton getInstance() {
return instance;
}
private Singleton() {
// ...
}
}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理。

饿汉式和懒汉式的区别

这两种乍看上去非常相似,其实是有区别的,主要两点

1、线程安全:

饿汉式是线程安全的,可以直接用于多线程而不会出现问题,懒汉式就不行,它是线程不安全的,如果用于多线程可能会被实例化多次,失去单例的作用。

如果要把懒汉式用于多线程,有两种方式保证安全性,一种是在getInstance方法上加同步,另一种是在使用该单例方法前后加双锁。

2、资源加载:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,会占据一定的内存,相应的在调用时速度也会更快,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次掉用时要初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

应用

以下是一个单例类使用的例子,以懒汉式为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestSingleton {
String name = null;
private TestSingleton() {
}
private static TestSingleton ts = null;
public static TestSingleton getInstance() {
if (ts == null) {
ts = new TestSingleton();
}
return ts;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void printInfo() {
System.out.println("the name is " + name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TMain {
public static void main(String[] args){
TestStream ts1 = TestSingleton.getInstance();
ts1.setName("jason");
TestStream ts2 = TestSingleton.getInstance();
ts2.setName("0539");
ts1.printInfo();
ts2.printInfo();
if(ts1 == ts2){
System.out.println("创建的是同一个实例");
}else{
System.out.println("创建的不是同一个实例");
}
}
}

运行结果:

result

结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

参考资料: