单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要重复实例化该类的对象。保证一个类仅有一个实例,并提供一个访问它的全局访问点。
优点是在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。避免对资源的多重占用(比如写文件操作)。
单例模式的实现有多种方式例如饿汉模式、懒汉模式、双重检查模式、静态内部类方式以及枚举类。其中饿汉模式、懒汉模式又有多种变形,这里只介绍其基本的方式。
1.饿汉模式
public class Singleton {
// 饿汉模式
public static Singleton getInstance() {
return instance;
}
private static Singleton instance = new Singleton();
private Singleton() {
}
}
特点:在类加载的时候就完成了实例化,避免了线程同步问题,因此线程安全。由于是在类加载时就完成了实例化,没有达到懒加载的效果。如果一直没有使用过这个实例,就造成了内存的浪费!内存资源不是特别匮乏的情况下都可以使用这种模式。简单、实用。
2.懒汉模式
public class Singleton2 {
// 懒汉模式
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
private Singleton2() {
}
}
特点:实现了对象的延迟加载,在一定程度节省了资源。在单线程情况下可以使用,多线程情况下不安全。原因在于 instance = new Singleton2() 这个行代码的执行可能会发生重排序,具体原因在下面双检查模式中解释。
3.双重检查
public class Singleton3 {
// 双重检查
private static Singleton3 instance = null;
public static Singleton3 getInstance() {
if (instance == null) { // 1
synchronized (Singleton3.class) {
if (instance == null) { // 2
instance = new Singleton3(); // 3
}
}
}
return instance;
}
private Singleton3() {
}
}
分析:synchronized 会导致性能开销,因此尽量在必要的位置使用,将同步块的位置范围限制的尽可能小。双检查模式在代码 1 处第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,降低了synchronized 带来的性能开销。
但上面的代码是经典的错误方式,原因在代码 3 处, instance = new Singleton3(); 这行代码大致可以分成三个步骤:
- 1.分配对象所需要的内存
- 2.初始化对象
- 3.设置instance 指向刚才分配的内存地址
单线程情况下我们可以认为jvm是按照这样的顺序执行的,就算编译器优化代码被重排序,把步骤2和步骤3的顺序调换,也不影响最后结果,这种重排序是被允许的。
在多线程情况下,如果线程A执行到instance = new Singleton3()并且步骤2和步骤3发生了重排,instance 指向了刚才分配的内存地址,但还没完成初始化。这时线程B执行到代码1处,判断instance不为null,访问到了一个未被初始化的对象,得到不可预知的结果。
解决这个问题有两个思路:
1.禁止代码重排,使用volatile关键字修饰变量,禁止步骤2和步骤3的重排序。代码如下:
private static volatile Singleton3 instance = null;
了解更多关于volatile关键字可以参考:深入理解volatile关键字
2.允许步骤2和步骤3重排序,但不允许其他线程观察到这个重排序,即下面介绍的静态内部类方式。
4.静态内部类方式
public class Singleton4 {
private static class InstanceHolder {
private static Singleton4 instance = new Singleton4();
}
private Singleton4() {
}
public static Singleton4 getInstance() {
return InstanceHolder.instance;
}
}
分析:
Singleton4 getInstance() 被调用时会触发InstanceHolder 类的初始化,在初始化阶段执行类构造器clinit方法,clinit是class类构造器对静态变量,静态代码块进行初始化。
Java虚拟机必须保证一个类的clinit方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待,直到活动线程执行完毕clinit方法,其他线程虽然会被阻塞,但如果执行clinit方法的那条线程退出clinit方法后,其他线程唤醒后则不会再次进入clinit方法。同一个类加载器下,一个类型只会被初始化一次。
在多线程情况下,A线程在调用getInstance()方法就算发生了:
- 2.初始化对象
- 3.设置instance 指向刚才分配的内存地址
的代码重排序也不影响线程安全,因为B线程看不到这个变化,可以这样理解:对线程B来说,InstanceHolder静态内部类实例化Singleton4对象的过程是一个原子操作。
相对于volatile双检查方式,静态内部类方式的代码更简洁。
5. 枚举类方式
代码:
public enum Singleton5 {
INSTANCE;
public void methodA() {
System.out.println("枚举类方式!");
};
分析:
枚举类本身很符合单例模式的特点,它的构造方法是私有的。
枚举类由JVM在加载的时候,实例化枚举对象,在枚举类中定义了多少个就会实例化多少个,JVM为了保证每一个枚举类元素的唯一实例,是不会允许外部进行new的,所以会把构造函数设计成private,防止用户生成实例,破坏唯一性。
代码中INSTANCE就是一个枚举类的实例。
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,认为是实现单例模式的最佳方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,防止多次实例化。