单例模式介绍

线程安全

Posted by qin4zhang on April 6, 2021

注意

想法及时记录,实现可以待做。

简介

有时对于某些类来说,只有一个实例对某些类很重要。 我们只需要一个对象的一个​​实例,并且如果我们实例化超过一个,我们将遇到各种问题,例如不正确的程序行为、过度使用资源或不一致的结果。

我们只需要一个实例,比如:

  • 应用上下文
  • 缓存
  • 线程池
  • 输入输出的驱动

饿汉模式(线程安全)

public final class SingletonEager {
    // 自行创建实例
    private static final SingletonEager instance=new SingletonEager();
    // 私有构造函数
    private SingletonEager(){}
    // 通过该函数向整个系统提供实例
    public static SingletonEager getInstance(){
        return instance;
    }
}

该方式是以静态变量的形式提供的实例,它通过JVM来保证只会有一个实例被初始化。

这种方式的特点明显,在类的初始化阶段就进行了对象的生成,所以称之为饿汉模式。如果我们有大量的这种类的初始化,还未使用,就已经生成了对象,占用内存,这种是会占用资源的。

懒汉模式

public final class SingletonLazy {
    // 与饿汉式相比,类初始化并不进行实例化
    private static Singleton instance= null;
    // 私有构造函数
    private SingletonLazy(){}
    // 通过该函数向整个系统提供实例
    public static SingletonLazy getInstance(){
        // 当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            // 真正进行实例化对象
            instance = new SingletonLazy();
        }
        // 返回已存在的对象
        return instance;
    }
}

这种懒汉模式与上述饿汉模式相比,差异明显在于,只有调用的时候,才会进行初始化,否则不会提前进行实例化对象。这种有利于按需使用资源,不浪费资源。

但上述的方式,在单线程的情况下才是可行的,多线程是不安全的。试想多个线程同时在判断实例化,进而就会产生多个新的对象,导致实例化对象不是全局唯一。

synchronized的懒汉模式

public final class SingletonLazy {
    // 与饿汉式相比,类初始化并不进行实例化
    private static SingletonLazy instance= null;
    // 私有构造函数
    private SingletonLazy(){}
    // 加同步锁,通过该函数向整个系统提供实例
    public static synchronized SingletonLazy getInstance(){
        // 当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            // 真正进行实例化对象
            instance = new SingletonLazy();
        }
        // 返回已存在的对象
        return instance;
    }
}

这种加同步锁的方式确实可以保证线程安全,不过在方法上加锁,这种锁的粒度比较大,会造成锁竞争激烈,带来系统的性能问题。

Double-Check-Lock的懒汉模式

public final class SingletonLazy {
    // 与饿汉式相比,类初始化并不进行实例化
    private static SingletonLazy instance= null;
    // 私有构造函数
    private SingletonLazy(){}
    // 通过该函数向整个系统提供实例,注意:这里没加同步锁
    public static SingletonLazy getInstance(){
        // 第一次判断,当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            // 加同步锁
            synchronized (SingletonLazy.class){
                // 第二次判断
                if(null == instance){
                    // 真正进行实例化对象
                    instance = new SingletonLazy();
                }
            } 
        }
        // 返回已存在的对象
        return instance;
    }
}

之所以要两次判断,是因为多线程环境中,第一次判断如果已经有多个线程进入,那么同步锁这一步,只是让多线程同步进入实例化的过程,最终还是会有多个实例。

第二次判断,就可以完全避免其他的线程获取到锁,从而进行实例化对象。

而且这种同步锁的粒度远低于方法上的同步,可以同时被大量的线程使用,而不会出现抢锁激烈的情况。

那这么看来,目前这这种方案是比较合适的吗?仔细看看,不一定,由于JVM会有指令重排发生,也就是说我们希望的代码执行顺序在JVM层面执行会存在变化,那么这种方式的实例化对象,也是不安全的。

于是,给实例变量使用关键字,保证线程间的可见性 volatile,也可以避免被指令重拍,让JVM按照我们希望的执行顺序去执行。

volatile+Double-Check-Lock的懒汉模式

public final class SingletonLazy {
    // 与饿汉式相比,类初始化并不进行实例化
    private volatile static SingletonLazy instance= null;
    // 私有构造函数
    private SingletonLazy(){}
    // 通过该函数向整个系统提供实例,注意:这里没加同步锁
    public static SingletonLazy getInstance(){
        // 第一次判断,当instance为null时,则实例化对象,否则直接返回对象
        if(null == instance){
            // 加同步锁
            synchronized (SingletonLazy.class){
                // 第二次判断
                if(null == instance){
                    // 真正进行实例化对象
                    instance = new SingletonLazy();
                }
            } 
        }
        // 返回已存在的对象
        return instance;
    }
}

这种写法,相比较而言已经安全了许多,但是也不是完全可靠。还有一些方式可以破坏这种实例化对象。

  • 序列化
  • clone
  • 反射
  • 多个类加载器加载

不过有相应的方式可以一一解决这个问题,参考以下方式:

    // 方式反射实例化对象,这里繁盛调用,会抛出异常,防止多次实例化对象
    private Singleton() {
        if (sc != null) {
            throw new IllegalStateException("Already created.");
        }
    }
    // 这里防止序列化对生成新的对象
    private Object readResolve() throws ObjectStreamException {
        return sc;
    }
    private Object writeReplace() throws ObjectStreamException {
        return sc;
    }

    // 覆写clone方法,防止生成新的对象
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Singleton, cannot be clonned");
    }

    // 防止不同的类加载器导致生成新的对象
    private static Class getClass(String classname) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null)
            classLoader = Singleton.class.getClassLoader();
        return (classLoader.loadClass(classname));
    }

静态内部类的懒汉模式

当调用方法的时候才会创建静态内部类的实例,从而保证了只会实例化一次静态变量,达到全局唯一。

public final class SingletonLazy {
    // 私有构造函数
    private SingletonLazy(){}

    // 静态内部类实现
    public static class InnerSingleton {
        // 自行创建实例
        private static SingletonLazy instance=new SingletonLazy();
    }
    
    // 通过该函数向整个系统提供实例
    public static SingletonLazy getInstance() {
        // 返回内部类中的静态变量
        return InnerSingleton.instance;
    }
}

枚举类的单例模式(推荐使用)

这种实现方式借助于枚举类,实现方式简单可靠,它还有以下优势:

  • Singleton实例是线程安全的,因为JVM保证以线程安全的方式创建枚举实例。
  • JVM 还保证在 Singleton 类实现 Serializable 时保持 Singleton 状态,这在没有 Enum 的情况下仍然可以使用 readResolve() 方法,但繁琐而复杂。
public enum SingletonLazy{
    SINLETON;

    public void doSomething(){
        
    }
}

总结

一般情况下,建议使用饿汉方式。只有在要明确实现懒加载效果时,才会使用静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。

参考

  1. The Singleton Design Pattern
  2. Java Singleton Design Pattern Best Practices with Examples
  3. 单例模式