原创 帮你搞定单例模式是个啥

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

你可能会想单例不就是懒汉式饿汉式吗?谁还不会!
但是事实上可不是如此,懒汉式饿汉式只是基础,还有更高级、更更高级、更更更高级、更更更更高级的呦

懒汉式:第一次使用时才创建,不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

public class BaseLazy {
    private static BaseLazy instance  = null;

    /** 让构造函数为 private,这样该类就不会被实例化 */
    private BaseLazy(){}

    /** 使用时创建 */
    public static BaseLazy getInstance(){
        if(null == instance){
            instance = new BaseLazy();
        }
        return  instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20 ; i++) {
            new Thread(() ->{
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(BaseLazy.getInstance());
            }).start();
        }
    }
}

饿汉式:类加载时就初始化,浪费空间。没有加锁,执行效率会提高,这种方式比较常用,但容易产生垃圾对象。

public class BaseHungry {
    /** 直接创建 */
    private static BaseHungry instance = new BaseHungry();

    /** 让构造函数为 private,这样该类就不会被实例化 */
    private BaseHungry(){}

    public static  BaseHungry getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20 ; i++) {
            new Thread(() ->{
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(BaseHungry.getInstance());
            }).start();
        }
    }
}

双检锁/双重校验锁(DCL,即 double-checked locking):这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

public class VolatileDoubleCheckLock {

    /**
     * 关键点一:为啥 要加volatile?
     *
     * 1. 为了提高运行速度存在编译重排序与运行重排序,在遵循as-if-serial原则下调整指令顺序
     * 因为指令的调整,线程A读取instance的时候线程B可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的
     *
     * 2. 线程之间存在数据可见性问题
     * 线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中,也就是working memory
     * 这个时候多个线程访问同一个变量,其实就是访问自己的内部cache
     * 线程A把变量加载到自己的内部缓存cache中
     * 线程B修改变量后,即使重新写入主内存
     * 但是线程A不会重新从主内存加载变量,看到的还是自己cache中的变量
     * 因此会存在 数据不一致的问题
     *
     *
     * 而volatile修饰的变量不允许线程内部cache缓存和重排序
     *
     */
    private static volatile VolatileDoubleCheckLock instance = null;

    /** 让构造函数为 private,这样该类就不会被实例化 */
    private VolatileDoubleCheckLock(){}

    /**
     * 关键点二:为啥 二次判断 null ?
     *
     * 场景:
     * 多线程下调用 getInstance()时,两个线程同时判断 instance == null
     * 线程一获得锁进入 线程二等待
     * 如果没有 二次判断 instance == null
     * 则线程一完成后 线程二进入则会重复创建 instance
     */
    public static VolatileDoubleCheckLock getInstance(){
        if(null == instance){
            synchronized (VolatileDoubleCheckLock.class){
                if(null == instance){
                    instance = new VolatileDoubleCheckLock();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20 ; i++) {
            new Thread(() ->{
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(VolatileDoubleCheckLock.getInstance());
            }).start();
        }
    }
}

登记式/静态内部类:通过类加载的机制保证 instance 只被实例化一次,且是在调用getInstance()时被实例化,保证了线程安全以及懒加载

public class HolderSingleton {
    private static class Holder{
        public static HolderSingleton instance = new HolderSingleton();
    }

    /** 让构造函数为 private,这样该类就不会被实例化 */
    private HolderSingleton(){}

    public static HolderSingleton getInstance(){
        return Holder.instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20 ; i++) {
            new Thread(() ->{
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(HolderSingleton.getInstance());
            }).start();
        }
    }
}

枚举:这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。虽然还没有被广泛采用,但这是实现单例模式的最佳方法

public class EnumLazy {
    private EnumLazy(){}

    private enum EnumHolder{
        INSTANCE;
        private EnumLazy instance;
        EnumHolder(){
            instance = new EnumLazy();
        }
    }

    public static EnumLazy getInstance(){
        return EnumHolder.INSTANCE.instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 20 ; i++) {
            new Thread(() ->{
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(EnumLazy.getInstance());
            }).start();
        }
    }
}
本文为 Laysonx 原创 文章,转载无需和我联系,但请注明来自 李鑫的杂货铺 或 李鑫博客