草庐IT

重新讲讲单例模式和几种实现

ColdSmog 2023-03-28 原文

一、什么讲单例模式

单例模式,最简单的理解是对象实例只有孤单的一份,不会重复创建实例。

这个模式已经很经典了,经典得我不再赘述理论,只给简单注释,毕竟教科书详尽太多。

解决 sonar RSPEC-2168 异味的时候,发现目前业界推荐的单例模式和教科书上的已经有了较大差异,双重锁定不再推荐,甚至业内认为的最优方案不在sonar的推荐里

于是提笔记录,顺带补充了自己对多线程单例的理解 。

二、经典的单线程单例

这个部分没有改动,简单而经典,大致源码如下

public final class SignUtil {

    /**
     * 需要保持单例的对象
     */
    private static Object object;

    /**
     * 只允许SignUtil.getInstance获取对象,也就是入口唯一
     */
    private SignUtil() {
    }

    /**
     * 对象的唯一出口 调用时才初始化(懒加载)
     * @return Object 确保单线程情况下这里出去就是初始化好的
     */
    public static Object getInstance() {
        if (null == object) {
            object = new Object();
        }
        return object;
    }

    /**
     * 内部函数也必须使用 getInstance这个入口
     */
    public static String getString() {
        return getInstance().toString();
    }
}

三、经典的双重锁定多线程单例 (JDK5-JDK7继续适用)

public final class SignUtil {

    /**
     * 需要保持单例的对象
     * 这里需要声明对象是易失的,因为object = new Object()不是一个原子操作,是被分拆为了实例化和初始化,一个申请空间,一个分配值
     * 那么就有可能出现 C在第三瞬间进入getInstance函数,发现null!=object,此时对象实例化了但没初始化就直接返回,是个高危操作
     */
    private volatile static Object object;

    /**
     * 只允许SignUtil.getInstance获取对象,也就是入口唯一
     */
    private SignUtil() {
    }

    /**
     * 对象的唯一出口
     *
     * @return Object 多线程情况下这里出去就是初始化好的
     */
    public static Object getInstance() {
        // 第0瞬间 A B 两个线程同时初始化,一看都是null嘛
        if (null == object) {
            // 第1瞬间 A B都进来了,因为不能重复初始化,所以被synchronized锁约束开始竞争.
            // A 赢了SignUtil的对象锁,B 只能等着
            synchronized (SignUtil.class) {
                // 这里为什么不直接object = new Object()呢?
                // 因为B还等着呢,直接初始化就拦不住B再来一次初始化了.
                if (null == object) {
                    // 第2瞬间, A终于初始化成功,且B不会重新初始化了.
                    object = new Object();
                    // 第3瞬间,因为object被volatile约束了,可以视为原子操作,补上最后一个漏洞,成功返回。
                }
            }
        }
        return object;
    }

    /**
     * 内部函数也必须使用 getInstance这个入口
     */
    public static String getString() {
        return getInstance().toString();
    }
}

四、 JDK8 以后的多线程单例

可以看到,三的要点太多了,很经典的双重锁定,但是不够简单优雅。目前更推荐下面两种格式

4.1 synchronized变为轻量级锁

JDK8 带来的一个特性之一即是synchronized关键字,从原来的monitor重量级锁,转变成了由偏向锁进行逐级升级到重量级锁。换句话说,使用synchronized的代价被降低了,我们可以将上面的函数进行一个改进,让它保持简单和优雅。

但是代价依旧存在,以下适合并发冲突不严重的项目。

public final class SignUtil {

    /**
     * 需要保持单例的对象
     */
    private static Object object;

    /**
     * 只允许SignUtil.getInstance获取对象,也就是入口唯一
     */
    private SignUtil() {
    }

    /**
     * 对象的唯一出口 是的,仅比单线程版多了一个synchronized
     * @return Object 由于synchronized,同一瞬间只能有一个对象进行获取实例
     */
    public static synchronized Object getInstance() {
        if (null == object) {
            object = new Object();
        }
        return object;
    }

    /**
     * 内部函数也必须使用 getInstance这个入口
     */
    public static String getString() {
        return getInstance().toString();
    }
}

4.2 利用静态内部类的初始化特性

很巧妙地利用了jvm的类加载机制。那就是静态内部类的延迟加载性完成单例。

public final class SignUtil {

    /**
     * 利用jvm的初始化规则 静态内部类的静态内部对象,只有在调用时才对静态类开始初始化,
     * 类的初始化过程是线程安全的,所以也只有一个线程能进行初始化
     */
    private static class Node {
        /**
         * 在读写调用时才真正初始化,也就是懒加载
         */
        private static final Object object = new Object();
    }

    /**
     * 只允许SignUtil.getInstance获取对象,也就是入口唯一
     */
    private SignUtil() {
    }

