单例模式
单例模式是应用最广的设计模式之一,该模式保证了一个类实例存在,有利于我们协调系统整体的行为。
单例模式实现起来并不复杂,但不同的实现方式在不同的情况下可能会产生不同的效果,因此,需要根据实际的情况选择最优的实现方式。下面就来说一下常见的单例模式写法。
饿汉式
饿汉式
懒汉式(Lazy Load)
懒汉式是声明一个静态对象,并且在用户第一次调用getInstance() 时进行初始化。
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
在上述代码中,getInstance() 方法中添加了synchronized 关键字,保证了在多线程情况下单例对象的唯一性。但即使sInstance 已经被初始化,每次调用getInstance() 方法都会进行同步,这样就会消耗不必要的资源,这也是懒汉式存在的最大问题。
双重锁模式(Double Check Lock)
双重锁模式的优点是能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance() 不进行同步锁。
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static Singleton getInstance() {
//第一层判断避免不必要的同步
if (sInstance == null) {
//第二层判断即在null 的情况下创建实例
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
分析下sInstance = new Singleton() 这句代码,这里看似只是实例化了sInstance 对象,实际上并不是一个原子操作,而是被编译成多条汇编指令,大概做了三件事:
- 给Singleton 的实例分配内存;
- 调用Singleton 的构造函数,初始化成员字段;
- 将sInstance 对象指向分配的内存空间(此时sIntance 非空);
由于Java 编译器允许处理器乱序执行,以及JDK1.5 之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存回写顺序规定,导致无法保证上面这三点是顺序执行的,可能是1-2-3的顺序,也可能是1-3-2。如果刚好是1-3-2,那么在3执行完,2还未执行时,此时线程B 直接取走了 sInstance,使用时就会报错。这个问题发生的概率很小,但一旦发生,便难以跟踪。
基于上文的描述,我们意识到DCL可能存在失效的问题,为了解决这个问题,官方建议使用 volatile 关键字。我们只需将sInstance 的定义下面的这种写法,
private volatile static Singleton sInstance;
就可以保证sInstance 对象每次都是从主内存中读取,就可以使用DCL 写法来完成单例模式,即volatile+ DCL。
静态内部类单例模式
除了上面的单例定义模式,还有一种推荐的写法,即静态内部类单例模式。
private Singleton() {}
public Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
当第一次夹在Singleton 类时并不会初始化sInstance,只有在第一次调用Singleton 的 getInstance 方法时才会将sInstance 初始化。因此,第一次调用getInstance 方法会导致虚拟机加载SingletonHolder 类,这种方式不仅能够准确保线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。