Skip to content
Sev7e0

单例模式-SingletonPattern

设计模式1 min read

定义

Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并对整个系统提供这个实例。)

角色

Singleton:单例类

Singleton 类通过通过 private 修饰的构造函数使该类不可以被外部实例化,从而保证全局唯一的实例。

使用场景

在系统中,要求该类的对象有且仅有一个,多个的情况下可能会出现性能下降等不良反应,此时就应该使用 Singleton 。

  • 在要求生成唯一序列号的环境中
  • 数据库连接以及频繁的 IO 等消耗大量资源的环境中。
  • 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式

Singleton 优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

Singleton 的缺点

  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中

注意事项

在单例使用过程中,要注意在并发的情况下线程同步问题!避免在多线程的情况下产生多个单例对象,这样就失去了单例的本质。

具体解决方案将会在实现中具体说明,主要依赖 Synchronize 实现同步,再就是通过枚举类保证。

同时在针对单例可能会被反射和序列化破坏,导致单例失效。

如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。具体查看[饿汉模式]

具体实现

单例模式实现有多重方式,其中包括线程安全和线程不安全两大类。

线程不安全

懒汉模式

1public class Singleton {
2 private static Singleton instance;
3 private Singleton (){}
4
5 public static Singleton getInstance() {
6 //在用到时才会被初始化
7 if (instance == null) {
8 instance = new Singleton();
9 }
10 return instance;
11 }
12}

线程安全

懒汉模式

这种方式能够保证在多线程的情况下,保证安全,而且也能保证只有在用到时才会被初始化,但问题的对整个方法同步性能很差。

1public class Singleton {
2 private static Singleton instance;
3 private Singleton (){}
4 //synchronized进行同步
5 public static synchronized Singleton getInstance() {
6 //在用到时才会被初始化
7 if (instance == null) {
8 instance = new Singleton();
9 }
10 return instance;
11 }
12}

饿汉模式

该种方式是在类被加载是就已经进行了初始化,借助于类加载机制保证了线程安全,避免了在多线程的情况下需要进行同步,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

1public class Singleton {
2 private static Singleton instance = new Singleton();
3 private Singleton (){}
4 public static Singleton getInstance() {
5 return instance;
6 }
7}

静态内部类

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉模式不同的是(很细微的差别):饿汉模式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉模式就显得很合理。

1public class Singleton{
2 private Singleton(){}
3 private static class SingletonHolder{
4 private static final Singleton instance = new Singleton();
5 }
6 public static Singleton getInstance(){
7 return SingletonHolder.instance;
8 }
9}

枚举实现

目前被推荐最多的实现方式,同样在第三版的《Effective Java》中被大佬推荐的方式,由于枚举类型的天生线程安全的优势,避免了手动同步,而且在其他实现方式中可以被反序列化的问题也得到了解决。墙裂推荐。

1public emum Singleton{
2 INSTANCE;
3 public void udf(){
4 }
5}

双重检查🔒实现

1public class Singleton {
2 private volatile static Singleton singleton;
3 private Singleton (){}
4 public static Singleton getSingleton() {
5 if (singleton == null) {
6 synchronized (Singleton.class) {
7 if (singleton == null) {
8 singleton = new Singleton();
9 }
10 }
11 }
12 return singleton;
13 }
14}

总结

单例模式作为最普遍的设计模式,无论在实践还是面试中都会经常遇到,可具体分为五种,懒汉模式、饿汉模式、静态内部类模式、枚举模式、双重检查🔒模式。需要重点掌握枚举模式、双重检查🔒模式

参考资料

设计模式 设计模式之禅 [转+注]单例模式的七种写法-HollisChuang's Blog 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题-HollisChuang's Blog