欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

创建型——单例模式

程序员文章站 2022-07-13 23:44:40
...

创建型——单例模式

单例模式是创建型设计模式中写法最最最多的一种(面试官也常问),提起单例模式,我们总会想到有饿汉式,懒汉式,枚举式等等实现方式。这个时候问题来了,我们往往对写法单一的知识能够清晰掌握,而对写法多样(虽然它本身并不难)的,在面试过程中,或者在使用过程中会手忙脚乱。归根结底还是没有熟悉每一种写法中对应比较适合的应用场景。本文尽量将每一种单例的写法与其对应适合的场景进行列举,让单例相关的知识更加深刻。

定义

  • 保证系统中一个类仅有一个实例,并提供一个全局访问点

适用场景

  • 确保任何情况下都绝对只有一个实例。

优点

  • 减少内存开销,在内存里只有一个实例。
  • 避免对资源的多重占用
  • 设置全局访问点,严格控制访问

缺点

  • 扩展困难

重点

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化反序列化安全(了解)

序列化和反序列化安全指的是,将单例对象序列化之后,再反序列化之后,发现:是一个新对象。

解决方案:单例类里面写一个readResolve()方法,将单例对象返回就行了,具体原因是在ObjectStreamClass.java里面。

  • 防御反射攻击(了解)

使用反射进行单例对象的创建会破坏单例模式。解决方法:在私有构造器里面进行逻辑判断即可。

code

饿汉式——必须掌握,因为它足够简单

优点和缺点

饿汉式的优点和缺点都围绕一点,那就是系统初始化就加载。它不管我们程序中是否使用了这个实例,哪怕在某个系统中只有1%的流量会触发使用了这个单例对象的业务,他也会一直占用着这个资源。单例对象本身是个小对象还好说,但是如果这个单例对象还是一个“大对象”,这就造成了资源的浪费,还有可能出现频繁gc,这就是大事了!

但是!出现以上问题的原因是什么?根本原因还是没有认真分析到业务场景,书写单例对象本身就是为了让业务进行使用,那这个业务本身就很低频,使用很少,那为什么还要硬去使用单例模式?有必要?应该没有那么必要。

线程安全性

  • 线程安全,因为系统启动就加载好了对象,用的时候直接给。

适用场景

  • 业务频繁使用。
  • 需要满足多线程“安全”,这个安全指的是多线程下这个对象也只有一个。
  • 单例对象比较大(因为大,所以才需要程序启动的时候就加载)

代码实现

/**
 * 饿汉式
 */
public class HungrySingleton {

    // 这个在初始化的时候也可以写在一个静态块里面。这个在启动的时候,就会加载这个静态的对象。
    // 不理解为啥程序启动的时候会加载,可以看一下《深入理解java虚拟机》(第三版)
    private final static HungrySingleton instance = new HungrySingleton();

    // 单例的各种属性,很多属性
    private String field1;
    private String field2;
    private String field3;
    private String field4;
    private String field5;
    private String field6;
    private String field7;
    private String field8;
    private String field9;
    private String field10;

    // 私有构造器,防止类外进行调用。简单起见就随便赋值了
    private HungrySingleton(){
        this.field1 = "field1";
        this.field2 = "field2";
        this.field3 = "field3";
        this.field4 = "field4";
        this.field5 = "field5";
        this.field6 = "field6";
        this.field7 = "field7";
        this.field8 = "field8";
        this.field9 = "field9";
        this.field10 = "field10";
    }

    // 开放方法供外边的类进行单例对象的使用
    public static HungrySingleton getInstance(){
        return instance;
    }
}

