单例设计模式(Singleton Design Pattern)

260 阅读5分钟

参考资料

image.png

应用场景

  • 资源访问冲突(多线程导致冲突)
  • 表示全局唯一类(例如配置文件,加载进内存以对象形式存在,唯一)

单例模式实现方式

饿汉式

public class Singleton { 
	private static final Singleton instance = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return instance;
	}
}

有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。


懒汉式

public class Singleton { 
	private static Singleton instance;
	private Singleton() {}
	public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
	    }
	    return instance;
	}
}

懒汉式的缺点也很明显,给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。


双重检测

public class Singleton { 
	private static Singleton instance;
	private Singleton() {}
	public static Singleton getInstance() {
	    if (instance == null) {
		    synchronized(Singleton.class) { 
		        if (instance == null) {
			        instance = new Singleton();
		        }
		    }
	    }
    return instance;
	}
}

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。

网上有些说法说这个会有指令重排的问题,需要加volatile关键字。

但是实际上jdk8已经确保new初始化为原子性操作,不会出现JIT指令重排的情况。


静态内部类

public class Singleton { 
	private Singleton() {}
	private static class SingletonHolder{
	    private static final Singleton instance = new Singleton();
	}
	public static Singleton getInstance() {
	    return SingletonHolder.instance;
	}
}

SingletonHolder 是一个静态内部类,当外部类 Singleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

这种方式既保证了线程安全,又能做到延迟加载。


枚举


public enum Singleton {
	INSTANCE;
}

就是这么简单,用的时候直接Singleton.Class.INSTANCE.method()就可以

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的。保证了实例创建的线程安全性和实例的唯一性。


单例模式存在的问题

对OOP特性的支持不友好

OOP 的四大特性是封装、抽象、继承、多态。

这里之所以会用“不友好”这个词,而非“完全不支持”,是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。


单例会隐藏类之间的依赖关系

单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。


单例对代码的扩展性不友好

单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

例如:假如之前在用单例模式对数据库连接池进行创建。如果后续我们要将慢Sql 和 普通Sql 分开来处理,这个时候就要创建两个,代码也需要全部改动。


单例对代码的可测试性不友好

如果单例类依赖比较重的外部资源,那么在编写测试类的时候,是无法mock的。

如果单例类持有成员变量,那它实际上相当于一种全局变量,被所有的代码共享。那就有可能被其他测试用例修改。


单例不支持有参数的构造函数

解决方案

既然是单例,那么即使需要有参构造,参数也是固定的。 所以可以将固定的参数写入配置类或配置文件中。再去加载。


其实不一定要用单例模式

单例模式的弊端也说完了,其实很多方式都可以达到使用单例模式的初衷。

  • 工厂模式
  • IOC
  • 程序员手动控制,避免创建两个类对象

注:

  • Spring中,每个Bean默认就是单例的,由Spring容器管理这些Bean的生命周期
  • 如果采用多例模式(Prototype),则Bean初始化以后交给J2EE容器,Spring容器不再跟踪生命周期