轻松搞定单例模式以及线程安全等问题

    技术2026-08-05  22

    单例模式

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    定义:保证一个类仅有一个实例,并提供一个全局访问点类型:创建型单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。

    适用场景

    想确保任何情况下都绝对只有一个实例

    优点与缺点

    优点:

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

    缺点:

    没有接口,拓展困难

    单例模式的重点

    私有构造函数,因为单例模式只提供一个全局访问点,不允许将构造函数暴露给用户在多线程下,可能发生的线程安全问题是否延迟加载,即大家熟知的饿汉式,懒汉式序列化以及反序列化的安全问题防止使用反射破坏单例

    编码实现单例模式—懒汉式

    public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton() { } public synchronized static LazySingleton getInstance() { //延迟加载 if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } // 与上面的写法是一致的 // public static LazySingleton getInstance() { // synchronized (LazySingleton.class) { // if (lazySingleton == null) { // lazySingleton = new LazySingleton(); // } // } // //延迟加载 // return lazySingleton; // } }

    在多线程下,为了避免多次创建实例,破坏单例。所以可以使用synchronized来修饰方法,达到同步的目的。对于静态方法来说,锁住的是类的Class对象,即其他线程无法再访问该类,Class对象的加锁与解锁需要消耗资源,且这段代码需要同步的只是下面代码。可以对这种方式进行改进。

    if (lazySingleton == null) { lazySingleton = new LazySingleton(); }

    双重检测懒汉式

    public class LazyDoubleCheckSingleton { private static LazyDoubleCheckSingleton lazySingleton = null; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { //延迟加载 if (lazySingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazySingleton == null) { lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }

    使用同步代码块,避免了只要访问getInstance方法就锁住Class对象。双重判断也可以避免因为线程调度而产生的实例多次创建的问题。但这样也没完全避免线程安全问题,就是指令重排序带来的问题。这里不讨论指令重排序以及JMM和多线程相关的原理。 new LazyDoubleCheckSingleton()其实包括了3个步骤:

    分配对象的内存空间初始化对象设置instance指向内存可见(引用)

    2和3是可以调换顺序的,由于2、3之间没有依赖关系,cpu可能会对指令重排序达到提高效率的目的。

    所以可能出上图发生的情况,由于重排序,instance对象并没有完成初始化,但实际上此时instance对象已经不是null。把未初始化的对象发布了出去,这就产生了安全问题! 这里主要讲2种解决方法:禁用指令重排序以及对其他线程指令重排序不可见。

    volatile禁用重排序

    public class LazyDoubleCheckSingleton { //禁用重排序 private volatile static LazyDoubleCheckSingleton lazySingleton = null; private LazyDoubleCheckSingleton() { } /** *由于直接在静态方法使用synchronized,锁的是class对象,即其他线程无法获取此锁。降低性能可以优化。 *正真需要同步的是延迟加载这个代码块,而不是整个方法。 * 由于可能发生重排序导致对象未初始化就先完成引用的操作—> lazySingleton !=null,会出现线程安全问题。 * 所以使用volatile使lazySingleton对所有线程可见。 */ public static LazyDoubleCheckSingleton getInstance() { //延迟加载 if (lazySingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazySingleton == null) { lazySingleton = new LazyDoubleCheckSingleton(); // TODO: 2020/7/4 理解volatile JMM 重排序 happens-before /* 1. 分配内存 2. 初始化对象 3. 将lazySingleton 指向该对象 可能会发生指令重排序(仅针对单个处理器中执行的指令序列和单个线程中执行的操作) */ } } } return lazySingleton; } }

    volatile禁用重排序的原理是内存屏障,这里不作讨论。

    基于静态内部类的懒加载

    public class StaticInnerClassSingleton { private static class InnerClass{ private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton(); } private StaticInnerClassSingleton() { } public static StaticInnerClassSingleton getInstance() { return InnerClass.instance; } }

    首先对于内部类,static final修饰的常量,在类的初始化阶段就完成赋值了。并且类的初始化具有Class对象锁,只有一个线程可获取到Class对象锁。所以就算发生了重排序对于其他线程也是不可见的。从而屏蔽了重排序问题。

    编码实现单例模式—饿汉式

    public class HungrySingleton implements Serializable { public static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); private HungrySingleton() { if (HUNGRY_SINGLETON!=null) { throw new RuntimeException("禁止在单例模式使用反射"); } } public static HungrySingleton getInstance() { return HUNGRY_SINGLETON; } private Object readResolve() { return HUNGRY_SINGLETON; } }

    饿汉式是比较容易实现的,且没有线程安全问题。因为instance为常量,在类加载是就已经赋值了。 上面例子种增加了一些无关代码,下面再来解释一下原因,主要是针对反射破坏以及序列化破坏单例方面。

    破坏单例模式的一些问题

    主要包括:

    在多线程下,可能发生的线程安全问题序列化以及反序列化的安全问题防止使用反射破坏单例

    线程安全问题已经在上面说明了,就不赘述。

    防止使用反射破坏单例

    Class<HungrySingleton> objectClass = HungrySingleton.class; Constructor<HungrySingleton> constructor = objectClass.getDeclaredConstructor(); constructor.setAccessible(true); HungrySingleton newInstance = constructor.newInstance();

    我们可以通过反射来调用单例类的私有构造器,从而破坏了单例模式。

    private HungrySingleton() { if (HUNGRY_SINGLETON!=null) { throw new RuntimeException("禁止在单例模式使用反射"); } }

    这个时候,这段的代码的作用就看出来,由于在类加载阶段就已经生成了instance对象,我们在构造器对instance进行判断,抛出异常。 可以有效防止外部通过反射调用私有构造函数。但对于懒汉式,就很难防止反射攻击,因为懒汉式的instance对象是在getInstance方法中生成的,所以无论采取什么方式,比如:标志位(flag),反射都可以获取到变量,修改变量来达到new一个新的instance的目的。

    序列化以及反序列化的安全问题

    HungrySingleton instance = HungrySingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry_singleton")); oos.writeObject(instance); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry_singleton")); HungrySingleton newInstance = (HungrySingleton) ois.readObject(); System.out.println(instance.getData() == newInstance.getData());

    通过结果,输入输出的对象不是同一个对象。那么为什么两次的结果不一致,这里只展出jdk的关键源码。

    /* 定义在ObjectInputSteam的readOrdinaryObject方法 该类是否包含有构造器,有则返回true,当返回true时,通过反射new一个instance */ obj = desc.isInstantiable() ? desc.newInstance() : null; boolean isInstantiable() { requireInitialized(); return (cons != null); } /* 当类里包含有readResolve方法,就会调用readResolve的返回值作为序列化的返回值,不然将返回返回上面新new的对象 */ if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); } boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); } readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);

    主要的逻辑就是先判断该类是不是可以支持序列化,然后先通过反射new一个instance,之后再判断类里是否有readResolve方法,有则返回readResolve方法返回的object,没有则返回新new出的object。所以在单例类里补充readResolve方法就可以解决序列化的问题。

    private Object readResolve() { return HUNGRY_SINGLETON; }

    单例模式最佳实践—基于枚举类

    public enum EnumInstance { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance() { return INSTANCE; } }

    我们使用jad来反编译这个类:

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumInstance.java package com.qzh.design.pattern.creational.singleton; public final class EnumInstance extends Enum { public static EnumInstance[] values() { return (EnumInstance[])$VALUES.clone(); } public static EnumInstance valueOf(String name) { return (EnumInstance)Enum.valueOf(com/qzh/design/pattern/creational/singleton/EnumInstance, name); } private EnumInstance(String s, int i) { super(s, i); } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumInstance getInstance() { return INSTANCE; } public static final EnumInstance INSTANCE; private Object data; private static final EnumInstance $VALUES[]; static { INSTANCE = new EnumInstance("INSTANCE", 0); $VALUES = (new EnumInstance[] { INSTANCE }); } }

    可以看到INSTANCE在类加载阶段就初始化了,所以是线程安全的。且具有一个私有的构造函数,符合单例模式。 并且枚举类是不支持反射的。在Constructor的newInstance方法中

    if ((this.clazz.getModifiers() & 16384) != 0) { throw new IllegalArgumentException("Cannot reflectively create enum objects"); }

    可以看到无法使用构造器来反射出枚举类对象。 枚举类也天生支持序列化,我们继续来看ObjectInputSteam的代码:

    String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } }

    可以看到,是通过名称去获取枚举对线,而枚举对象又是static final修饰的,所以拿到的都是同一个对象,有兴趣的朋友可以深入到Eunm的valueOf方法去分析。

    后记

    总结了常见的单例模式的一些具体实现,以及一步步去优化代码,解决问题。以及解释大家平时不常用的用枚举类来实现单例。希望大家有所收获! github地址 源码:https://github.com/DiangD/design_pattern_practice

    Processed: 0.009, SQL: 9