Skip to content
CodingDiary
返回

设计模式之创建型设计模式-单例模式

编辑页面
导读

单例模式怎么写才安全?7 种实现方式全解析!饿汉式、懒汉式、双重检查锁、静态内部类、枚举、容器式、ThreadLocal——哪种最推荐?更关键的是:反序列化和反射都能破坏单例!本文深入 ObjectInputStream 源码,揭秘 readResolve 方法的原理。

1. 模式简介

确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问入口点;隐藏所有的构造方法;属于创建型设计模式

2. 实现方式

2.1. 饿汉式单例

先将实例创建出来,用的时候直接拿

2.1.1. 属性初始化

/**
 * <p>
 * 饿汉式单例,在属性初始化
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 11:32
 */
public class EagerSingleton1 {
    private final static EagerSingleton1 INSTANCE = new EagerSingleton1();

    /**
     * 私有化构造方法
     */
    private EagerSingleton1() {
    }

    /**
     * 提供全局访问入口
     */
    public static EagerSingleton1 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + EagerSingleton1.getInstance());
        }, 10, 5);
    }

}

2.1.2. 静态代码块初始化

/**
 * <p>
 * 饿汉式单例,在静态代码块初始化
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 11:46
 */
public class EagerSingleton2 {
    /**
     * 此处需要设置为 {@code final} 的,防止被后续赋值!
     */
    private final static EagerSingleton2 INSTANCE;

    static {
        INSTANCE = new EagerSingleton2();
    }

    /**
     * 私有化构造方法
     */
    private EagerSingleton2() {
    }

    /**
     * 提供全局访问入口
     */
    public static EagerSingleton2 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + EagerSingleton2.getInstance());
        }, 10, 5);
    }
}

2.2. 懒汉式单例

用的时候再去创建

2.2.1. 简单实现

/**
 * <p>
 * 懒汉式单例,简单实现
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:15
 */
public class LazySingletonSimple {
    private static LazySingletonSimple INSTANCE = null;

    private LazySingletonSimple() {
    }

    /**
     * 此处需要加 {@code synchronized},保证线程安全
     */
    public synchronized static LazySingletonSimple getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingletonSimple();
        }
        return INSTANCE;
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + LazySingletonSimple.getInstance());
        }, 10, 5);
    }
}

2.2.2. 双重检查锁实现

/**
 * <p>
 * 懒汉式单例,双重检查锁
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:19
 */
public class LazySingletonDoubleCheck {
    /**
     * 添加 {@code volatile} 解决底层 CPU 指令重排的问题
     */
    private volatile static LazySingletonDoubleCheck INSTANCE = null;

    private LazySingletonDoubleCheck() {
    }

    public static LazySingletonDoubleCheck getInstance() {
        if (INSTANCE == null) {
            synchronized (LazySingletonDoubleCheck.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LazySingletonDoubleCheck();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + LazySingletonDoubleCheck.getInstance());
        }, 10, 5);
    }
}

2.2.3. 静态内部类实现(推荐写法

/**
 * <p>
 * 懒汉式单例,内部类写法,{@code 推荐写法之一}
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:24
 */
public class LazySingletonInnerClass {
    private LazySingletonInnerClass() {
        // 反射获取类的构造方法,通过newInstance() 获取对象会存在单例被破坏的问题
        // 添加以下代码解决,强制不允许构建
        if (SingletonHolder.INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static LazySingletonInnerClass getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private final static LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 普通方式获取
        LazySingletonInnerClass instance1 = LazySingletonInnerClass.getInstance();

        // 反射方式获取
        Class<LazySingletonInnerClass> clazz = LazySingletonInnerClass.class;
        Constructor<LazySingletonInnerClass> constructor = clazz.getDeclaredConstructor(null);
        // 设置访问级别,因为是private的
        constructor.setAccessible(true);
        LazySingletonInnerClass instance2 = constructor.newInstance();

        System.out.println("instance1 = " + instance1);
        System.out.println("instance2 = " + instance2);
    }
}

2.3. 注册式单例

将每一个实例都缓存到统一的容器中,使用唯一标识获取实例

2.3.1. 枚举式单例(推荐写法

/**
 * <p>
 * 注册式单例,枚举实现,{@code 推荐写法之一}
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:35
 */
public enum RegisterSingletonEnum {
    /**
     * 单例
     */
    INSTANCE;

    public static RegisterSingletonEnum getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonEnum.getInstance());
        }, 10, 5);
    }
}

