跳转至

单例模式

图片的样式

单例模式的核心目的就是控制对象的创建,以达到特定对象的唯一性,实际业务中有很多都是要保证对象的不变性, 其中Spring Boot框架的容器思想,将对象放入容器进行管理,这个对象就是单例对象。 具体实现上,最经典的就是懒汉和饿汉两种,懒汉就是你需要使用到了才创建,饿汉就是无论你用不用的到都先创建。 这其实很具有计算机的思想,在很多其他地方都有应用。

➊ 懒汉式

懒汉式就是使用到了固定对象在创建它,按照我的理解就是避免创建了对象,却长时间不使用而造成计算机资源的浪费, 但是代价就是,你要用就要自己创建,这个是要时间的,尽管在现代计算机这个时间很短。而且这个方式有一个最大的问题: 使用时要考虑多线程环境,不然不安全,会创建多个对象而破坏了对象的唯一性。

要控制对象的创建,第一步就是将构造方法设置成private,否则没有任何一种方式叫作单例。第二步就是要有一个获取对象的方法, 且是public的,示例代码中的getInstance()就是,通过判断对象是否已经存在,来保证对象的唯一性。

但是它在多线程环境下是不安全的,多个线程同时调用getInstance()方法,if判断这个条件是不安全的,也就容易破坏单一性,适用方法就有限制。

public class SingletonOfLazy {
    private static SingletonOfLazy INSTANCE = null;

    private SingletonOfLazy() {
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象无法重复创建");
        }
    }

    public static SingletonOfLazy getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonOfLazy();
        }
        return INSTANCE;
    }
}

➋ 饿汉式

饿汉式就是无论你用不用的到,我都先创建对象,你要用就直接用,比喻使用者饿了,不用等待创建过程直接“吃”。

首先它在类加载时就创建实例,同时Java的类加载机制确保了类是线程安全的;
然后私有构造函数,防止外部实例化;
最后提供全局访问点以获取对象。

public class SingletonOfHungry {
    private static final SingletonOfHungry INSTANCE = new SingletonOfHungry();

    private SingletonOfHungry() {
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象无法重复创建");
        }
    }

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

➌ 双检锁懒汉式

前面两种,一种懒汉式,用到才创建,避免了对象创建而长时间不使用的问题,但是它有线程安全问题; 一种饿汉式,直接创建对象,将创建对象的资源消耗放到类加载时,也避免了线程安全问题,但是对象一直在内存里而不使用, 会有浪费。两者各有优劣。

双检锁懒汉式是正对懒汉式线程安全问题的一种改良,很好的体现了对于锁🔒的理解。使用起来很简单,理解里面的意思就难了, 比如两次判断的含义,也算是面试会考的题目了。

public class SingletonOfDCLLazy {
    /**
     * 用volatile主要是为了保证变量的有序性,因为CPU会自己优化代码执行顺序,
     * 单线程下无所谓,多线程下会有问题,而volatile可以防止指令重排,它会在赋值操作后加上一个内存屏障,
     * 这样就可以让之前的操作不允许越过这个赋值操作来执行,而防止CPU的自动优化.
     */
    private static volatile SingletonOfDCLLazy INSTANCE = null;

    private SingletonOfDCLLazy() {
        //构造器必须私有  不然直接new就可以创建
    }

    public static SingletonOfDCLLazy getInstance() {
        // 1️⃣第一次判断,假设会有好多线程,如果doubleLock没有被实例化,那么就会到下一步获取锁,只有一个能获取到,
        // 如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程
        if (INSTANCE == null) {
            synchronized (SingletonOfDCLLazy.class) {
                // 2️⃣第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入,然后判断doubleLock是null,
                // 他就实例化了doubleLock,然后他出了锁,
                // 这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new DoubleLock(),
                // 再创建一个实例,所以为了防止这种情况,需要第二次判断
                if (INSTANCE == null) {
                    // 下面这句代码其实分为三步:
                    // 1.开辟内存分配给这个对象
                    // 2.初始化对象
                    // 3.将内存地址赋给虚拟机栈内存中的doubleLock变量
                    // 注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题
                    // 假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给doubleLock
                    // 然后恰好这时候有另一个线程执行了第一个判断if(doubleLock == null),然后就会发现doubleLock指向了一个内存地址
                    // 这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序
                    INSTANCE = new SingletonOfDCLLazy();
                }
            }
        }
        return INSTANCE;
    }
}

➍ 内部类单例

这个模式使自己比较喜欢的,没有那么多弯弯绕,利用Java的静态内部类机制来实现懒加载和线程安全,用起来也方便。

public class SingletonOfInnerClass {
    private SingletonOfInnerClass() {
    }

    private static class Holder {
        static SingletonOfInnerClass INSTANCE = new SingletonOfInnerClass();
    }

    public static SingletonOfInnerClass getInstance() {
        return Holder.INSTANCE;
    }
}

➎ 枚举单例

Java的枚举类型天然支持单例模式,并且是线程安全的,就是使用范围小,只能说算是,但是你要写一个单例, 枚举肯定不在考虑范围,枚举的设计就不是为单例而生,只是“凑巧”它天然是单例。

public enum SingletonOfHungryEnum {
    INSTANCE;

    // 其他方法
    public void someMethod() {
        // 方法实现
    }
}