深入探究单例模式

深入探究单例模式

1. 概述

单例模式的作用

  • 节省内存和计算。比如从数据库连接中获取连接,这个连接只需要一个。
  • 保证结果正确。比如用多线程统计人数,则需要用单例,大家需要对同一个内容修改。
  • 方便管理。比如工具类只需要一个实例,比如说日期工具类,字符串工具类等

单例模式的适用场景

  • 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
  • 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

2. 单例模式的8种写法

下面的用Java演示各种单例模式的书写

2.1 静态常量

  • 静态常量只和类本身有关,和对象无关,因此静态常量只会存在一个。
// 饿汉式
public class Singleton {
    
    
    private final static Singleton INSTANCE = new Singleton();  // 在链接阶段的准备环节显式赋值

    private Singleton(){
    
    }
    
    public static Singleton getInstance(){
    
     return INSTANCE; }
}

2.2 静态代码块

  • 静态变量和静态代码块一般是在 初始化阶段 (类的加载一般分为:加载、链接(验证、准备、解析)、初始化;参考网址)在<clinit>函数中被执行。因为函数<clinit>() 带锁线程是安全的,只会在类的加载时执行一次,因此是线程安全的。
// 饿汉式
public class Singleton {
    
    
    private final static Singleton INSTANCE;
    static {
    
    
        INSTANCE = new Singleton();
    }

    private Singleton(){
    
    }
    
    public static Singleton getInstance(){
    
     return INSTANCE; }
}

2.3 synchronized加锁

  • 这里在static方法上加锁,相当于加了类锁(详细内容可参照:参考网址),可以保证多线程下的安全,但是效率低下,比如:如果这个单例是个处理字符串工具类,如果多个进程想要得到该实例去处理字符串,则无法实现,效率低下。因此不推荐使用
// 懒汉式
public class Singleton {
    
    
    private static Singleton instance;

    private Singleton(){
    
    }

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

2.4 双重检查

  • 这种方式是最推荐的两种方式中的一种,十分重要,必须记住
// 双重检查:DCL (Double Check-Loading)
public class Singleton {
    
    
    private volatile static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }
}

下面是关于这种写法的说明

  • 这种写法的优点是什么?

    线程安全;延迟加载,效率较高

  • 为什么要double-check,单check行不行?

    不行,如果去掉内部的 if 判断,会造成线程不安全;如果去掉外部的 if 判断,会造成效率低下,每次只能有一个线程运行。

  • 为什么不把synchronized放在getInstance()这个方法前?

    这样做事可以的,但这种方式不好,因为:比如这个单例是个处理字符串工具类,如果多个进程想要得到该实例去处理字符串,则无法实现,效率低下。

  • 为什么需要用volatile修饰instance?

    (1)因为新建对象不是原子操作,实际上包含三个步骤(详细内容可参考:参考网址,找到该网址中的5 对象的创建与访问指令小节查看即可):

    /**
     * 1. 新建一个空的对象;
     * 2. 调用构造函数赋值;
     * 3. 将对象引用赋值给instance。
     */
    

    (2)volatile保证了这三个步骤不会重排序,重排序会导致NPE。 如果没有volatile可能发生重排序,比如一种重排序方式为:132,如果此时线程A执行完13后切换到线程B,线程B发现instance非空,返回instance,会导致线程B使用成员变量时抛出空指针异常(NPE)。

    (3)volatile同时也保证了可见性。(这种说法本身没有问题,但不应作为这个问题的回答,因为:synchronized具有happens-before原则,已经保证了可见性)

2.5 静态内部类

  • 静态内部类和非静态内部类一样,都不会因为外部内的加载而加载,同时静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类,参考网址
  • 这也是一种推荐使用的单例模式。
// 懒汉式
public class Singleton {
    
    
    private Singleton(){
    
    }

    private static class SingletonInstance {
    
    
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
    
    
        return SingletonInstance.INSTANCE;
    }
}

2.6 枚举

  • 这种方式是最推荐的两种方式中的另一种
// 枚举
public enum SingletonByEnum {
    
    
    
    INSTANCE;
    
    public static SingletonByEnum getInstance() {
    
    
        return INSTANCE;
    }
}

2.7 两种常见错误写法

错误写法一

// 懒汉式
public class Singleton {
    
    
    private static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null) {
    
     instance = new Singleton(); }
        return instance;
    }
}
  • 错误原因:两个线程同时去 if 判断,均成立,会创建两个实例。

错误写法二

// 懒汉式
public class Singleton {
    
    
    private static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
    
                instance = new Singleton();
            }
        return instance;
    }
}
  • 错误原因:两个线程同时去 if 判断,均成立,会创建两个实例。

3. 单例模式的安全性探究

3.1 使用序列化破坏单例模式

  • 这里演示破坏DCL
  • 如下是破坏程序
