单例设计模式

  • 有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

1. 什么是单例设计模式

  • 单例设计模式(Singleton Pattern)是一种常用的软件设计模式,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
  • 单例模式通常用于需要全局控制访问的资源,例如配置管理器、日志记录器、线程池等。
  • 通过单例模式,可以避免重复创建对象,节省资源,并确保在整个应用程序中只有一个实例存在,从而实现对共享资源的有效管理。

2. 单例设计模式的好处

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,减少系统开销

  • 由于new操作的次数减少,因为对于系统内存的使用频率也会降低,可以减轻GC(Garbage Collection,垃圾回收器)的压力,缩短GC的停顿时间

  • Spring中的Bean的默认作用域就是singleton(单例)的。除了singleton作用域外,Spring还支持其他几种作用域:

    • prototype(原型):每次请求都会创建一个新的bean实例
    • request(仅Web应用可用):每个HTTP请求都会创建一个新的bean实例(请求bean),该bena仅在当前HTTP request内有效
    • session(仅Web应用可用):每一次来自新session的HTTP请求都会创建一个新的bean实例(会话bean),该bean仅在当前HTTP session内有效
    • application/global-session(仅Web应用可用):每个Web应用在启动时创建一个bean(应用bena),该bean仅在当前应用启动时间内有效
    • websocket(仅Web应用可用):每个WebSocket会话都会创建一个新的bean实例(WebSocket bean)

3. 单例设计模式的实现方式

  • Spring通过ConcurrentHashMap实现单例注册表的特殊方式实现单例模式
  • 核心代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 通过 ConcurrentHashMap(线程安全) 实现单例注册表
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "'beanName' must not be null");
    synchronized (this.singletonObjects) {
    // 检查缓存中是否存在实例
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    //...省略了很多代码
    try {
    singletonObject = singletonFactory.getObject();
    }
    //...省略了很多代码
    // 如果实例对象在不存在,我们注册到单例注册表中。
    addSingleton(beanName, singletonObject);
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
    }

    //将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
    this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));

    }
    }

4. 单例Bean存在线程安全问题吗?

  • 大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
  • 常见的有两种解决办法:
    • 在Bean中尽量避免定义可变的成员变量。
    • 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中(推荐的一种方式)。不过,大部分Bean实际都是无状态(没有实例变量)的(比如DaoService),这种情况下, Bean是线程安全的