Contents
  1. 1. 单例模式介绍
  2. 2. Java中单例模式的写法
    1. 2.1. 懒汉方式
    2. 2.2. 饿汉方式
    3. 2.3. 静态内部类
    4. 2.4. 枚举
    5. 2.5. 双重校验锁
  3. 3. Conclusion

单例模式介绍

  单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):

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

  单例模式,也叫单子模式,是一种简单和常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
  单例模式主要有3个特点:

(1). 单例类确保自己只有一个实例。
(2). 单例类必须自己创建自己的实例。
(3). 单例类必须为其他对象提供唯一的实例。

  实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
  单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。

Java中单例模式的写法

  单例模式是Java中最常用的模式之一,它通过阻止外部实例化和修改,来控制所创建的对象的数量。这个概念可以被推广到仅有一个对象能更高效运行的系统,或者限制对象实例化为特定的数目的系统中(From:Java Design Pattern: Singleton)。例如:

  • 私有构造函数 - 其他类不能实例化一个新的对象。
  • 私有化引用 - 不能进行外部修改。
  • 公有静态方法是唯一可以获得对象的方式。

  单例模式的实现方式有五种方法:懒汉,恶汉,静态内部类,枚举和双重校验锁。

懒汉方式

  懒汉方式,指全局的单例实例在第一次被使用时构建。注意线程安全与否。

  线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
//Non Thread Safe
public class Singleton {
private static Singleton instance;
private Singleton (){}
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
//Thread Safe
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

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

饿汉方式

  饿汉方式,指全局的单例实例在类装载时构建。

1
2
3
4
5
6
7
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

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

静态内部类

  因为单例是静态的final变量,当类第一次加载到内存中的时候就初始化了,其thread-safe性由 JVM 来负责保证。

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的类时,只是新建了一个引用,并没有开辟一个的堆空间存放(对象所在的内存空间)。接着,当使用Singleton.getInstance()方法后,Java虚拟机(JVM)会加载SingletonHolder.class(JLS规定每个class对象只能被初始化一次),并实例化一个Singleton对象。这种方式的缺点是需要在Java的另外一个内存空间(Java PermGen 永久代内存,这块内存是虚拟机加载class文件存放的位置)占用一个大块的空间。

枚举

  枚举单例(Enum Singleton)是实现单例模式的一种新方式,枚举这个特性是在Java5才出现的。《Effective Java》一书中有介绍这个特性,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

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

  默认枚举实例的创建是线程安全的,但是在枚举中的其他任何方法由程序员自己负责。如果你正在使用实例方法,那么你需要确保线程安全(如果它影响到其他对象的状态的话)。传统单例存在的另外一个问题是一旦你实现了序列化接口,那么它们不再保持单例了,但是枚举单例,JVM对序列化有保证。枚举实现单例的好处:有序列化和线程安全的保证,代码简单。

双重校验锁

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
public static final Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronize (Singleton.class){
if( singleton == null ) { // double check
singleton = new Singleton();
}
}
return singleton;
}
}

  当两个线程执行完第一个 singleton == null 后等待锁, 其中一个线程获得锁并进入synchronize后,实例化了,然后退出释放锁,另外一个线程获得锁,进入又想实例化,会判断是否进行实例化了,如果存在,就不进行实例化了。

Conclusion

  单例模式有五种写法:懒汉、饿汉、静态内部类、枚举、双重检验锁。一般来说,推荐使用饿汉和静态内部类两种方式。
  Singleton 是最简单也最被滥用的模式。和其他设计模式一样,需要不断的实践经验的积累。对于 Beginner 来说了解一些基本的概念和模式即可,不要刻意追求设计模式。不断地看优秀的代码,努力写出高可阅读性代码才是王道。

References

Contents
  1. 1. 单例模式介绍
  2. 2. Java中单例模式的写法
    1. 2.1. 懒汉方式
    2. 2.2. 饿汉方式
    3. 2.3. 静态内部类
    4. 2.4. 枚举
    5. 2.5. 双重校验锁
  3. 3. Conclusion