public class DestroyWithSerializable {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 使用序列化破坏 DCL
        Singleton instance = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
        Singleton instance2 = (Singleton) ois.readObject();

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

我们发现Singleton无法被序列化,输出结果如下:

在这里插入图片描述


  • 为了演示序列化破坏单例,这里让Singleton实现序列化接口,更改DCL程序如下:
public class Singleton implements Serializable {
    
      // 实现序列化接口
    private volatile static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }
}
  • 此时再执行上面的DestroyWithSerializable程序,结果如下,发现已经破坏了单例

在这里插入图片描述


  • 那么如果一个单例类实现了Serializable接口,序列化就一定可以破坏单例吗,有没有什么方法防止序列化破坏单例?

    存在防止序列化破坏单例的方式,我们可以更改Singleton类如下,这样就可以解决了序列化破坏单例的问题。

public class Singleton implements Serializable {
    
    
    private volatile static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }

    private Object readResolve() {
    
     return instance; }
}
  • 此时再执行上面的DestroyWithSerializable程序,结果如下,发现已经无法破坏单例:

在这里插入图片描述

此时,我们脑海中就会存在这样一个疑问:为什么加上这样一个函数就可以防止序列化破坏单例? 下面我们就从源码的角度探究这个问题。

(1)我们要从Singleton instance2 = (Singleton) ois.readObject();这句话开始看起,下面是readObject()的源码

public final Object readObject() throws IOException, ClassNotFoundException {
    
    
    if (enableOverride) {
    
     return readObjectOverride(); }
    
    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
    
    
        Object obj = readObject0(false);  // 关键点
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
    
     throw ex; }
        if (depth == 0) {
    
     vlist.doCallbacks(); }
        return obj;
    } finally {
    
    
        passHandle = outerHandle;
        if (closed && depth == 0) {
    
     clear(); }
    }
}

(2)我们看到这段源码调用了Object obj = readObject0(false);这个函数,接着分析这个函数,如下(无关代码已被省略):

private Object readObject0(boolean unshared) throws IOException {
    
    
    // ...
    try {
    
    
        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);
			// ......
        }
    } finally {
    
    
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

(3)因为我们最开始调用的是readObject()方法,因此这里会进入这个分支,接着分析这个函数,如下(无关代码已被省略):

private Object readOrdinaryObject(boolean unshared) throws IOException {
    
    
    // ...
    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();
    // ...
    Object obj;
    try {
    
    
        // 关键点,新的Singleton一定会被创建,只不过这个新对象不一定被返回
        // 如果被序列化的对象定义readResolve()方法,则返回该方法返回的对象
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
    
    
        // ...
    }
	// ...
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())  // 关键点
    {
    
    
        // 关键点,通过反射执行我们定义的readResolve()方法
        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);  // 关键点:obj被重新赋值
        }
    }

    return obj;
}

(4)我们可以看到上面的代码调用了desc.hasReadResolveMethod(),下面进入这个方法查看,这个方法位于另外一个类ObjectStreamClass

/**
 * Returns true if represented class is serializable or externalizable and
 * defines a conformant readResolve method.  Otherwise, returns false.
 */
// 如果被表示的类是可序列化的或可外部化的,并且定义了一个一致的readResolve方法,则返回true。否则,返回false
boolean hasReadResolveMethod() {
    
    
    requireInitialized();
    return (readResolveMethod != null);
}

(5)我们在Singleton中定义了readResolve方法,因此该方法返回true,if判断成立,我们可以进入执行Object rep = desc.invokeReadResolve(obj);这句话,之后就会执行我们定义的readResolve方法,然后返回我们自己的单例,因此防止了序列化破坏单例。

3.2 使用反射破坏单例模式

补充:反射相关知识

