Singleton Design Pattern - tenji/ks GitHub Wiki

设计模式之单例模式

一、单例的五种写法及应用场景

1.1 懒汉,线程安全

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

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的 lazy loading,但是,遗憾的是,效率很低,99% 情况下不需要同步。

1.2 饿汉

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

这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

1.3 枚举

public class EnumSingleton {
    private EnumSingleton() {}
    public static EnumSingleton getInstance() {
        return Singleton.INSTANCE.getInstance();
    }
    
    private static enum Singleton {
        INSTANCE;
        
        private EnumSingleton singleton;
        //JVM会保证此方法绝对只调用一次
        private Singleton() {
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance() {
            return singleton;
        }
    }
}

这种方式是 Effective Java 作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。

1.4 双重校验锁(Double-Checked Locking)

单例的双重检查锁是一种反模式(anti-pattern),不应使用。

public class Singleton
{
    private volatile static Singleton singleton;
    
    private Singleton()
    {
    }
    
    public static Singleton getSingleton()
    {
        if (singleton == null) // 第11行
        {
            synchronized (Singleton.class)
            {
                if (singleton == null)
                {
                    singleton = new Singleton(); // 第17行
                }
            }
        }
        return singleton;
    }
}

这个是第二种方式的升级版,俗称双重检查锁定。在 JDK1.5 之后,双重检查锁定才能够正常达到单例效果。

双重校验锁的方式为什么需要加 volatile 关键字?

主要是为了防止指令重排。更多关于指令优化导致的重排序问题,传送门

因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

  1. memory = allocate(),给 singleton 分配内存空间;
  2. ctorInstanc(memory),开始调用 Singleton 的构造函数等,来初始化 singleton;
  3. s = memory,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。

详情参考:The "Double-Checked Locking is Broken" Declaration

1.5 静态内部类方式

public class Singleton
{
    private static class SingletonHolder
    {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton()
    {
    }
    
    public static final Singleton getInstance()
    {
        return SingletonHolder.INSTANCE;
    }
}

这种方式同样利用了 classloder 的机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。

想象一下,如果实例化 instance 很消耗资源,我想让他延迟加载,另外一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

二、什么时候适合使用单例模式?

The main point of singletons isn't resources but domain modelling. If your class represents something, like a log file, that you only have one of, it should be a singleton. If not, it shouldn't.

单例的实际使用场景:

  • 记录器(Logger):管理整个系统的记录操作;
  • 数据库连接(Database Connection):为数据库交互提供共享连接;
  • 配置管理器(Configuration Manager):集中应用程序设置和属性;
  • 缓存管理器(Cache Manager):处理缓存操作以提高性能;
  • 线程池(Thread Pool):管理并发编程中的线程创建和执行。

三、什么时候不适合使用单例模式?

Short answer: most of the time. Long answer: when it's simpler to pass an object resource as a reference to the objects that need it, rather than letting objects access the resource globally.

Despite its benefits, the Singleton pattern has drawbacks:

  • 全局状态(Global State):引入全局状态,使系统行为跟踪复杂化并可能导致错误;
  • 紧耦合(Tight Coupling):直接访问 Singleton 可能会导致紧耦合,从而使将来的更改或替换变得困难;
  • 生命周期管理(Lifecycle Management):管理单例的生命周期,尤其是重新初始化,可能很复杂;
  • 违反单一职责原则(Single Responsibility Principle Violation):Singleton 类具有双重职责:实例创建和任务执行;
  • 违反开放封闭原则(Open Closed Principle Violation):单例不开放扩展,因为它总是返回自己的实例。

∞、参考链接