定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
使用场景
确保某一个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如要访问IO和数据库等资源,这时候就可以考虑使用单列模式。
UML 类图
classDiagram
Singleton <.. Client
class Singleton{
+getInstanc()Singleton
-Singleton()
}
需要注意的几个关键点:
- 构造函数不对外开发,通常为Private;
- 通过静态方法或者枚举返回单列类对象;
- 确保单列类有且只有一个对象,尤其是在多线程的环境下;
- 确保单列类对象在反序列化时不会重新构建。
单列的实现方式
- 饿汉模式
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这种方式在类加载的时候就自行实例化,避免了多线程同步的问题。
- 懒汉模式
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
这种方式的优点是只有在第一次使用的时候才会被实例化,可以看到 getInstance() 添加了 synchronized 修饰,它是一个同步方法,这是为了在多线程的情况下保证单列对象的唯一性,如果只在单线程使用可以不加。也是因为添加了 synchronized 导致每次调用getInstance()都需要进行同步,导致不必要的同步开销,所以不建议使用这种写法。
- 双重检查模式 DCL(Double Check Lock)
public class Singleton{
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这种方式相比上面的避免了没必要的同步,同时也能做到线程安全。可以看到有两次判空,第一次试为了避免不必要的同步,第二次是为了确保单列对象的唯一性。这里还是用了 volatile 关键字,如果不用就有可能出现DCL 失效的问题。
是什么导致DCL 失效的?我们接着往下看。
假设A线程执行到 instance = new Singleton() 这行代码,这行代码最终会被编译成多条指令,大致做以下3键事情:
- 给Singleton 实例分配内存
- 调用 Singleton() 构造函数,实例化对象
- 将 instance 对象指向分配的内存空间 (这时候 instance 就不为 null了)
但是由于Java 编译器允许处理器乱序执行,这时候就有可能出现执行顺序为1-3-2,当A线程执行完3还未执行2的时候,B线程取走了instance ,在使用时就会出错,这就是DCL 失效问题。而使用volatile 关键字修饰可以禁止进行指令重排序,所有可以有效的避免这个问题。当然使用volatile 也会对性能有一点影响,但考虑程序的正确性,这点牺牲是值得的。
- 静态内部类模式
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
}
因为静态内部类不会因为外部类的加载而加载,静态内部类加载不需要依附于外部类,但在加载静态内部类时一定会加载外部类。
因此使用这种方式可以确保线程安全,也能保证单列对象的唯一性,同时也延迟了单列的实例化,也不会有性能影响。所以这是最推荐的使用方式。
- 枚举模式
public enmu Singleton{
INSTANCE;
public void doSomething(){
}
}
枚举默认是线程安全的而且反序列化也不会导致重新创建对象,保证单列对象的唯一性。如果之前4种模式要杜绝反序列化时重新生成对象,那么必须加入 readResolve() 函数。
public class Singleton implements Serializable{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
private Object readResolve() throws ObjectStreamException{
return instance;
}
}
小结
以上介绍了单列的5种实现方式,并非全部。就个人而言,不考虑懒加载的情况下使用 饿汉模式 即可,否则建议使用 静态内部类模式,如果需要考虑反序列化的情况可以考虑 枚举模式 ,枚举模式因为可读性不太好,所以一般用的比较少。