方圆

单例模式

2019-11-10

  • 动机
    在软件系统中有些特殊的类,必须保证只有一个实例,才能保证逻辑正确性以及良好的效率。如何绕过常规的构造器,来提供一种机制保证一个类只有一个实例。

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

  • 懒汉模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Singleton1 {
    private static Singleton1 instance = null;
    private Singleton1() {
    }
    // 1、适用于单线程环境(不推荐)
    public static Singleton1 getInstanceA() {
    if (null == instance) {
    // 用到才构造,懒汉模式
    instance = new Singleton1();
    }
    return instance;
    }
    // 2、适用于多线程环境,但效率不高(不推荐)
    public static synchronized Singleton1 getInstanceB() {
    if (instance == null) {
    instance = new Singleton1();
    }
    return instance;
    }
    // 3、双重检查加锁,重排序会导致问题
    public static Singleton1 getInstanceC() {
    if (instance == null) {
    synchronized (Singleton1.class) {
    if (instance == null) {
    instance = new Singleton1();
    }
    }
    }
    return instance;
    }
    }

    实现3逻辑上正确,但是由于编译器指令重排导致不安全(某一线程可能拿到一个不等于nullptr但是没有执行构造函数的 instance ),在Java平台可以对 instance 加上 volatile 使得整个赋值过程不能reorder;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Singleton{
    private static class SingletonHolder{
    public static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton newInstance(){
    return SingletonHolder.instance;
    }
    }

    利用了类加载机制来保证只创建一个instance实例。与饿汉模式一样,不存在多线程并发的问题。
    在应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。
    也就是说这种方式可以同时保证延迟加载和线程安全。

  • reorder
    instance=new Singleton(); 这行代码逻辑上是先分配内存,再调用构造函数,再将内存地址复制给变量名;重排之后可能先申请内存,然后就复制给名称,最后再调用构造函数。

  • 饿汉模式
    eg. singleton in Spring-AOP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public final class GlobalAdvisorAdapterRegistry {
    private GlobalAdvisorAdapterRegistry() {
    }
    // 事先构造好,饿汉模式
    private static AdvisorAdapterRegistry instance = new DefaultAdvisorAdapterRegistry();
    public static AdvisorAdapterRegistry getInstance() {
    return instance;
    }
    static void reset() {
    instance = new DefaultAdvisorAdapterRegistry();
    }
    }
  • 总结

    1. 饿汉模式线程安全,但是一开始就初始化,没有用到时造成浪费,适合内存占用小且一定会使用的情况。
    2. 实现线程安全的singleton,注意检查双检查锁的正确实现。
    3. 不使用锁和双重检查,最好使用内部类的方式。
    4. 另外基于枚举实现单例能解决序列化的问题。
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章