重新讲讲单例模式和几种实现
一、什么讲单例模式
单例模式,最简单的理解是对象实例只有孤单的一份,不会重复创建实例。
这个模式已经很经典了,经典得我不再赘述理论,只给简单注释,毕竟教科书详尽太多。
解决 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();
}
}