2.3.2. 容器式单例

2.3.2.1. Spring式实现
/**
 * <p>
 * 注册式单例,容器实现,Spring 容器的实现
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:38
 */
public class RegisterSingletonContainer {
    private static final Map<String, Object> BEAN_CACHE = new ConcurrentHashMap<>();

    private RegisterSingletonContainer() {
    }

    public static Object getInstance(String className) {
        synchronized (BEAN_CACHE) {
            // 判断缓存是否存在
            if (!BEAN_CACHE.containsKey(className)) {
                // 构建对象,放在缓存
                Class<?> aClass = null;
                try {
                    aClass = Class.forName(className);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                BEAN_CACHE.put(className, aClass);
                return aClass;
            } else {
                return BEAN_CACHE.get(className);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrentExecutor.execute(() -> {
            Object instance = RegisterSingletonContainer.getInstance("com.xkcoding.design.pattern.creational.singleton.register.RegisterSingletonContainer");
            System.out.println(System.currentTimeMillis() + " :: " + instance);
        }, 10, 5);
    }
}
2.3.2.2. ThreadLocal式单例

ThreadLocal式单例其实也属于 容器式单例,伪线程安全,保证线程内部全部唯一,同一线程内线程安全

/**
 * <p>
 * 注册式单例,ThreadLocal实现,伪线程安全,保证线程内部全部唯一,同一线程内线程安全
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:55
 */
public class RegisterSingletonThreadLocal {
    private static final ThreadLocal<RegisterSingletonThreadLocal> INSTANCE = ThreadLocal.withInitial(RegisterSingletonThreadLocal::new);

    private RegisterSingletonThreadLocal() {
    }

    public static RegisterSingletonThreadLocal getInstance() {
        return INSTANCE.get();
    }

    /**
     * 测试可见,同一线程内单例,不同线程间非单例
     */
    public static void main(String[] args) throws InterruptedException {
        System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonThreadLocal.getInstance());
        System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonThreadLocal.getInstance());
        System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonThreadLocal.getInstance());
        System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonThreadLocal.getInstance());

        ConcurrentExecutor.execute(() -> {
            System.out.println("线程号: " + Thread.currentThread().getName() + "" + RegisterSingletonThreadLocal.getInstance());
        }, 10, 5);
    }
}

3. 问题点

3.1. 反序列化破坏单例的情况

测试代码:

/**
 * <p>
 * 饿汉式单例,测试反序列化被破坏的情况
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 20:11
 */
public class EagerSingleton3 implements Serializable {
    private final static EagerSingleton3 INSTANCE = new EagerSingleton3();

    private EagerSingleton3() {
    }

    public static EagerSingleton3 getInstance() {
        return INSTANCE;
    }