    /**
     * 不再是对象的唯一出口,其他地方也只要读写都能完成初始化
     *
     * @return Object 调用时,会触发内部静态类的初始化,返回时,初始化已完成
     */
    public static Object getInstance() {
        return Node.object;
    }

    /**
     * 内部函数终于不用再依赖 getInstance这个入口
     */
    public static String getString() {
        return Node.object.toString();
    }
}

五、 有没有办法让单例模式不单例?

听起来很魔鬼,但实际上,上述的多线程程单例都有两个共同的缺陷可以做到:a 反射Constructor::setAccessible将私有构造函数改为公有函数 b.序列化时还是会返回多个实例。

解决方法为改造构造函数和申明readResolve函数,参考如下,解决方案是通用的。

public final class SignUtil {

    private static volatile boolean init = false;

    private static class Node {
        private static final Object object = new Object();
    }

    /**
     * 添加一个volatile的变量去判断,防止反射初始化
     * 第二次初始化会抛出类强制转换异常 当然你也可以用其他运行时异常
     */
    private SignUtil() {
        if (!init) {
            init = true;
        } else {
            throw new ClassCastException();
        }
    }

    public static Object getInstance() {
        return Node.object;
    }

    public static String getString() {
        return Node.object.toString();
    }

    /**
     * 反序列化时直接返回单例的对象,这么写的原因在 ObjectInputStream::readUnshared里
     */
    private Object readResolve() {
        return Node.object;
    }
}

六、枚举单例

6.1 单元素枚举单例

和4.2一样,《Effective Java 》找到了另一种利用jvm类加载机制实现单例的方法:单元素枚举单例。
这里有几个前提:

  • Enum禁用了默认序列化。Enum::readObject、Enum::readObjectNoData约束了枚举对象的默认反序列化,保证序列化安全
  • Enum提供了自己的序列化。Enum::toString 返回的是属性名称name,再通过Enum::valueOf把name转回实例,保证了枚举不会被“退货”(这个直译了,大概是final且不会被clone的意思)。
  • 这里说一下valueOf的底层是Class::enumConstantDirectory,作用是调用时,生产一个Map<name, 枚举>的映射,而这个map很像单线程单例模式,但他不是静态共享变量,所以是线程安全的,

不得不说,单元素枚举的确成功避免了重重的繁琐,但代价是没有了懒加载的特性,变成了饿汉模式

public enum SignUtil {
    /**
     * 从javap的反编译结果看,会变成一个类公开的静态变量,也就是饿汉模式
     * public static final SignUtil INSTANCE = new SignUtil();
     * 也就是会在加载类时直接初始化INSTANCE对象,而object对象是在构造时作为内部变量初始化,而构造函数是由jvm保证的
     */
    INSTANCE;

    /**
     * 由于INSTANCE单例,所以object才是单例的
     */
    private final Object object = new Object();

    public Object getInstance() {
        return object;
    }

    public String getString() {
        return object.toString();
    }

}

补一下javap反编译后的结果

public final class SignUtil extends java.lang.Enum<SignUtil> {
  public static final SignUtil INSTANCE;
  private final java.lang.Object object;
  private static final SignUtil[] $VALUES;
  public static SignUtil[] values();
  public static SignUtil valueOf(java.lang.String);
  private SignUtil(java.lang.Object);
  public java.lang.Object getInstance();
  public java.lang.String getString();
  static {};
}

6.2 多元素枚举的单例呢?

由于多元素枚举的构造函数可以被反射修改成公用函数并设置object,但由于INSTANCE和object都是final约束的,所以修改就会报错,以此保证了单例性。
所以按照理解 多元素枚举也能完成单例,只是适用场景偏少

public enum SignUtil {
	/*
	 * 对的,唯一的区别就是由无参变成了有参构造,本质是不变的饿汉
	 * public static final SignUtil INSTANCE = new SignUtil(new Object());
	 */
    INSTANCE(new Object()),
    OTHER(new Object());

    private final Object object;

    private SignUtil(Object object) {
        this.object = object;
    }

    public Object getInstance() {
        return this.object;
    }

    public String getString() {
        return this.object.toString();
    }
}

有关重新讲讲单例模式和几种实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby - 在 Ruby 中重新分配常量时抛出异常? - 2

    我早就知道Ruby中的“常量”(即大写的变量名)不是真正常量。与其他编程语言一样,对对象的引用是唯一存储在变量/常量中的东西。(侧边栏:Ruby确实具有“卡住”引用对象不被修改的功能,据我所知,许多其他语言都没有提供这种功能。)所以这是我的问题:当您将一个值重新分配给常量时,您会收到如下警告:>>FOO='bar'=>"bar">>FOO='baz'(irb):2:warning:alreadyinitializedconstantFOO=>"baz"有没有办法强制Ruby抛出异常而不是打印警告?很难弄清楚为什么有时会发生重新分配。 最佳答案

  7. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