设计模式之单例模式

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

1、什么是单例

1.1 定义

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

1.2 实现思路

面向对象编程中,我们通过类的构造器生成对象,只要内存足够就可以构造出很多个实例,所以要限制某个类型只有唯一的一个实例对象,那就要从构造函数着手。

  1. 需要声明一个能返回对象的引用,定义一个获得该对象引用的方法(必须是静态方法,通常使用getInstance这个名称)
  2. 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用
  3. 最后将该类的构造函数定义为私有方法

2、懒汉式单例

按照以上的实现思路,实现出第一个单例类型:

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance; //引用
private Singleton (){} //私有构造器

public static Singleton getInstance() { //静态方法
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这种实现方式称为懒汉式,所谓懒汉,指的是只有在需要对象的时候才生成。

2.1 单例的线程安全

单例的线程安全是指在并发环境中,不同的线程拿到的单例对象也必须保证是同一个实例。

上文实现的单例类型是线程不安全的,如果有两个线程同时执行到 if (instance == null) 这行代码,判断都通过,然后各自执行 new 语句并各自返回一个实例,这时候就产生了多个对象。

解决方法有两种:

  • 给getInstance方法加互斥锁(不推荐使用)
1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

  • 双重检验锁(推荐使用
    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-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

还有值得注意的是,双重校验锁的实现方式中,静态成员变量singleton必须通过volatile来修饰,保证其初始化的原子性,否则可能被引用到一个未初始化完成的对象。

3、饿汉式单例

前面提到的懒汉模式,其实是一种lazy-loading思想的实践,这种实现有一个比较大的好处,就是只有真正用到的时候才创建,如果没被使用到,就一直不会被创建,这就避免了不必要的开销。

但是这种做法,其实也有一个小缺点,就是第一次使用的时候,需要进行初始化操作,可能会有比较高的耗时。如果是已知某一个对象一定会使用到的话,其实可以采用一种饿汉的实现方式。所谓饿汉,就是事先准备好,需要的时候直接给你就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

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

以上两段代码都是通过定义静态的成员变量(懒汉式只有声明没有定义)。饿汉模式中的静态变量是随着类加载时被完成实例化的。饿汉变种中的静态代码块也会随着类的加载一块执行。

因为类的初始化是由ClassLoader完成的,这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)

除了以上两种饿汉方式,还有一种实现方式也是借助了calss的初始化来实现的,那就是通过静态内部类来实现的单例(推荐使用):

1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

前面提到的饿汉模式,只要Singleton类被装载了,那么instance就会被实例化。

而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。

使用静态内部类,借助了classloader来实现了线程安全,这与饿汉模式有着异曲同工之妙,但是他有兼顾了懒汉模式的lazy-loading功能,相比较之下,有很大优势。

4、枚举式单例

Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

枚举单例:(墙裂推荐

1
2
3
4
5
public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

  • 最精简的
  • 线程安全的
  • 可解决反序列化破坏单例的问题

5、应用场景

  • Windows的Task Manager(任务管理器)就是很典型的单例模式
  • windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,用单例模式来维护,就可以大大降低这种损耗。
  • 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  • HttpApplication 也是单例的典型应用。