    /**
     * 重写 {@code readResolve} 方法,可解决反序列化单例破坏的场景
     */
    private Object readResolve() {
        return INSTANCE;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 正常获取实例
        EagerSingleton3 instance1 = EagerSingleton3.getInstance();

        // 通过序列化获取
        EagerSingleton3 instance2 = null;

        // 将 instance1 写出到文件
        FileOutputStream fos = new FileOutputStream("EagerSingleton3.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance1);
        oos.flush();
        oos.close();

        // 读取文件到 instance2
        FileInputStream fis = new FileInputStream("EagerSingleton3.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        instance2 = (EagerSingleton3) ois.readObject();
        ois.close();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    }
}

其实上面的代码已经给出答案了,重写 readResolve() 方法,将 INSTANCE 返回即可解决。

为什么重写 readResolve() 方法,将 INSTANCE 返回就可以解决这个单例被破坏的问题呢?跟一波源码看看 ~

// 1. 首先得明确 原因应该是反序列化为对象的时候 导致单例失败的
//    所以跟踪一波 ois.readObject() 源码 -> java.io.ObjectInputStream#readObject

// 2. debug 可以发现,执行了 readObject0(false) 返回对象
Object obj = readObject0(false);

// 3. 继续跟踪 readObject0() 的源码 -> java.io.ObjectInputStream#readObject0

// 省略部分代码,只看下面的 switch 部分代码
switch (tc) {
    case TC_NULL:
        return readNull();

    case TC_REFERENCE:
        return readHandle(unshared);

    case TC_CLASS:
        return readClass(unshared);

    case TC_CLASSDESC:
    case TC_PROXYCLASSDESC:
        return readClassDesc(unshared);

    case TC_STRING:
    case TC_LONGSTRING:
        return checkResolve(readString(unshared));

    case TC_ARRAY:
        return checkResolve(readArray(unshared));

    case TC_ENUM:
        return checkResolve(readEnum(unshared));

    case TC_OBJECT:
        return checkResolve(readOrdinaryObject(unshared));

    case TC_EXCEPTION:
        IOException ex = readFatalException();
        throw new WriteAbortedException("writing aborted", ex);

    case TC_BLOCKDATA:
    case TC_BLOCKDATALONG:
        if (oldMode) {
            bin.setBlockDataMode(true);
            bin.peek();             // force header read
            throw new OptionalDataException(
                    bin.currentBlockRemaining());
        } else {
            throw new StreamCorruptedException(
                    "unexpected block data");
        }

    case TC_ENDBLOCKDATA:
        if (oldMode) {
            throw new OptionalDataException(true);
        } else {
            throw new StreamCorruptedException(
                    "unexpected end of block data");
        }

    default:
        throw new StreamCorruptedException(
                String.format("invalid type code: %02X", tc));
}

// debug可以发现反序列化的时候走的是
case TC_OBJECT:  return checkResolve(readOrdinaryObject(unshared));

// 4. 因此,继续跟踪源码 readOrdinaryObject(unshared) -> java.io.ObjectInputStream#readOrdinaryObject

// 省略部分代码
obj = desc.isInstantiable() ? desc.newInstance() : null;
// 这段代码里 desc.isInstantiable() 主要是判断类是否可以被序列化和反序列化,同时是否提供一个无参的构造方法
// 这里显然为 true, 所以 obj = desc.newInstance(),这就意味着已经被新创建了一个对象

// 5. 上面只是解释了为什么反序列化会破坏单例的情况,下面我们看看怎么解决
// 接着看 java.io.ObjectInputStream#readOrdinaryObject 这个方法

// 省略部分代码
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        // Filter the replacement object
        if (rep != null) {
            if (rep.getClass().isArray()) {
                filterCheck(rep.getClass(), Array.getLength(rep));
            } else {
                filterCheck(rep.getClass(), -1);
            }
        }
        handles.setObject(passHandle, obj = rep);
    }
}
// 这里存在一个 if 的条件判断,desc.hasReadResolveMethod()
// 满足条件的时候,Object rep = desc.invokeReadResolve(obj);
// 如果 rep != obj 的时候呢,就把 rep 赋值给 obj, handles.setObject(passHandle, obj = rep);

// 6. 看到这里,我们就得出一个结论:
// 如果要是仍然保证单例,我们只要让 rep 返回的时候等于原先的对象覆盖给 obj 就可以实现单例了

// 7. 所以这里的关键就是 desc.hasReadResolveMethod() 和 desc.invokeReadResolve(obj) 这两个方法了

// 7.1. 那我们首先来看看 desc.hasReadResolveMethod() 这个方法
boolean hasReadResolveMethod() {
    requireInitialized();
    return (readResolveMethod != null);
}