/*
反射:任何一个类都是Class的实例对象,这个实例对象有三种表示方式,称为类类型,比如有个类Foo,里面有个函数print(int a, int b)
Foo foo = new Foo();
Class c1 = Foo.class;  // 第一种创建方式
Class c2 = foo.getClass();  // 第二种创建方式
Class c3 = Class.forName("Foo");  // 第三种创建方式

我们完全可以通过类的类类型创建该类的实例对象
Foo newFoo = (Foo)c1.newInstance();

获取方法信息
Method[] ms = c1.getMethods();  // 所有的public的函数,包括父类继承而来的
Method[] ms = c1.getDeclaredMethods();  // 获取所有该类自己声明的方法,不问访问权限
Class returnType = ms[0].getReturnType();  // 得到方法返回类型
String methodName = ms[0].getName();  // 得到方法名称
Class[] paramTypes = ms[0].getParameterTypes();  // 得到参数列表的类型的类类型

获取成员变量信息
Field[] fs = c1.getFields();  // 获取所有public的成员变量的信息
Field[] fs = c1.getDeclareFields();  // 获取所有该类自己声明的成员变量的信息
Class fieldType = fs[0].getType();  // 得到成员变量的类类型
String typeName = fieldType.getName();  // 得到成员变量的类型
String fieldName = fs[0].getName();  // 得到成员变量的名称

获取构造函数
Constructor[] cs = c1.getConstructors();  // 获取所有的public构造函数
Constructor[] cs = c1.getDeclareConstructors();  // 获取所有的构造函数
String constructorName = cs[0].getName();  // 获取构造函数名称
Class[] paramTypes = cs[0].getParameterTypes();  // 得到参数列表的类型的类类型
String paramName = paramTypes[0].getName();  // 获取参数名称

通过反射调用函数
Method m = c1.getMethod("print", new Class[]{int.class, int.class});  // 获取该方法
Method m = c1.getMethod("print", int.class, int.class);  // 获取该方法
Object o = m.invoke(foo, new Object[]{10, 20});  // 用print方法这个对象m来操作foo,如果没有返回值就返回null
Object o = m.invoke(foo, 10, 20);  // 和上句话效果一致
*/
  • 既然序列化无法破坏单例了,那么我们就使用更加厉害的工具来破坏单例,即 反射
  • 此时的需要被破坏的代码如下:
public class Singleton implements Serializable {
    
    
    private volatile static Singleton instance;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }

    private Object readResolve() {
    
     return instance; }
}
  • 如下是破坏程序X
public class DestroyWithReflection {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 通过反射破坏 DCL
        Class objectClass = Singleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);  // 打开访问private权限

        Singleton instance = Singleton.getInstance();
        Singleton instance2 = (Singleton) c.newInstance();  // 会调用空参构造器

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

单例模式被成功破坏,结果如下:

在这里插入图片描述

刚才我们辛辛苦苦防止序列化破坏单例,在这里就失效了吗?对,确实失效了,刚才的方式只能解决序列化破坏单例模式。


  • 我们需要进一步升级Singleton类,既然反射会调用空参构造器,那么我们可以在空参构造器中加入判断,以防止单例被破坏,代码如下:
public class Singleton implements Serializable {
    
    
    private volatile static Singleton instance;

