[设计模式] 单例模式

手机用户请横屏获取最佳阅读体验,REFERENCES中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。

平台 地址
CSDN https://blog.csdn.net/sinat_28690417
简书 https://www.jianshu.com/u/3032cc862300
个人博客 https://yiyuery.github.io/NoteBooks/

单例模式:确保一个类只有一个实例,并提供一个全局访问点

经典单例模式

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
  • 该实现方式会有线程安全问题,因为if的判断语句并不是原子操作。
  • 该实现方式提供了该实例的全局访问点:当你需要时,铜鼓延迟初始化获取这个单例对象。

线程安全问题优化

synchronized 关键词

public static synchronized Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
}
  • 使用JDK原生 synchronized 关键词虽然可以实现同步,但是如果该单例在多线程场景下被访问的频次非常高,也会出现性能问题。

使用静态成员变量创建单例

public class StaticSingleton {

    private static StaticSingleton instance = new StaticSingleton();

    private StaticSingleton() {
    }

    public static StaticSingleton getInstance() {
        return instance;
    }
}
  • 该方式在JVM加载类的时候就会马上创建这个唯一的单例对象,在项目规模和运行时负担不太繁重时可以选择该方式。

双重检查加锁

public class DoubleLockSingleton {

    private volatile static DoubleLockSingleton instance;

    private DoubleLockSingleton() {
    }

    public static DoubleLockSingleton getInstance() {
        //检查单例是否已存在
        if (instance == null) {
            //多线程进入获取初始化实例时
            synchronized (DoubleLockSingleton.class) {
                //进入区块后,再检查一次,如果还为null,才创建实例
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}
  • volatile 关键词确保了 instance 变量被初始化时,多个线程操作的实例对象是从主存中加载的。
  • volatile 关键词确保了相关变量被JVM解析为字节码指令后不会进行重排序。可以避免多线程场景下获取到的单例实例是初始化完成后写入到主存的。
  • 该方式是对 DoubleLockSingleton.class 类直接加锁,针对于初始化场景下多线程同时触发的,单例实例化仍然会有获取锁的性能消耗,但是区别在于后续判断非 null 的话直接返回了,而不是让所有调用该方法的线程都处于阻塞状态。
  • 考虑到不同JVM对volatile的实现机制会有差异,很多JVM没有实现 volatile 的顺序一致性功能,所以更为严谨的说 Double-Check 加 volatile 也不完全可靠。

登记式/静态内部类

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟使用静态成员变量创建单例方式不同的是:后者只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。

因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比使用静态成员变量创建单例方式就显得很合理(这也是延迟初始化的优势所在)。

public class StaticHolderSingleton {

    private static class SingletonHolder {
        private static final StaticHolderSingleton INSTANCE = new StaticHolderSingleton();
    }

    private StaticHolderSingleton (){}

    public static StaticHolderSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚举方式

这种方式可以组合前文的单例创建方式来提供一个单例的管理入口。

@Getter
public enum SingletonEnum {

    /**
     * 单例
     */
    INSTANCE(Singleton.getInstance());

    private Singleton singleton;

    SingletonEnum(Singleton singleton) {
        this.singleton = singleton;
    }
}

或是这样

@Getter
public enum SingletonEnum {

    /**
     * 单例
     */
    INSTANCE;

    public void method(){
        //TODO STH
    }
}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

而且不能通过 reflection attack 来调用私有构造方法。

总结

上述实现方式总结如下:

方式 昵称 线程安全 延迟初始化 特点
经典实现方式 懒汉式,线程不安全 不支持多线程
synchronized 关键词 懒汉式,线程安全 多线程性能不佳
使用静态成员变量创建单例 饿汉式 对于项目规模大,单例多的时候谨慎使用
双重检查加锁 懒汉式 多线程性能良好
登记式/静态内部类 \ 多线程性能良好,但是只支持静态域
枚举方式 \ \ \ 简洁

单例模式特点

  • 单例模式确保程序中只有一个实例
  • 单例模式的实现需要一个私有构造器、一个静态方法、一个静态变量
  • 单例设计需要考虑多线程场景
  • 不同JDK版本可能对单例的实现方式有所限制
  • 多个类加载器会导致多个单例出现

设计原则

对于公共使用的,全局唯一的访问点,可以使用单例封装内部实现逻辑后对外提供能力,而非多处创建依赖对象,消耗系统资源。

REFERENCES

更多

扫码关注架构探险之道,回复文章标题,获取本文相关源码和资源链接

[设计模式] 单例模式_设计模式

知识星球(扫码加入获取历史源码和文章资源链接)

[设计模式] 单例模式_设计模式_02