// 这里就只是判断 readResolveMethod 这个是否为 null,那我们就来看看 readResolveMethod 这到底是个啥吧
/** class-defined readResolve method, or null if none */
private Method readResolveMethod;

// 属性的声明说的很清楚,是一个名为 readResolve 的方法,但是返回值是啥呢,我们再找找,这个值不会凭空生成,要么是构造方法生成的时候放进去的,要么就是手动set的
// 那我们就用快捷键 Command + B 看看这个属性在哪里被用到了吧
// 不难发现,这是在 java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>) 构造方法的时候,通过反射设置的

// 省略部分代码
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);

// 再看 java.io.ObjectStreamClass#getInheritableMethod(Class<?> cl, String name, Class<?>[] argTypes, Class<?> returnType)
// readResolveMethod 就是 函数名为 readResolve,无参,返回值是 Object 的方法

// 7.2. 再看看 desc.invokeReadResolve(obj) 这个方法 -> java.io.ObjectStreamClass#invokeReadResolve
// 这个方法就是反射调用 readResolveMethod 这个 Method 对象返回结果

// 8. 到这儿,聪明的你应该知道怎么玩儿了吧,重写 readResolve 方法,返回 INSTANCE 完事~
private Object readResolve() {
    return INSTANCE;
}

注意哦,当我们使用枚举式单例的写法就不会出现这种情况,具体原因如下:

// 还是走到 上面的 switch 分支,但是枚举式单例就会走
case TC_ENUM: return checkResolve(readEnum(unshared));

// 跟踪 java.io.ObjectInputStream#readEnum 源码可以发现
// 省略部分代码
Enum<?> en = Enum.valueOf((Class)cl, name);
// 所以不会导致单例被破坏的情况~

3.2. 反射破坏单例的情况

测试代码:

/**
 * <p>
 * 懒汉式单例,内部类写法,{@code 推荐写法之一}
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2019-08-11 19:24
 */
public class LazySingletonInnerClass {
    private LazySingletonInnerClass() {
        // 反射获取类的构造方法,通过newInstance() 获取对象会存在单例被破坏的问题
        // 添加以下代码解决,强制不允许构建
        if (SingletonHolder.INSTANCE != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static LazySingletonInnerClass getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private final static LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 普通方式获取
        LazySingletonInnerClass instance1 = LazySingletonInnerClass.getInstance();

        // 反射方式获取
        Class<LazySingletonInnerClass> clazz = LazySingletonInnerClass.class;
        Constructor<LazySingletonInnerClass> constructor = clazz.getDeclaredConstructor(null);
        // 设置访问级别,因为是private的
        constructor.setAccessible(true);
        LazySingletonInnerClass instance2 = constructor.newInstance();

        System.out.println("instance1 = " + instance1);
        System.out.println("instance2 = " + instance2);
    }
}

其实上面的代码已经给出答案了,只需要在构造方法里判断当前实例是否已创建,已创建抛出运行时异常,即可解决。

4. 应用

// 1. ServletContext

// 2. ServletConfig

// 3. ApplicationContext

// 4. DBPool

5. 优缺点

优点: 在内存中只有一个实例,减少内存开销;可以避免对资源的多重占用;设置全局访问点,严格控制访问

缺点: 没有接口,扩展困难;扩展单例对象,只能修改代码,不符合开闭原则

6. 完整代码地址

https://github.com/xkcoding/design-pattern/tree/master/src/main/java/com/xkcoding/design/pattern/creational/singleton

测试代码里 ConcurrentExecutor 具体实现请看这里:https://github.com/xkcoding/design-pattern/blob/master/src/main/java/com/xkcoding/design/pattern/utils/ConcurrentExecutor.java


编辑页面
分享到:

上一篇
设计模式之创建型设计模式-原型模式
下一篇
设计模式之创建型设计模式-抽象工厂模式