    private Singleton() {
    
    
        if (instance != null) {
    
    
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    public static Singleton getInstance() {
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }

    private Object readResolve() {
    
     return instance; }
}
  • 此时执行上面的破坏程序,结果如下:

在这里插入图片描述

可也看到,抛出了我们预期的异常。**但你以为这样就结束了吗?**事实上面这种方式并不能解决单例模式被破坏,我们只需要更改破坏程序中的代码,就可以让这种方法失效,即先用反射创建对象,此时Singleton中的instance未被赋值,然后再调用getInstance()就可以得到另一个不同的对象,升级为破坏程序Y:

public class DestroyWithReflection {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 通过反射破坏 DCL
        Class objectClass = Singleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);  // 打开访问private权限

        Singleton instance2 = (Singleton) c.newInstance();  // 会调用空参构造器
        Singleton instance = Singleton.getInstance();

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

单例模式又被成功破坏,结果如下:

在这里插入图片描述


  • 此时我们又该怎么办呢?别急,我们还有办法,继续升级我们的Singleton,我们可以在Singleton中设置标志位,开始为false,一旦创建对象(无论是怎么创建的),就将flag置为true,下次如果再创建对象,发现flag为true直接抛出异常,升级后的Singleton如下:
public class Singleton implements Serializable {
    
    
    private volatile static Singleton instance;
    private static boolean flag = false;

    private Singleton() {
    
    
        if (!flag) flag = true;
        else throw new RuntimeException("单例构造器禁止反射调用");
    }

    public static Singleton getInstance() {
    
    
        if (instance == null)
            synchronized (Singleton.class) {
    
      // 类锁
                if (instance == null)
                    instance = new Singleton();
            }
        return instance;
    }

    private Object readResolve() {
    
     return instance; }
}

执行刚刚升级过得破坏程序Y,结果如下:

在这里插入图片描述

同样可以看到,抛出了我们预期的异常。**但你以为这样总可以结束了吗?**事实上面这种方式并不能解决单例模式被破坏,我们只需要更改破坏程序中的代码,通过反射可以获取flag(我们如何知道该类中有个标志位叫做flag呢,既然我们想去破坏单例模式,一定有办法得到的,比如javap反编译),然后将其值强行置为true,此时单例又被破坏,升级为破坏程序Z:

public class DestroyWithReflection {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 通过反射破坏 DCL
        Class objectClass = Singleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);  // 打开访问private权限

        Singleton instance2 = (Singleton) c.newInstance();  // 会调用空参构造器
        Field flag = objectClass.getDeclaredField("flag");  // 获取objectClass的flag字段
        flag.setAccessible(true);  // 打开访问private权限
        flag.set(objectClass, false);  // 将objectClass的flag字段设为false
        Singleton instance = Singleton.getInstance();

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

单例模式又被成功破坏,结果如下:

在这里插入图片描述


  • 那么,我们怎样修改代码才能避免DCL被反射攻击呢?最后的答案是:没办法!!!

反射攻击单例得到的最终的结论

对于懒汉式单例,我们无法避免发射攻击;对于饿汉式单例,我们可以通过在所有构造函数加入判断避免单例被破坏。

3.3 最安全的单例模式:枚举单例

  • Joshua Bloch大神在《Effctive Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。”

    (1)枚举简单,相当于饿汉式;

    (2)线程安全有保证,枚举反编译为final class,继承枚举父类,并且各个变量以及方法都是用static定义的,所以枚举的本质就是一个静态的对象。

    (3)可以避免反序列化和反射破坏单例

  • 如下测试使用的枚举单例如下:

public enum EnumSingleton {
    
    
    INSTANCE {
    
    
        @Override
        protected void printTest() {
    
    
            System.out.println("Print Test");
        }
    };
    protected abstract void printTest();  // 保证外部可以调用

    public static EnumSingleton getInstance() {
    
    
        return INSTANCE;
    }
}

  • 尝试使用序列化破话枚举单例,代码如下:
public class DestroyWithSerializable {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 使用序列化破坏 枚举单例:无法破坏
        EnumSingleton instance = EnumSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);
        instance.printTest();  // 调用INSTANCE中的printTest方法

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton"));
        EnumSingleton instance2 = (EnumSingleton) ois.readObject();

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

无法破坏单例,结果如下:

在这里插入图片描述


  • 尝试使用反射破坏单例,枚举类中不存在空参构造器,虽然IDEA反编译出来的.class文件中显示存在空参构造器,再虽然javap反编译出来内容显示有空参构造器,这些显示内容可以理解为不正确,没错,就可以理解为工具错了(参考视频),也可以查看Enum的源码,发现里面没有空参构造器。此时就需要使用更加专业的工具反编译.class文件了,那就是jad:下载网址,如下是枚举单例对应的.class文件反编译出来的结果:
// 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:   EnumSingleton.java
package com.wxx;

import java.io.PrintStream;

public abstract class EnumSingleton extends Enum
{
    
    
    public static EnumSingleton[] values()
    {
    
    
        return (EnumSingleton[])$VALUES.clone();
    }
    public static EnumSingleton valueOf(String name)
    {
    
    
        return (EnumSingleton)Enum.valueOf(com/wxx/EnumSingleton, name);
    }
    private EnumSingleton(String s, int i)
    {
    
    
        super(s, i);
    }
    protected abstract void printTest();

    public static EnumSingleton getInstance()
    {
    
    
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
    
    
        INSTANCE = new EnumSingleton("INSTANCE", 0) {
    
    
            protected void printTest()
            {
    
    
                System.out.println("Print Test");
            }

        };
        $VALUES = (new EnumSingleton[] {
    
    
            INSTANCE
        });
    }
}

尝试使用反射破坏单例:

public class DestroyWithReflection {
    
    
    public static void main(String[] args) throws Exception {
    
    

        // 通过反射破坏 枚举单例:无法破坏
        Class objectClass = EnumSingleton.class;
        Constructor c = objectClass.getDeclaredConstructor(String.class, int.class);
        c.setAccessible(true);  // 打开访问private权限

        EnumSingleton instance2 = (EnumSingleton) c.newInstance();
        EnumSingleton instance = EnumSingleton.getInstance();

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

无法破坏单例,结果如下:

在这里插入图片描述

4. 单例模式举例

  • 例如JDK中的Runtime类:静态常量

在这里插入图片描述

5. 单例模式常见面试题

哪种单例模式最好?

  • 枚举单例最好。原因如下:(1)简单;(2)安全(可以避免反序列化和反射攻击)

  • 其次是DCL,但饿汉式单例无法避免反射攻击。

饿汉式和懒汉式的优缺点

  • 饿汉式

    优点:写法简单,线程安全

    缺点:上来就加载实例,造成一定的浪费

  • 懒汉式

    优点:不需要一上来就加载实例,解决了浪费问题

    缺点:写法稍微复杂,稍不注意可能造成线程不安全

单例模式各种写法的使用场合

  • 最好的方法时利用枚举,因为还可以防止反序列化和反射重新创建新的对象;

  • 如果程序一开始要加载的资源太多,那么就应该使用懒加载

  • 饿汉式如果是对象的创建需要配置文件就不合适,因为可能需要调用单例的一些方法做准备工作;

  • 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性

写出DCL,为什么是这样的?volatile去掉可以吗?

  • 请参照2.4

猜你喜欢

转载自blog.csdn.net/weixin_42638946/article/details/114329715
今日推荐