并发设计模式(1):单例模式

40 阅读4分钟

单例模式

不使用单例模式会出现什么问题?

在使用单例模式之前,如果程序中需要某个对象,就直接new一个出来,即每次使用都要创建一个新的对象。

这种方法在某些场景下会出现明显的问题,有些对象创建成本很高:

数据库连接池
线程池
缓存管理器
配置中心客户端
日志管理器

这些对象一般不需要创建很多份。

其次多个对象会导致状态不一致:

系统本来希望共享一份配置,
但因为创建了多个对象,
导致状态分裂。

最后是无法限制对象的数量,有些类从业务语义上就应该只有一个对象:

系统配置管理器
任务调度器
全局 ID 生成器
Spring 容器
日志管理器

如果随便的new,就没有办法约束外部代码

单例模式解决的问题

单例模式主要解决两个问题:

  1. 保证一个类只有一个实例
  2. 提供一个全局访问点

单例模式的核心就是保证一个类在程序运行过程中只有一个实例,并提供一个全局访问点来获取这个实例

通常实现单例模式要有三个核心设计点:

1. 构造方法私有化,防止外部 new 对象
2. 类内部自己创建唯一实例
3. 对外提供静态方法返回这个实例

单例模式的饿汉式实现

public final class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这是一个典型的饿汉式的单例模式。

优点:

  1. 线程安全:在类加载阶段中的初始化阶段就完成了实例化,类加载过程天然线程安全,避免了多线程同步。
  2. 获取速度快:实例已经加载完成,可以直接返回
  3. 防止反射攻击

缺点:

  1. 内存浪费:无论是否使用,它都会创建
  2. 启动延迟:如果单例的初始化很耗时,会延迟应用的启动时间

因此饿汉式单例模式适用于:

  1. 对象创建成本不高
  2. 对象一定会被使用

单例模式的懒汉式实现

public final class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

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

懒加载的思想是:最初并不需要实例化instance,⽽是当getInstance()⽅法被第⼀次调⽤时,创建单例对象。但这会引出线程同步的问题,因此需要使用synchronized来锁住同步。

但是写操作只会在还没有实例化instance的情况下发生,其余都是读操作,这样直接用synchronized将整个方法包裹,太过于笨重,性能不够好。

单例模式的懒汉式双重检查实现

public final class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
    }

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

        return instance;
    }
}

第一次判断避免读操作也加锁,第二次判断防止排队的线程获取锁后再次创建新的实例。 volatile关键字防止指令重排序导致拿到还没有初始化的对象,其他业务代码直接使用这个没有初始化的对象:

线程 A:
进入 synchronized
分配对象内存
instance = memory   // 此时 instance 已经不是 null
对象还没初始化完

线程 B:
执行第一次 if (instance == null)
发现 instance != null
直接 return instance
使用了一个未初始化完成的对象

同时volatile也防止在instance = new Singleton()后续线程B就能直接看到最新的值,不需要等待再加锁才能看到最新值。

单例模式的静态内部类懒加载实现

public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE; // 这里才会触发类加载,实现懒加载
    }
}

静态内部类是一个独立的类,它与外部类没有绑定关系。静态内部类和其外部类在编译后会生成两个独立的字节码文件.class文件)这意味着:

  1. 不会随外部类加载而加载:加载Singleton类时,不会自动加载Holder
  2. 有自己的类加载时机Holder类只在被引用时才加载

因此上述的单例模式,只有在调用Holder.INSTANCE时才会创建实例,其线程安全有JVM进行保证:

当多个线程同时首次调用getInstance()时:

  1. 第一个线程触发Holder类的加载
  2. JVM类加载机制保证<clinit>(类初始化方法)只会被执行一次
  3. 其他线程会等待类加载完成
  4. 所有线程都得到同一个已初始化的实例

单例模式的枚举类实现

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("执行逻辑");
    }
}

枚举单例的优点:

  1. 写法简单
  2. 线程安全
  3. 天然防止反射破坏
  4. 天然支持序列化

普通单例的私有构造方法可以通过反射强行调用:

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s = constructor.newInstance();

但枚举类的实例创建由JVM控制,反射不能随便创建枚举对象。

所以枚举单例在安全性上很强。