全面总结单例模式

192 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

所谓单例模式就是全局只存在类的一个对象,需要确保进程中任何情况下都绝对只有一个实例。

在很多场景都有单例模式的应用,示例如下。

  • ServletContext
  • ServletConfig
  • ApplicationContext
  • DBPool
  • ......

对于单例模式,实现写法有多种,例如:饿汉式懒汉式枚举式注册式

但是单例并不是一定安全的,某些情况下,会存在单例的破坏

本篇文章,将从上面的论述,对单例模式进行全面总结。

正文

一. 饿汉式单例

示例代码如下所示。

public class HungrySingleton {

    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return instance;
    }
    
}

优点:没有添加锁,执行效率高。

缺点:容易造成内存浪费。

二. 懒汉式单例

1. 简单写法

最简单的懒汉式单例实现如下。

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

}

优点:使用时才创建,不会造成内存浪费。

缺点:多线程下线程不安全。

2. 加锁写法

为了解决简单写法的缺点,可以进行如下改进。

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {}

    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

}

优点:多线程下线程安全。

缺点:添加了锁,执行效率低。

3. 双重检查锁

可以使用双重检查锁(DCL)来提高效率,如下所示。

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {}

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

}

缺点:还会存在指令重排序的问题。

4. 指令重排序

Java中创建(new)一个对象时,创建对象的操作是非原子的,创建对象的操作可以用下列伪码描述。

// 1. 为对象分配内存空间
memory = allocate();
// 2. 初始化对象
ctorInstance(memory);
// 3. 让instance指向memory内存空间
instance = memory;

为了提升编译性能,编译器可能会将上述的执行步骤重排,比如重排如下。

// 1. 为对象分配内存空间
memory = allocate();
// 3. 让instance指向memory内存空间
instance = memory;
// 2. 初始化对象
ctorInstance(memory);

上述指令重排序会导致某一刻时间instance指向一个未初始化的对象。单线程环境下指令重排序不会影响程序执行结果,但是多线程环境下可能会出现线程安全问题,比如下面的双重检查锁单例写法。

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {}

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

}

线程1获取到锁后,执行instance = new LazyDoubleCheckSingleton(); 这一行代码创建对象时,可能会由于指令重排序导致instance字段在某一刻时间指向未初始化的LazyDoubleCheckSingleton对象,那么在同一刻时间如果线程2来获取单例,就会判断到instance字段不为空,从而将instance返回,此时线程2就持有了一个未初始化的LazyDoubleCheckSingleton对象。

指令重排序解决方案是使用volatile关键字来禁止指令重排序,如下所示。

private volatile static LazyDoubleCheckSingleton instance;

5. 静态内部类写法

静态内部类写法能够利用JVM特性来做到执行效率高,不会造成内存浪费,并且写法优雅。静态内部类写法如下所示。

public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton() {}

    public LazyStaticInnerClassSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final LazyStaticInnerClassSingleton INSTANCE 
                = new LazyStaticInnerClassSingleton();
    }

}

三. 反射破坏单例模式

无论是饿汉式单例还是懒汉式单例,都会被反射破坏。比如下面的单例写法。

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {}

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

}

使用了volatile关键字并且加入了双重检查锁的懒汉式单例写法,正常情况下通过LazyDoubleCheckSingletongetInstance() 方法只会获取到LazyDoubleCheckSingleton的全局唯一的一个实例instance,但是实际上可以通过下列代码获取到和instance不一样的LazyDoubleCheckSingleton的其它实例。

public class ReflectTest {

    public static void main(String[] args) throws Exception {
        // 获取单例类Class对象
        Class<LazyDoubleCheckSingleton> aClass = LazyDoubleCheckSingleton.class;
        // 获取单例类构造器
        Constructor<LazyDoubleCheckSingleton> constructor = aClass.getDeclaredConstructor();
        // 设置构造器访问权限为true
        constructor.setAccessible(true);
        // 使用构造器创建实例
        LazyDoubleCheckSingleton lazyDoubleCheckSingleton = constructor.newInstance();
        // 打印通过构造器创建的实例信息
        System.out.println(lazyDoubleCheckSingleton);
        // 打印通过单列类获取的实例信息
        System.out.println(LazyDoubleCheckSingleton.getInstance());
    }

}

运行上述程序,会打印出两个不同的LazyDoubleCheckSingleton实例的信息,表明单例模式遭到了破坏。

四. 枚举式单例

枚举式单例本质上是注册式单例可以防止被反射破坏单例性质,是优雅的单例实现

public enum EnumSingleton {

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

}

还是尝试通过反射来破坏EnumSingleton的单例,如下所示。

public class ReflectTest {

