单例模式

1,590 阅读4分钟

单例模式的应用

单例模式的优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能无法优化,单例模式的优势就非常明显。
  • 由于单例模式生成一个实例,所以减少了系统性能的开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接生产一个单例对象,然后用永远驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
  • 单例模式可以避免对一个资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

单例模式的缺点

  • 单例模式一般没有接口,扩展很难,若要扩展,除了修改代码基本没有第二种途径可以实现。

    单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例,接口和抽象类是不可能被实例化的。当然在特殊情况下,单例模式可以实现接口、被继承等,需要从系统开发中根据环境判断。

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

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

单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现”不良反应“,可以采用单例模式,具体的场景如下:

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

单例模式的注意事项

首先,在高并发情况下,请注意单例模式的线程同步问题。

如:

public class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}
    
    public static Singleton getSingleton() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

该单例模式在低并发的情况下尚不会出现问题,若系统压力增大,并发量增加时则可能在内存中出现多个实例,破坏了最初预期。

单例模式的扩展

如果要求一个类只能产生固定数量的实例,这种需要产生固定数量的模式叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定内存中有多少个实例,方便系统进行扩展,修正单例可能存在的问题,提供系统响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时可以快速响应

最佳实践

单例模式比较简单,应用也比较广泛,如在Spring中,每个Bean默认就是单例,这样做的优点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁的时候如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。