懒汉式——写法繁多,分场景看待

  • 首先,这里不再写线程不安全的懒汉模式,因为就目前的国内情况来看,各公司基本都是什么高并发,多线程,动不动就千万的QPS(也不知道是不是真的),所以线程不安全的懒汉模式写法,其实用武之地很小很小(除了写着玩)。
  • 其次,线程不安全的单例模式,本身就不应该叫做单例模式,因为单例模式是:任何情况下保证只有一个实例。恰巧,多线程也是情况之一。
  • 最后,大多数博文(可能是作者想一步步让这个单例变成安全的,一步步分析,给读者更多的解惑),单例模式上来就是线程不安全的懒汉式,很多读者不完整的看一篇博客。结果就是,线程安全的单例没看到,就像每次翻书只看了个开头,就坚持不下去了。翻看的多了,就记住这个不安全的了,面试官让写的时候,就会这个,岂不是很尴尬?所以接下来只介绍线程安全的懒汉单例写法

线程安全的几个懒汉式的写法

枚举型写法——最最应该掌握的一种写法!

枚举型的单例模式是最最推荐的一种写法,因为枚举是从jvm层面直接保证单例的。我们无需考虑任何情况。

枚举类型的单例模式,是最为推荐的一种写法,有以下优点:

  • 无需使用任何的锁或者条件判断。多线程方面,jvm就帮我们做了,且是用到的时候才会去加载。
  • 保证单例,只会加载一次。
  • 书写简单!(这个应该是除了饿汉式的最简单的写法了)
/**
 * 最推荐的一种单例模式
 */
public enum  EnumSingleton {
    
    // 具体的单例对象
    INSTANCE("data1","data2","data3","data4","data5","data6");

    // 各种属性。
    private Object data1;
    private Object data2;
    private Object data3;
    private Object data4;
    private Object data5;
    private Object data6;
	
    // 枚举的构造,默认就是private的。外面不可能创建对象。
    EnumSingleton(Object data1, Object data2, Object data3, Object data4, Object data5, Object data6) {
        this.data1 = data1;
        this.data2 = data2;
        this.data3 = data3;
        this.data4 = data4;
        this.data5 = data5;
        this.data6 = data6;
    }
	// 枚举只可以在内部进行实例化,所以在使用的时候,这里通常只开放getter方法
    public Object getData1() {
        return data1;
    }

    public Object getData2() {
        return data2;
    }

    public Object getData3() {
        return data3;
    }

    public Object getData4() {
        return data4;
    }

    public Object getData5() {
        return data5;
    }

    public Object getData6() {
        return data6;
    }

    // 返回枚举对象
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

静态内部类写法——最应该掌握的一种写法!

这种写法的原理:依赖类初始化的时候是加锁的来保证线程之间不可见,从而达到线程安全。静态内部类在初始化的时候,依赖jvm的类加载过程中的初始化环节的加锁过程。

/**
 * 静态内部类,通过保证类初始化的时候是加锁的来进行线程间不可见,从而达到线程安全
 */
public class LazyStaticInnerClassSingleton {

    // 私有构造防止外界直接new对象
    private LazyStaticInnerClassSingleton(){}
    
    // 对外开放获取单例对象的方法
    public static LazyStaticInnerClassSingleton getInstance(){
        // 返回静态内部类中的单例对象
        return InnerClassSingleton.instance;
    }
    

    // 静态内部类中含有最终的单例对象。
    private static class InnerClassSingleton{
        private static LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton();
    }

}

双重检查机制——应该掌握的一种写法!

这种写法相比上面两个稍微复杂一点点,仅仅是一点点。但是知识点牵涉的不少!

原理:

  • synchronized加锁(如果是直接加到方法上面,就不用双重检查了)保证同一时间只有一个线程访问。
  • volatile可见性关键字。保证初始化的时候防止指令重排序。

代码:

/**
 * 双重检查
 */
public class LazyDoubleCheckSingleton {

    // volatile 保证 lazySingleton = new LazyDoubleCheckSingleton() 的操作,不会指令重排
    // 具体是因为在对象初始化的时候会有三个指令,分配内存地址,初始化,分配引用地址。这三个指令如果发生指令重排序,那可能就会出问题,具体哪里出问题,可以参考其他博客文章。这里不再赘述。
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton(){}

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

}

源码阅读

jdk

  • Runtime.java 饿汉式的单例模式

Spring

  • AbstractFactoryBean.java 这个类的getObject方法,会进行单例的判断。

Mybatis

  • ErrorContext.java