    public static void main(String[] args) throws Exception {
        // 获取单例类Class对象
        Class<EnumSingleton> aClass = EnumSingleton.class;
        // 获取单例类构造器,Enum类只有一个构造函数protected Enum(String name, int ordinal)
        Constructor<EnumSingleton> constructor = aClass
                .getDeclaredConstructor(String.class, int.class);
        // 设置构造器访问权限为true
        constructor.setAccessible(true);
        // 使用构造器创建实例
        EnumSingleton enumSingleton = constructor.newInstance();
        // 打印通过构造器创建的实例信息
        System.out.println(enumSingleton);
        // 打印通过单列类获取的实例信息
        System.out.println(EnumSingleton.INSTANCE);
    }

}

运行上述程序,会有如下错误堆栈。

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

ConstructornewInstance() 方法如下所示。

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, null, modifiers);
        }
    }
    // 这里规定了如果是枚举,则不能反射创建枚举对象
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor; 
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

所以从JDK源码层面就规定了反射无法破坏单例。

另一方面,已知enum修饰的类,相当于继承于Enum类,Enum的构造方法的注释中有如下一段话。

Sole constructor. Programmers cannot invoke this constructor. It is for use by code emitted by the compiler in response to enum type declarations.

即枚举的构造方法明确说明了不能被我们调用

那么为什么说枚举式单例是注册式单例呢。首先看EnumvalueOf() 静态方法,该方法是通过枚举常量的标识符来获取枚举常量,如下所示。

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                            String name) {
    // 根据枚举常量标识符获取枚举常量
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
}

EnumvalueOf() 方法是根据枚举常量标识符来获取枚举常量,首先会通过Class对象的enumConstantDirectory() 方法将枚举常量标识符和枚举常量的映射获取出来,enumConstantDirectory() 方法如下所示。

Map<String, T> enumConstantDirectory() {
    // enumConstantDirectory是一个map
    // enumConstantDirectory存储枚举常量标识符和枚举常量的映射关系
    if (enumConstantDirectory == null) {
        T[] universe = getEnumConstantsShared();
        if (universe == null)
            throw new IllegalArgumentException(
                    getName() + " is not an enum type");
        Map<String, T> m = new HashMap<>(2 * universe.length);
        for (T constant : universe)
            m.put(((Enum<?>)constant).name(), constant);
        enumConstantDirectory = m;
    }
    return enumConstantDirectory;
}

enumConstantDirectory是枚举类型对应的Class对象中的一个map字段,key是枚举常量标识符,value是枚举常量,所以每个枚举类型的所有枚举常量都会注册到enumConstantDirectory字段中,枚举式单例也就是注册式单例。

缺点:枚举类型的枚举常量很多时,会造成内存浪费。

五. 注册式单例

注册式单例即使用容器将单例对象缓存,单例对象在容器中只会存在一个实例。

public class ContainerSingleton {

    private ContainerSingleton() {}

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }

}

缺点:线程不安全。

六. 序列化破坏单例

public class SeriableSingleton implements Serializable {

    public final static SeriableSingleton INSTANCE 
            = new SeriableSingleton();

    private SeriableSingleton() {}

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

}

上面是一个饿汉式单例,下面再给出序列化破坏单例的测试代码。

public class SeriableSingletonTest {

    public static void main(String[] args) {
        SeriableSingleton s1 = SeriableSingleton.getInstance();
        SeriableSingleton s2;

        FileOutputStream fos;
        try {
            // 序列化s1到磁盘上
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            oos.flush();
            oos.close();

            // 反序列化并将得到的对象赋给s2
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (SeriableSingleton) ois.readObject();
            ois.close();

            // 打印s1和s2并判断是否相等
            System.out.println(s2);
            System.out.println(s1);
            System.out.println(s2 == s1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

运行上述测试代码,最后会发现s1s2是两个不同的对象,即破坏了单例。

解决办法就是在单例类中添加readResolve() 方法,如下所示。

public class SeriableSingleton implements Serializable {

    public final static SeriableSingleton INSTANCE
            = new SeriableSingleton();

    private SeriableSingleton() {}

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return INSTANCE;
    }

}

总结

单例模式的写法总结如下。

  1. 饿汉式单例优点是不加锁,执行效果高,缺点是没有使用到也会创建出来,容易造成内存浪费,且可被破坏单例性质;
  2. 懒汉式单例优点是执行效率高,且不会造成内存浪费,缺点是可被破坏单例性质;
  3. 枚举式单例本质上是注册式单例,是单例的优雅实现,并且不会被破坏单例性质,但是可能会有内存浪费的风险;
  4. 注册式单例。使用容器缓存单例对象,根据具体实现的不同,各有优劣,可被破坏单例性质。

单例模式的破坏总结如下。

  1. 基于反射破坏单例
  2. 基于序列化破坏单例

枚举式单例即不会被反射破坏,也不会被序列化破坏


大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情