单例设计模式
一个类只允许创建一个对象。
单例设计模式的应用场景
处理资源访问冲突。
例:多个线程中同时写入一个文件,如果每个线程都创建一个 FileWriter,会导致多个线程写入的内容相互覆盖。此时就可以使用单例设计模式,全局只创建一个 FileWriter,多个线程共享一个 FileWriter。
全局唯一类。
某些数据在系统中只应保留一份,也比较适合用单例。如 ID 生成器。
单例实现的关键点
构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
考虑对象创建时的线程安全问题;
考虑是否支持延迟加载;
考虑 getInstance() 性能是否高(是否加锁)。
单例实现方式
饿汉式
关于懒汉式的争论:
反对者:
饿汉式不支持延迟加载,如果实例占用资源多或初始化耗时长,比较浪费资源。最好的方法应该在用到的时候再去初始化。
支持者:
如果初始化耗时长,最好不要等到真正要用它的时候,才去执行这初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。
示例代码:
缺点:不支持延迟加载;
优点:简单,安全,高效。
懒汉式
懒汉式相对于饿汉式的优势是支持延迟加载。其实现复杂度要比饿汉式高。
synchronized
示例代码:
缺点:效率低。
双重检测
示例代码:
优点:支持懒加载,支持高并发。
静态内部类
示例代码:
优点:线程安全,高效,支持懒加载。
枚举
通过枚举类型本身得特性,保证了示例创建的线程安全和示例的唯一性。
示例代码:
容器
如 Spring 容器。
单例存在的问题
单例对 OOP 特性支持不友好
违背了基于接口而非实现的设计原则。即违背了 OOP 抽象的特性。
单例对继承、多态特性的支持也不友好。单例继承会导致代码的可读性变差。
一旦你选择将某个类设计成到单例类,也就意味着放弃了抽象、继承和多态这三个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。
单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
单例对代码扩展性不友好
单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
单例对代码的可测性不好
单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
单例不支持有参数的构造函数
单例的唯一性
即对象唯一性的作用范围。
通常我们所说的单例模式对象唯一指的时进程内唯一。相对应的还有我们的线程内唯一以及集群内唯一。
线程唯一实现方式:ThreadLocal
集群唯一:需要把这个单例对象序列化并存储到外部共享存储区,进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
声明:本文所有内容整理自 极客时间 王争 老师的(设计模式之美 41~42),如果大家对原文感兴趣的话可前往购买学习,写得非常好。我不是销售人员,我只是一个快乐的搬运工。
评论