单例模式是我们常用的一种模式,我也经常使用,但是却不知道它还是一种设计模式,估计很多初级工程师也跟我一样,只会这一种设计模式吧。
第二章 应用最广的模式——单例模式
单例模式是应用最广的模式之一
1.定义
确保某一个类只有一个实例,而且自行实例化并向整个程序提供这个实例。许多时候整个程序只需要拥有一个全局对象,这样有利于我们协调程序整体的行为。
2.使用场景
避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。比如创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源,这时候就要考虑使用单例模式。
3.UML类图
4.单例模式关键点
1).构造方法不对外开放,一般为 private。
2).通过一个静态方法或者枚举返回单例类对象。
3).确保单例类的对象有且只有一个,尤其是在多线程环境下。
4).确保单例类对象在反序列化的时候不会重新构建新的对象。
通过私有化单例类的构造方法,使客户端不能通过 new 的形式构造单例类的对象,然后暴露一个公有的静态方法,使客户端可以获取到单例类的唯一对象,在获得这个单例对象的过程中,需要确保线程安全,即在多线程环境下获取单例类对象也是有且只有一个,这也是单例模式中比较困难的地方。
5.实现单例模式的六种方式
1).饿汉模式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
之所以会被称为饿汉模式,是因为这样的方式在声明静态对象的同时就已经初始化了,无论用户是否需要,都先给创建单例对象了,生怕没有机会创建似的。饿汉模式是线程安全的。
2).懒汉模式
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
之所以会被称为懒汉模式,是因为声明的时候懒得去创建对象,等用户需要的时候再去创建,给 getInstance() 方法添加了synchronized 关键字后,懒汉模式也是线程安全的,如果不添加 synchronized 关键字,则在多线程的情况可能仍会创建多个实例,但是如果加了 synchronized 关键字,那么每次调用 getInstance() 方法,无论单例是否已经实例化都会去进行同步,所以会消耗不必要的资源,这也是懒汉模式的最大问题。
懒汉模式在使用时才会实例化,虽然一定程度上节约了资源,但是正因为这样,所以第一次加载的时候可能反应稍慢,最大的问题还是每次调用 getInstance() 方法都会进行同步,造成不必要的开销,所以一般不建议使用懒汉模式。
3).Double Check Lock(DCL)
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种模式跟懒汉模式比较,不同点就在于 getInstance() 方法中对 singleton 两次判断是否为空,第一次是为了避免不必要的同步,第二次是为了在 singleton 为空的情况下创建实例。这种模式的优点是资源利用率高,既能在需要单例的时候才实例化,也能避免不必要的同步。
DCL 模式能在绝大多数情况下保证单例对象的唯一性,但是在高并发的环境下也可能例外,虽然概率很小。具体原因暂时还没有去深究,等学得更深了会去了解的。
4)静态内部类单例模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingleHolder.singleton;
}
private static class SingleHolder {
private static final Singleton singleton = new Singleton();
}
}
这种模式既能保证线程安全,也能保证单例对象的唯一性,同时也延迟了单例对象的实例化(即在需要单例的时候才实例化),所以这是推荐使用的单例模式实现方式。
5).枚举单例模式
public enum Singleton {
SINGLETON;
public void doSomething() {
System.out.println("do something");
}
}
这种模式最大的优点就是写法简单,枚举在 Java 中和普通的类一样,有字段也可以有方法,创建枚举实例默认是线程安全的,而且任何情况下它都是一个单例。
这个任何情况与其他几种方式不同,其他几种方式在某种情况下会重新创建对象,这种情况就是反序列化,什么是反序列化暂时我也没去了解,慢慢深究吧,反正它是一种特殊途径,要避免这种特殊情况,上面四种模式都需要添加如下方法:
private Object readResolve() throws ObjectStreamException {
return singleton;
}
但是枚举则不存在这个问题,即使反序列化它也不会重新生成新的实例。
6).使用容器实现单例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManager() {
}
public static void registerSingleton(String key, Object singleton) {
if (!objMap.containsKey(key)) {
objMap.put(key, singleton);
}
}
public static Object getSingleton(String key) {
return objMap.get(key);
}
}
这是单例模式的一种另类实现,在程序初始时,将多种单例类型注入到一个统一的管理类中,使用时根据 key 来获取对应的单例。这种方式可以管理多种类型的单例,并且使用时可以通过统一接口进行获取单例的操作,降低使用成本,也隐藏了具体实现,降低了耦合。
6.总结
单例模式是很常用的一种设计模式,通常没有高并发的情况下,这几种单例模式的实现方式都不会有太大区别,出于效率考虑,一般使用 DCL 方式和静态内部类实现单例方式。
单例模式的优缺点:
优点:
1).减少了不必要的内存开支,特别是一个对象频繁的创建和销毁时。
2).减少了系统开销,特别是一个对象需要较多资源时,如产生其他依赖对象、读取配置等。
3).避免对资源的多重占用,比如写文件操作时,由于只有一个实例,就避免对同一个资源文件同时进行写操作。
4).可以在系统设置全局的访问点,优化和共享资源访问。
缺点:
1).单例模式一般没有接口,扩展很困难,想扩展基本上只能修改代码
2).单例模式如果持有Context,则很容易引发内存泄漏,所以传递给单例对象的Context最好是ApplicationContext。