你写的单例模式真的足够安全吗? | Java-Debug笔记

745 阅读3分钟

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看<活动链接>


单例的几种写法

我在之前的文章中列举了8种单例模式的写法,详见单例模式的8种写法

在开发中,大家可能用的最多的是懒汉式、双重检查这种写法

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;
    }
}

这种写法,两次判断保证线程安全,懒汉式延迟加载,效率较高,并且在 new Singleton() 新建对象时使用 volatile 可以防止重排序。

看似完美的写法,真的能保证多次获取到的Singleton对象是同一个吗?我们来试验下

正常调用单例获取对象

public static void main(String[] args) {
    Singleton singleton1 = Singleton.getInstance();
    Singleton singleton2 = Singleton.getInstance();
    System.out.println(singleton1);
    System.out.println(singleton2);
}

正常的调用中,输出 singleton1和singleton2 确实是同一个对象

com.test.Singleton@14514713
com.test.Singleton@14514713

Process finished with exit code 0

在这个单例模式中,因为设置了 private 无参构造函数,所以我们无法通过 new Singleton() 直接创建对象来破坏单例的结构

那有没有一种方法可以直接忽略 private ,直接调用 new Singleton() 呢,当然是有的,那就是反射

简单说一下Java反射中常用的api

  • 1.获取Class对象

    • 1.1. 通过包名+类名

      Class<?> aClass = Class.forName("com.test.Singleton");

    • 1.2. 通过类名.class

      Class<?> aClass = Singleton.class;

    • 1.3. 使用getClass()

      Class<?> aClass = new Singleton().getClass();

  • 2.创建类对象

    • 2.1. 只能使用默认的无参数构造方法

      Singleton singleton = (Singleton) clazz.newInstance();

    • 2.2. 可以选择特定构造方法,getConstructor()需要传参

      Constructor<?> con = clazz.getConstructor();

      Singleton singleton = (Singleton)con.newInstance();

  • 3.获取类的属性

    • 3.1. 获取public的

      Field[] fields = clazz.getFields();

    • 3.2. 获取所有的,包括private

      Field[] declaredFields = clazz.getDeclaredFields();

  • 4.获取类的方法

    • 4.1. 获取类中所有方法,不包括private

      Method[] methods = clazz.getMethods();

    • 4.1. 获取类中private方法

      Method[] declaredMethods = clazz.getDeclaredMethods();

  • 5.获取类的构造器

    • 5.1. 获取无参构造器,包括private

      Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();

    • 5.2. 获取public构造器

      Constructor<?>[] constructors = clazz.getConstructors();

    • 5.3. 获取所有构造器,包括private

      Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();

使用反射调用单例获取对象

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Singleton singleton1 = Singleton.getInstance();
    Singleton singleton2 = Singleton.getInstance();
    System.out.println(singleton1);
    System.out.println(singleton2);

    Class<?> clazz = Singleton.class;
    // 使用反射机制获取构造函数
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    // 将Accessible设置为true,就可以忽略private访问私有方法或变量的值了,不设置的话会抛出异常
    constructor.setAccessible(true);
    System.out.println(constructor.newInstance());
}

需要注意的是,通过反射调用 private 方法,需要将 Accessible 设置为 true,否则会执行异常

执行结果

com.test.Singleton@14514713
com.test.Singleton@14514713
com.test.Singleton@69663380

Process finished with exit code 0

从输出结果我们可以看到,这已经不是同一个对象了。因为我们用反射机制破坏了单例的结构,让我们可以直接去 new Singleton()

那么有哪种单例写法可以无视反射呢?那就是枚举

枚举单例模式

枚举为什么是最好的方法,大家可以看下之前的文章单例模式的8种写法中最后一种写法

更为详细的枚举单例模式写法

public class EnumSingleton {

    private EnumSingleton() {
    }

    /**
     * 供外界调用
     *
     * @return EnumSingleton
     */
    public static EnumSingleton getInstance() {
        return ContainerHolder.HOLDER.instance;
    }

    private enum ContainerHolder {
        HOLDER;
        private EnumSingleton instance;

        // 饿汉模式
        ContainerHolder() {
            instance = new EnumSingleton();
        }
    }
}

测试反射是否能打破这种单例

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {

    // 正常调用
    EnumSingleton singleton1 = EnumSingleton.getInstance();
    EnumSingleton singleton2 = EnumSingleton.getInstance();
    System.out.println(singleton1);
    System.out.println(singleton2);
    
    // 反射调用 EnumSingleton
    Class<?> clazz = EnumSingleton.class;
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance();
    System.out.println(enumSingleton.getInstance());
    
    // 反射调用内部枚举 ContainerHolder
    Class<?> containerHolder = Class.forName("com.wupao.channel.EnumSingleton$ContainerHolder");
    // 此处会报错,java.lang.NoSuchMethodException
    Constructor<?> declaredConstructor = containerHolder.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    // 反射调用
    System.out.println(declaredConstructor.newInstance());
}

输出结果

com.test.EnumSingleton@14514713
com.test.EnumSingleton@14514713
com.test.EnumSingleton@14514713
Exception in thread "main" java.lang.NoSuchMethodException: com.test.EnumSingleton$ContainerHolder.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.test.Test.main(Test.java:60)

Process finished with exit code 1

第三次输出,与前两次输出为同一个对象

而直接通过获取内部枚举类ContainerHolder的方式来创建对象,则会直接报错 NoSuchMethodException,没有这样的方法??

通过查询Enum源码(java.lang.Enum),则会看到Enum有两个带参的构造函数

改下代码,传入两个参数

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
    // 反射调用内部枚举 ContainerHolder
    Class<?> containerHolder = Class.forName("com.wupao.channel.EnumSingleton$ContainerHolder");
    // 传入两个参数
    Constructor<?> declaredConstructor = containerHolder.getDeclaredConstructor(String.class, int.class);
    declaredConstructor.setAccessible(true);
    // 反射调用
    System.out.println(declaredConstructor.newInstance());
}

执行结果

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

Process finished with exit code 1

报错写的也很明白,Cannot reflectively create enum objects,不能通过反射来创建枚举对象!

所以,枚举这种方式,是可以防住反射进行破坏的!