【软件工程】设计模式——单例模式

单例模式

概念

单例模式,故名思义是用来保证一个对象只能创建一个实例。单例模式是最简单也是最常用的一种设计模式,但因为过度使用或者在不恰当的时候使用,有的时候会被视为一种反模式。

如何实现?

只由单个类组成。为确保单例实例的唯一性,所有的单例构造器都要被声明为私有的(private),再通过声明静态(static)方法实现全局访问获得该单例实例。

注意一下要点:

  1. 单例构造器都要被声明为私有的(private)
  2. 通过申明静态方法全局访问获得该单例示例
  3. 自行向整个系统提供该实例
    单例模式实现示意图
    图:单例模式实现示意图

代码示例

以下为一段使用Java实现的单例模式,注意,要点在代码注释中再次写明:
Singleton.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
private static Singleton instance;

// 单例构造器都要被声明为私有的(private)
private Singleton() {
System.out.println("Singleton is Instantiated.");
}

public static Singleton getInstance() {
// 注意,检查实例是否为空,如果不为空则表明对象在之前被创建过
if (instance == null)
// 自行向整个系统提供该实例
instance = new Singleton();

return instance;
}

public void doSomething() {
System.out.println("Something is Done.");
}
}

MyMainClass.java

1
2
3
4
5
6
7
8
public class MyMainClass {

public static void main(String[] s) {
// 通过申明静态方法全局访问获得该单例示例
Singleton singleton = Singleton.getInstance();
}

}

同步锁单例模式

在多线程应用中使用这种模式,如果实例为空,可能存在两个线程同时调用getInstance方法的情况。如果发生这种情况,第一个线程会首先使用新构造器实例化单例对象,同时第二个线程也会检查单例实例是否为空,由于第一个线程还没完成单例对象的实例化操作,所以第二个线程会发现这个实例是空的,也会开始实例化单例对象。

解决方法:创建一个代码块来检查实例是否空线程安全。在Java中,可以有以下两种方式:

  1. 向getInstance方法的声明中添加synchronized关键字
    1
    public static Singleton synchronized getInstance()
  2. 用synchronized代码块包装if(instance==null)条件
    1
    2
    3
    4
    synchronized (Singleton.class) {
    if (instance == null)
    instance = new Singleton();
    }

拥有双重校验锁机制的同步锁单例模式

同步锁单例模式虽然能够保证线程安全,但同时带来了延迟。用来检查实例是否被创建的代码是线程同步的,也就是说此代码块在同一时刻只能被一个线程执行,但是同步锁(locking)只有在实例没被创建的情况下才起作用。如果单例实例已经被创建了,那么任何线程都能用非同步的方式获取当前的实例。

因此我们设置只有在单例对象未实例化的情况下,才能在synchronized代码块前添加附加条件移动线程安全锁。且instance==null条件需要被检查了两次,保证在synchronized代码块中也要进行一次检查。

1
2
3
4
5
6
7
8
synchronized (Singleton.class) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
}

无锁的线程安全模式

Java中单例模式的最佳实现形式中,类只会加载一次,通过在声明时直接实例化静态成员的方式来保证一个类只有一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LockFreeSingleton {
// 在声明时直接实例化静态成员
private static final LockFreeSingleton instance = new LockFreeSingleton();

private LockFreeSingleton() {
System.out.println("Singleton is Instantiated.");
}

public static synchronized LockFreeSingleton getInstance() {
return instance;
}

public void doSomething() {
System.out.println("Something is Done.");
}
}

两种单例模式

按照实例对象被创建的时机,可以将单例模式分为两类:

  1. 提前加载单例模式:在应用开始时创建单例实例
  2. 延迟加载单例模式:在getInstance方法首次被调用时才调用单例构造器(例如上文提到的无锁的线程安全模式)

优缺点

优点:

  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性
  3. 提供了对唯一实例的受控访问
  4. 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能
  5. 允许可变数目的实例
  6. 避免对共享资源的多重占用

缺点:
7. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态
8. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难
9. 单例类的职责过重,在一定程度上违背了“单一职责原则”
10. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失

适用场景

  1. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。e.g. 日志文件和应用配置。
  2. 控制资源的情况下,方便资源之间的互相通信。e.g. 线程池。

Reference

卡马尔米特·辛格(Kamalmeet Singh). Java设计模式及实践 (Java核心技术系列) (Chinese Edition) (Kindle Location 507). Kindle Edition.