参考资料
应用场景
- 资源访问冲突(多线程导致冲突)
- 表示全局唯一类(例如配置文件,加载进内存以对象形式存在,唯一)
单例模式实现方式
饿汉式
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容器不再跟踪生命周期