设计模式之美笔记 —— 单例模式


问题:

  1. 为什么要使用单例?
  2. 单例存在哪些问题?
  3. 单例与静态类的区别?
  4. 有何替代的解决方案?

一、为什么要使用单例模式

单例模式:约束一个类只可以创建一个对象(或者说实例)。

为什么我们需要单例模式?他可以解决哪些问题?

      实战案例1:处理资源访问冲突问题

      实战案例2:表示全局唯一类


二、如何实现一个单例类

详解


三、单例模式存在哪些问题?为什么会被称为反模式

1、单例对于OOP特性的支持不好

OOP特性:封装,抽象,继承,多态

单例模式的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。因此,例如我们通过单例模式实现一个全局的序列号生成器,如果对于产品和用户的序列号生成逻辑不同时候,对于单例类的修改,就会涉及到项目中所有调用了序列号生成器都需要进行修改。

单例模式对于继承和多态也不友好。虽然单例模式可以被继承,实现多态,但是一般情况下不回这么做,因为会导致代码的可读性下降,看起来很奇怪。所以使用单例模式,就会意味着放弃了继承和多态,从而损失了对之后的需求变更扩展性。

 

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

在使用普通类的时候,通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。

 

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

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

这样的需求并不少见。我们拿数据库连接池来举例解释一下。

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

所以对于数据库连接池,线程池这类资源池,最好不要设计成单例类。

 

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

 

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

比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。


四、单例模式的替代方式

类对象的全局唯一性可以通过多种不同的方式来保证。

  • 我们既可以通过单例模式来强制保证,
  • 也可以通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,
  • 还可以通过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

五、如何设计实现一个集群环境下的分布式单例模式?

 

1、如何理解单例模式中的唯一性。

“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?

答案:进程中唯一。进程指的一套可执行的指令。进程间不共用内存空间。

单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。进程唯一还保证了线程内和线程间都唯一。

 

2、如何实现一个线程唯一的单例对象。

通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。

实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

  private static final ConcurrentHashMap<Long, IdGenerator> instances
          = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}

3、如何实现集群环境下的单例

集群中的单例,也就指的是进程间也唯一的单例。

具体实现方式有些复杂:

  1. 首先需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。
  2. 进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
  3. 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值