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


    一、什么讲单例模式

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

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

    解决 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();
        }
    }
    
  • 相关阅读:
    语音激活检测(VAD)--前向神经网络方法(Alex)
    语音信号处理基础
    MySQL死锁系列-插入语句正常,但是没有插入成功
    关于wx.getProfile和wx.login获取解密数据偶发失败的原因
    指针访问数组元素
    libev 源码解析
    leveldb 源码--总体架构分析
    raft--分布式一致性协议
    使用springcloud gateway搭建网关(分流,限流,熔断)
    爬虫
  • 原文地址:https://www.cnblogs.com/hyry/p/16056069.html
Copyright © 2020-2023  润新知