《Java Design Patterns》第四章 单例模式 - 确保对象的唯一性

第四章 单例模式 - 确保对象的唯一性

单例模式用于确保对象的唯一性,为了确保系统中某一个类只有一个唯一实例,当这个唯一实例创建后,无法再创建一个同类型的其他对象。

一、概述

单例模式:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。属于对象创建型模式。

1.2 单例模式中的角色

  • Singleton - 单例角色
    • 在单例类内容实现只生成一个实例,同时提供一个静态工厂方法用于获取该唯一实例
    • 为了防止外部实例化,将其构造函数设计为私有
    • 单例类内部顶一个Singleton类型的静态对象,作为外部共享的唯一实例

二、懒汉模式与饿汉模式

2.1 饿汉式单例类 - 线程安全

懒汉单例模式

class EagerSingleton{
    private static final EagerSingleton instance = new EagerSingleton();
    private EagerSingleton(){}
    public static EagerSingleton getInstance(){
        return instance;
    }
}

饿汉模式实现起来最为简单,在类加载时,静态变量 instance 就会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。

2.2 懒汉式单例类(一) - 线程安全

懒汉单例模式

懒汉模式能将类进行 延迟加载,即在需要使用时再加载实例,为了避免多线程调用 getInstance() 时实例化多个 LazySingleton,这里使用 synchronized 关键字进行加锁。

class LazySingleton{
    private static LazySingleton instance = null;
    private LazySingleton(){}
    synchronized public static LazySingleton getInstance(){
        if(instance == null)
            instance = new LazySingleton();
        return instance;
    }
}

2.3 懒汉式单例类(二) - 线程不安全

虽然 synchronized 关键字进行了线程锁定,但是每次调用 getInstance() 方法时都会进行加锁操作,在多线程高并发场景下,会导致系统性能大大降低。

事实上,只需要在 new L:azySingleton() 时进行加锁即可,即第一次实例化实例时加锁。

public static LazySingleton getInstance(){
    if(instance == null){
        synchronized(LazySingleton.class){
            instance = new LazySingleton();
        }
    }
    return instance;
}

2.4 懒汉模式单例类(三 DoubleCheck) - 线程不安全

如果当两个线程同时进行了 if 条件之中,当线程A完成实例化之后,线程B继续执行,依旧会进行一次实例化。违背了单例模式的设计思想。

双重检测机制:因此我们需要在 synchronized 中在进行一次判断

public static LazySingleton getInstance(){
    if(instance == null){
        synchronized(LazySingleton.class){
            if(instance == null){
                instance = new LazySingleton();
            }
        }
    }
    return instance;
}

2.5 懒汉模式单例类(四 volatile+DoubleCheck) - 线程安全

上述代码看起来可能没什么问题,但是

instance = new LazySingleton() 在内存中的指令顺序应该为:

  1. instance 分配内存地址
  2. 实例化 LazySingleton
  3. 将实例化地址指向instance

因为 JVM 的指令重排和优化,可能会导致以上执行顺序与所想像的不一致。最终导致单例出现问题,因此我们在静态实例 instance 前,加上 volatile 关键词,避免 instance 的实例化发生指令重排。

因此完整的懒汉模式如下:

class LazySingleton{
    // 使用volatile 修改时静态实例,避免指令重排序
    private volatile static LazySignleton instance = null;
    // 构造方法私有化,防止外部实例
    private LazySingleton(){}
    // 静态工厂类,用于返回唯一实例,且使用延迟加载技术
    public static LazySingleton getInstance(){
        // 第一重检查,只有当instance 为空时才进行实例化
        if(instance == null){
            // 实例化时对当前类进行加锁,避免多线程同时实例化
            synchronized(LazySingleton.class){
                // 第二重检查,避免当前线程通过第一重检查后其他线程已实例化过
                if(instance == null){
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

缺点也很明显:1. volatile 关键词虽然能保证实例避免重排,但是只在1.5以上的版本才能生效 2. volatile关键词会屏蔽 JVM 的一些代码优化,可能导致系统运行效率降低

三、一种更好的单例实现方法

3.1 懒汉模式与饿汉模式的缺点

饿汉模式:不能实现延迟加载,不管实例是否使用都将占据内存

饿汉模式:繁琐的线程安全控制,且性能会受到一定影响

3.2 静态内部类单例 - IoDH - 线程安全

class Signleton{
    private Singleton(){}
    // 静态内部类只有在第一次调用时才会进行加载
    private static class HolderClass{
        // 因为instance 不是Singleton的成员变量,所以不会在Singleton加载时实例化
        private final static Singleton instance = new Singleton();
    }
    // 通过JVM特性来保证线程安全性,确保成员变量只能初始化一次
    public static Singleton getInstance(){
        return HolderClass.instance;
    }
}

Initialization Demand Holder 技术,在单例类内部增加一个静态内部类,在该内部类中创建单例对象,再将单例对象通过 getInstance() 方法返回给外部使用。

但是IoDH依赖于JVM的特性,并不适用于所有语言。但是IoDH既可以实现延迟加载,又可以保证线程安全,又不影响性能,不失为一种最好的Java语言单例模式。

四、总结

优点

  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,严格控制客户如何访问
  2. 节约系统资源。因为内存中只存在一个对象,无需频繁的创建、销毁对象
  3. 允许可变数目的实例。可以通过变种,让getInstance返回至多几个实例

缺点

  1. 由于单例类没有抽象层,很难进行扩展
  2. 单例类职责过重,违背了“单一职责原则”。因为单例类即是工厂角色,提供工厂方法,又是产品角色,包含一些业务方法。将产品的创建和产品的功能融合到了一起
  3. 许多面向对象语言(如Java、C#)的自动垃圾回收技术,如果实例化的共享变量长时间不被使用。GC 会自动销毁并回收资源,导致单例对象状态的丢失。

适用场景

  1. 系统只需要一个实例对象。如资源消耗过大的对象、资源管理器等只能实例一次的对象
  2. 客户调用类的单个实例只允许使用一个公共访问点,不能通过其他途径访问该实例。
文章作者: koral
文章链接: http://luokaiii.github.io/2019/06/27/读书笔记/《JavaDesignPatterns》/6.单例模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自