单例模式的定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的使用场景
确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源。例如创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源。
(1)Client——高层客户端;
(2)Singleton——单例类;
实现单例模式主要有如下几个关键点:
(1)构造函数不对外开放,一般为 Private;
(2)通过一个静态方法或者枚举返回单例类对象;
(3)确保单例类的对象有且只有一个,尤其是在多线程环境下;
(4)确保单例类对象在反序列化时不会重新构建对象。
单例模式的简单实例
(1)懒汉模式:
public class Singleton {
private static Singleton sInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
getInstance() 方法中添加了 synchronized 关键字,保证方法在多线程情况下单例对象的唯一性。
懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度下节约了资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用 getInstance 都进行同步,造成不必要的同步开销。一般不建议使用。
(2)Double Check Lock (DCL)实现单例
public class Singleton {
private static Singleton sInstance = null;
private Singleton() {}
public void doSomething() {
System.out.println("do sth.");
}
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
getInstance 方法中对 sIntance 进行了两次判空:
- 第一层避免了不必要的同步;
- 第二层是为了在 null 的情况下创建实例。
假设线程 A 执行到了 sInstance = new Singleton()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了 3 件事:
(1)给 Singleton 的实例分配内存;
(2)调用 Singleton 的构造函数,初始化成员字段;
(3)将 sInstance 对象指向分配的内存空间(此时 sInstance 就是不 null )
但是,由于 Java 编译器允许处理器乱序执行,以及 JDK1.5之前 JMM(Java Memory Model,即内存模型)中 Cache、寄存器到主内存回写顺序的规定,上面的第二和第三执行顺序是无法保证的,可能是 1-2-3,也可能是1-3-2。如果是后者,3 执行完毕,2 未执行之前,被切换到了线程 B,这时候 sInstance 以及不为 null,所以线程 B 会直接取走 sInstance,又因为没有执行 2 所以成员字段没有初始化,再使用时就会出错,这就是 DCL 失效问题,而且这种难以跟踪难以重现的错误很可能会隐藏很久。
在 JDK 1.5 之后,SUN 官方调整了 JVM,具体化了 volatile 关键字,因此只需要将 sInstance 的定义改成private volatile static Singleton sInstance = null;
就可以保证 sInstance 对象每次都是从主内存中读取,volatile 或多或少会影响到性能,但是考虑程序的正确性,这点牺牲还是值得的。
DCL 的优点:资源利用率高;缺点:第一次加载时反应稍慢,在高并发环境下也有很小概率的缺陷。DCL 单例模式是使用最多的单例实现方式,基本能满足需求。
(3)静态内部类单例模式
DCL 虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是它还是存在某些情况下出现失效的问题,这个问题被称为双重检查锁定(DCL)失效,建议使用如下的代码代替:
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
/**
* 静态内部类
*/
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
当第一次加载 Singleton 类时并不会初始化 sInstance,只有在第一次调用 getInstance 方法时才会初始化 sInstance。第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。
(4)枚举单例
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("");
}
}
写法简单是枚举单例最大的有点,枚举在 Java 中与普通的类是一样的。不仅能够拥有自己的字段,还能够有自己的方法,最终的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。
在上面(1)、(2)、(3)的三种单例模式实现中,当一个单例的实例对象通过序列化被写到磁盘,然后再读出来,从而获得一个实例,即使构造方法是私有的,反序列化依然可以通过特殊的途径去创建类的一个新的实例,相当于调用了该类的构造函数(例如:继承 Serializable,写到磁盘是序列化操作,读取是反序列化操作)。反序列化操作提供了一个很特别的钩子函数,类中具体有一个私有的、被实例化的方法 readReslove 方法,这个方法直接加入到单例模式中就可以让开发人员控制对象的反序列话操作。(这应该是底层的知识了,懵逼~~~)
private Object readReslove()throws ObjectStreamException {
return sInstance;
}
也就是在 readReslove 方法中将 sInstance 对象返回,而不是默认生成一个新的对象,JDK 5 的 enum 类型系统已经处理了这个readresolve的情况。
(5)使用容器实现单例模式
public class SingletonMnager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonMnager(){}
public static void registerService(String key,Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
在程序的初识,将多种单例类型注入到一个统一的管理类中,在使用时根据 key 获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户因此了具体实现,降低了耦合度。
不管一那种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,选择哪种实现方式取决于项目本身,如是否复杂的并发环境、JDK 版本是否过低,单例对象的资源消耗等。