单例模式
一个类只有一个实列,而且自行实例化并向整个系统提供这个实例。
实现单例模式有以下几个关键点:
1.构造函数不对外开放,一般为Private;
2.通过一个惊呆方法或者枚举返回单例对象;
3.确保单例类的对象有且只有一个,尤其是在多线程环境下;
4.确保单例类对象在反序列化时不会重新构建对象。
1. 饿汉单例模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
2.懒汉单例模式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
优点:单例只有在使用时才会被实列化,在一定程度上节约了资源。
缺点:第一次加载时需要进行实列化,反应稍慢,最大问题时每次调用
getInstance()都进行了同步,造成不必要的同步开销。这种模式一般不建议使用。
3.Double Check Lock(DCL) 实现单例
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
优点:能够在需要时才初始化单例,有能够包正线程安全,且店力对象初始化后调用 getInstance() 不进行同步锁。
缺点: 第一次加载时反应稍慢,由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率比较小。
为什么getInstance()方法中对 instance 进行了两次判空,而且还加了volatile关键字 ?
假设线程A执行到 instance = new Singleton() 语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,大致做了3件事情:
1.给Singleton 的实例分配内存;
2.调用Singleton()的构造函数,初始化成员字段;
3.将instance 对象指向分配的内存空间(此时instance就不是nulll 了)。
但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前 JVM( Java Memory Model,即 Java 内存模型)中 Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。
在JDK1.5之后,SUN 官方已经注意到了这个问题,调整了JVM,具体化 volatile 关键字,保证 instance 对象每次都是从主内存中读取。
4.静态内部类单例模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
优点:第一次调用getInstance() 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用这种单例模式实现
5.枚举单例模式
public enum SingletonEnum {
INSTANC;
}
枚举在Java中与普通类时一样的,不仅能够有字段,还能够有自己的方法。最终要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。
为什么这么说?在上述的几种单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化,
通过序列化可以将一个单例的实列对象写到磁盘,然后再读回来,从而有效的获得一个实例。即使构造方法是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下方法:
private Object readResolve()throws ObjectStreamException {
return INSTANC;
}
6.容器实现单例模式
public class SingletonManager {
private static Map<String, Object> objectMap = new HashMap<>();
private SingletonManager() {}
public static void registerService(String key, Object instance) {
if (!objectMap.containsKey(key)) {
objectMap.put(key, instance);
}
}
public static Object getService(String key) {
return objectMap.get(key);
}
}
在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据Key 获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
总结:不管以哪种方式实现单例模式,他们的核心都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程必须保证线程安全、防止序列化导致重新生成实例对象等问题。