Loading

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

一、什么讲单例模式

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

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

解决 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();
    }
}
posted @ 2022-03-25 18:09  寒烟濡雨  阅读(166)  评论(0编辑  收藏  举报

Loading