【一起学系列】之单例模式:只推荐三种~

时间:2022-07-22
本文章向大家介绍【一起学系列】之单例模式:只推荐三种~,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

别名:单件模式

单例模式的诞生

【开发】:老大,为什么我保存配置信息,每次都和我预期的不一样啊,总是会覆盖?

【BOSS】:哈?我来看看。

【BOSS】:你每次使用的时候都会new一个新的配置对象吗?

【开发】:对啊,有什么问题?

【BOSS】:那肯定不对啊,像这种配置信息,全局只应该有一个,不然会互相影响!

HeadFirst 核心代码

饿汉型 (不推荐)

public class HazardousTypeSingleton {

    private static final App APP = new App();

    // 私有构造方法
    private HazardousTypeSingleton () {}

    // 类加载时已初始化,不会有多线程的问题
    static App getInstance() {
        System.out.println("APP - 饿汉型模式");
        return APP;
    }
}

❝名字由来:因为随着类加载而加载,显得很“急迫”,所以称之为饿汉型 ❞

**评价:**这样的写法和全局变量没有本质的区别,不推荐

懒汉型 (不推荐)

public class LazyTypeSingleton {

    private LazyTypeSingleton () {}

    // 静态私用成员,没有初始化
    private static App intance = null;

    /***
     * 直接加synchronized关键字
     */
    synchronized  static App getIntance () {
        System.out.println("APP - 懒汉型模式");
        if (null == intance) {
            return new App();
        }
        return intance;
    }
}

❝名字由来:调用时才加载,因此称之为懒汉型 ❞

**评价:**这样写有延迟加载的功能,但是加了一个synchronized大锁,因此多线程环境下效率较低

懒汉型之双重锁校验 ?

public class LazyTypeSingleton {

    // volatile关键字修饰,防止指令重排
    private volatile static App app = null;

    /***
     * Double Check Lock(DCL) 双重锁校验
     */
    static App getInstanceByDCL () {
        if (null == app) {
            synchronized (LazyTypeSingleton.class) {
                if (null == app) {
                    System.out.println("APP - 饿汉模式DCL 双重锁校验");
                    return new App();
                }
            }
        }
        return app;
    }
}

❝注意volatile关键字起到的作用,详情请见:https://juejin.im/post/5ebadd9df265da7bda414c20 ❞

**评价:**比较推荐的写法,可以保证线程安全,同时具备延时加载的效果

静态内部类方式?

public class InnterTypeSingleton {

    private InnterTypeSingleton(){
        throw new IllegalStateException();
    }

    // 静态内部类方式,类似饿汉保证天然的线程安全
    private static class SingletonHolder{
        private final static App app = new App();
    }

    static App getInstance(){
        System.out.println("APP - 静态内部类方式(Holder)");
        return SingletonHolder.app;
    }
}

**评价:**线程安全,调用效率高,可以延时加载

静态内部类之神奇的报错

public class InnterTypeSingletonError {

    private InnterTypeSingletonError(){
        System.out.println(5 / 0);
    }

    private static class SingletonHolder{
        private final static InnterTypeSingletonError app = new InnterTypeSingletonError();
    }

    static InnterTypeSingletonError getInstance(){
        System.out.println("APP - 静态内部类方式(Holder)");
        return SingletonHolder.app;
    }

    public static void main(String[] args){
        try {
            InnterTypeSingletonError.getInstance();
        } catch (Throwable t) {
            t.printStackTrace();
        }

        try {
            InnterTypeSingletonError.getInstance();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

注意看上文中代码块:

private InnterTypeSingletonError(){
 System.out.println(5 / 0);
}

这样一定会出错,运行结果报错信息如下:

APP - 静态内部类方式(Holder)
APP - 静态内部类方式(Holder)
java.lang.ExceptionInInitializerError
	at com.design.singleton.InnterTypeSingletonError.getInstance(InnterTypeSingletonError.java:23)
	at com.design.singleton.InnterTypeSingletonError.main(InnterTypeSingletonError.java:28)
Caused by: java.lang.ArithmeticException: / by zero
	at com.design.singleton.InnterTypeSingletonError.<init>(InnterTypeSingletonError.java:14)
	at com.design.singleton.InnterTypeSingletonError.<init>(InnterTypeSingletonError.java:11)
	at com.design.singleton.InnterTypeSingletonError$SingletonHolder.<clinit>(InnterTypeSingletonError.java:18)
	... 2 more
java.lang.NoClassDefFoundError: Could not initialize class com.design.singleton.InnterTypeSingletonError$SingletonHolder
	at com.design.singleton.InnterTypeSingletonError.getInstance(InnterTypeSingletonError.java:23)
	at com.design.singleton.InnterTypeSingletonError.main(InnterTypeSingletonError.java:34)

可以发现它第一次报错是正常的异常,第二次如果再报错就是Could not initialize class ,为什么呢?

因为:「类加载时静态变量只会在第一次加载时,进行初始化,此后不管成不成功,都不会进行第二次初始化了」

所以使用的时候需要注意

枚举方式?

public enum  EnumSingleton {
    /***
     * APP对象
     */
    APP;

    private App app;

    EnumSingleton() {
        app = new App();
    }

    public App getInstance() {
        System.out.println("**************************");
        System.out.println("APP - 枚举方式");
        return app;
    }
}

**评价:**线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用

什么场景适用

在以下情况可以使用单例模式:

  • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时
  • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时

Code/生活中的实际应用

在很多项目中的数据库连接池,亦或是配置中心,配置文件对象等等,非常常见~

总结

感谢Java3Y的文章:三歪写Bug写哭了,从中学习到了内部类使用时的神器报错

单例模式使用的场景其实固化,任何需要单一对象工作时的场景都可以使用单例模式,同时只推荐以下三种写法:

  • 基于双重锁校验的懒汉型
  • 静态内部类方式
  • 枚举方式

相关代码链接

GitHub地址:https://github.com/kkzhilu/Kerwin-DesignPattern

  • 兼顾了《HeadFirst》以及《GOF》两本经典书籍中的案例
  • 提供了友好的阅读指导