单例模式确保一个类只有一个实例,并提供一个全局访问点。
其类图如下所示。
本文主要从饿汉式,懒汉式,懒汉式改进,来讲解单例模式。
1、饿汉式单例
饿汉式单例类是在 Java 语言里实现得最为简便的单例类。在类被加载时,就会将自己实例化。
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
return uniqueInstance;
}
// other methods...
}
2、懒汉式(双重加锁)
通过 synchronized 关键字,同步不同线程对 getInstance()的访问。这就是所谓的懒汉模式。与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化。这种简单实现的问题在于,每次访问 getInstance()都需要同步操作,而事实上同步只在第一次访问时有意义。为了避免不必要的同步操作,在 JDK1.5 以后可以使用一种双重检查加锁的方法。
public class Singleton {
// volatile is very important for uniqueInstance consistency.
private volatile static Singleton uniqueInstance = null;
private Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
// first check no need to synchronize.
if (uniqueInstance == null) {
// second check need to synchronize, but only run limit times.
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
// Other methods...
}
volatile 确保 uniqueInstance 被初始化为单例后的改变对所有线程可见,多线程能够正确处理 uniqueInstance 变量。getInstance()中包含两次判空操作,第一次判空每次访问都会执行,而第二次判空只在初始访问存在大量并发的情况下出现。通过两次判空避免了不必要的线程同步。之所以限制必须在 JDK1.5 后使用是因为,之前的 Java 存储模型不能保证 volatile 语义的完全正确实现。
3、懒汉式改进
为了突破这种限制《Effective Java》中给出了一种精妙的解决方法,充分利用了 Java 虚拟机的特性。
public class Singleton {
// an inner class holder the uniqueInstance.
private static class SingletonHolder {
static final Singleton uniqueInstance = new Singleton();
}
private Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
return SingletonHolder.uniqueInstance;
}
// Other methods...
}
当 getInstance 方法第一次被调用时,在第一次调用 SingletonHolder.uniqueInstance,初始化 SingletonHolder 类,这种用法的优雅之处在于 getInstance 方法不需要同步,执行只有一个字段访问,因此惰性初始化对实际的访问没有任何额外的代价。VM 同步字段访问,只需要初始化 SingletonHolder 类,一旦被初始化,后续的字段访问不会涉及到任何判断和同步。
JDK 中使用单例模式的有 Runtime、NumberFormat 等类。
总结
- 单件模式确保程序中一个类最多只有一个实例,提供访问实例的全局点。
- 在 Java 中实现单件模式需要私有的构造器,一个静态方法和一个静态变量。
- 确定在性能和资源上的限制,使用适当的方案解决多线程问题
- 使用多个类加载器,可能会导致单件失效而产生多个实例