创建型模式——单例模式

102 阅读6分钟

前言

本文在实现标题中包括了一些jvm、多线程的小知识,如果还没学的部分可以先跳,也可以去了解。

什么是单例模式

单例模式,是一种常见的创建型设计模式。单例模式将构造器私有化(防止其他对象实例化该对象),并且需要创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例,这个应该是参考ThreadLocal),并提供给其他对象一个能获取自己的唯一对象的方法。

适用场景

  1. 频繁IO资源的对象。 比如你的MySQL配置文件,经典的四个属性url、username、password、driver,通过properties将属性读取到你的配置类,而读取properties文件到配置类是一个IO过程(从磁盘文件中读取,写入内存),挺耗CPU资源的。

  2. 创建对象时耗时过多或者耗资源过多,但是又经常用到的共享资源对象。(数据库连接池在连接前需要去配置类中拿数据,而你这个配置类的数据又很常用,但这个常用是共享资源哈。)

  3. 频繁实例化和销毁,也就是频繁的new对象,并且要求对象不多变,要多变也必须整个系统共享,可以考虑单例模式。

分类

总的分成两大类,饿汉式和懒汉式
对比来看:

  1. 饿汉式都是线程安全的,而懒汉式需要看情况
  2. 懒汉式可以起到一个懒加载的作用,也就是说到需要使用的时候才实例化,这样的好处就是节省空间资源。而饿汉式是在类加载阶段就已经将实例对象创建了。 总结一句话来说,饿汉式是在类加载时就实例化了,懒汉式是在需要使用类时再实例化。

饿汉式(都是安全的)

在类常量直接实例化/静态代码块实例化(其实都是一样)

饿汉式即单例对象在类加载时实例化,并赋值给static变量(这个类加载指的是类的生命周期五个过程中的加载、链接、初始化前三个过程,而不是class文件的加载,以下也是,而关于类什么时候加载,并且类加载阶段为什么是线程安全的,可以去看一下jvm),也就是说在staitc变量上直接实例化,或者在静态代码块实例化再赋值。static变量在类加载时就以及存在类中了,以及静态代码块也会在类加载时进行执行。

public class SingletonObject{
    //实现分为三步
    //1.构造器私有化
    private SingletonObject(){
    }
    
    //2.声明成员变量
    private static final INSTANCE = new SingletonObject();
    
    //如果是静态代码块的情况,与上面的第二步,二选一
    private static final INSTANCE = null;
    static{
        INSTANCE = new SingletonObject();
    }
    
    //3.提供获取单例对象的方法
    public static SingletonObject getInstance(){
        return INSTANCE;
    }
    
}

枚举

枚举声明的常量也是在枚举类加载阶段就进行实例化了

public Enum SingletonObject{
    //有参数时如下,无参数时把括号去掉就好了
    INSTANCE(1);
    private final int value;
    private SinletonObject(int value){
        this.value = value;
    }
    
}

懒汉式

懒汉式又分为线程安全线程不安全

线程不安全

以下代码是在getInstance方法中进行实例化,判断类变量是否已赋值,没有赋值就实例化,赋值了则返回类变量。不过该用法是线程不安全的,原因是:假设两个线程同时进入了getInstance方法中的if语句判断INSTANCE为null,则两线程会接下去执行INSTANCE = new SingletonObject();语句,假设有一个线程比较快执行,然后返回实例给调用者,另一个线程还在实例化语句,等慢的线程实例化完成后返回,那么此时两线程执行该方法得到的返回值就会不一致。因此线程不安全,只适用于单线程环境。

public class SingletonObject{
    //还是那三步
    //只不过懒汉式在类变量上不立即实例化
    private static final INSTANCE = null;
    
    private SingletonObject(){}
    
    public staitc SingletonObject getInstance(){
        if(INSTANCE == null){
            INSTANCE = new SingletonObject();
        }
        return INSTANCE;
    }
    
}

线程安全

同步锁

本方法是使用synchronized修饰静态方法或者修饰该类的同步代码块,而synchronized方法修饰静态方法,在访问该方法时,先判断是否拿到该类的锁,没有的话阻塞,不让其他线程访问该类的该方法,直到方法执行完毕才释放锁。修饰同步代码块也是差不多的。与上一个代码区别也只是一个synchronized修饰符,该方法线程安全,只不过每次一进入这个方法就要将该方法锁住,不让其他线程访问,得等到锁释放,其他线程才能访问,这样做是线程安全了但是效率很低,还有没有改进方案呢?答案是:双重检查锁或静态内部类。

public class SingletonObject{
    //还是那三步
    //只不过懒汉式在类变量上不立即实例化
    private static final INSTANCE = null;
    
    private SingletonObject(){}
    
    public staitc synchronized SingletonObject getInstance(){
        if(INSTANCE == null){
            INSTANCE = new SingletonObject();
        }
        return INSTANCE;
    }
    
}

双重检查锁(DCK)     重点

双重检查锁顾名思义就是双重判断+锁,为什么要这样做呢?还是假设两个线程都进入了该方法,如果一上来就锁的话明显效率低,那我先判断这个类变量是否为空,不为空就返回,为空就进去实例化,这样想是否不会因为锁而降低了效率?接下来我们再进行实例化,如果两个线程执行了实例化,明显最终返回的肯定不同,那么我们就需要采取措施,让他们只能一个执行实例化,解决方案就是先锁住这个类,再判断是否类变量为空。如果两个线程都要这个同步代码块,先进先锁,后进的等待,先进去的那一个先进行实例化,实例化完毕后,从同步代码块中退出并释放了锁,执行返回,而另一个在等待的线程此时发现锁已经释放了,也进入了代码块,显然不能直接实例化吧,因为前一个线程已经进行实例化了,那么我们得进入之后得再判断一次是否类变量为空,不为空,直接返回实例。
在下面的代码中还有一个重要的东西,是在变量上修饰volatile,该修饰符是为了禁止该变量在执行指令时指令重排。该修饰符的作用,什么是指令重排,可以去多线程编程了解一下。

public class SingletonObject{
    //还是那三步
    //只不过懒汉式在类变量上不立即实例化
    private static final volatile INSTANCE = null;
    
    private SingletonObject(){}
    
    public staitc SingletonObject getInstance(){
        if(INSTANCE == null){
            synchronized(SingletonObject.class){
                if(INSTANCE == null){
                    INSTANCE = new SingletonObject();
                }
            }
            
        }
        return INSTANCE;
    }
    
}

静态内部类
静态内部类单例模式是线程安全的,并且有懒加载的效果。因为该静态内部类只有在调用静态内部类时才加载,所以可以起到懒加载的作用,而加载过程是线程安全的。

public class SingletonObject{
    //还是那三步
    //只不过懒汉式在类变量上不立即实例化
    private static final volatile INSTANCE = null;
    
    private SingletonObject(){}
    
    public staitc SingletonObject getInstance(){
        return InnerSingletonObject.INSTANCE;
    }
    private static class InnerSingletonObject{
        private final static SingletonObject INSTANCE = new SingletonObject();
    }
    
}