面试官: 能绕过单例模式得到多个不同实例吗?

面试官: 能绕过单例模式得到多个不同实例吗?

猴哥有次被面试到这个问题,听到这个问题,有点懵,既然设计成单例模式,就是不想让随便实例化了,还去反单例,是不是闲着DT,面试官非要这么搞,咱也没办法啊,莫非想搞啥坏事?想了想,得到对象的实例,一般就是new ,再者就是反射了,但是单例对象一般都经过私有化了,new 肯定是走不通了,如果通过反射改变私有构造方法的访问权限,应该就可以拿到对象实例了,原来JDK反射提供了此方法,通过设置 setAccessible(true),即可获取单例对象。

通过反射破坏单例

package com.monkeyjava.learn.basic.design;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author monkeyjava
 * @description
 * @date 2021/09/23
 */
public class Singleton {

    private final static Singleton SINGLETON = new Singleton();
    private Singleton(){
    }

    public static Singleton getInstance(){
        return SINGLETON;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            InstantiationException {
        Singleton s1 = Singleton.getInstance();

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();

        System.out.println("单例对象" + s1);
        System.out.println("反射获取单例对象"+ s2);

    }
}

输出结果:

单例对象com.monkeyjava.learn.basic.design.Singleton@610455d6
反射获取单例对象com.monkeyjava.learn.basic.design.Singleton@511d50c0

输出结果显示这两个不是同一个对象, 虽然不是同一个对象,s2对象是通过反射得到的实例,只不过单例模式失去了应有的意义,被破坏了而已。

实例化计数防止破坏

既然可以反射得到单例,有什么办法可以防止吗?如果对构造方法加一个计数器,超过2个对象实例化,抛一个异常是否能解决呢?

代码如下

package com.monkeyjava.learn.basic.design;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @author monkeyjava
 * @description
 * @date 2021/09/23
 */
public class Singleton {

    private final static Singleton SINGLETON = new Singleton();
    private static int count;

    private Singleton(){
        synchronized (Singleton.class) {
            if (count > 0) {
                throw new RuntimeException("error:创建了两个实例!");
            }
            ++count;
        }
    }

    public static Singleton getInstance(){
        return SINGLETON;
    }
    
    
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            InstantiationException {


        Singleton s1 = Singleton.getInstance();

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();

        System.out.println("单例对象" + s1);
        System.out.println("反射获取单例对象"+ s2);

    }
}

加了一个count 计数,并且加了同步锁,首次初始化时count为,并且count++ , count=1,下次初始化时count>0 满足条件,直接触发RuntimeException。运行上面的方法,直接抛出异常.

image-20210923221149912

别急,老哥!反射既然可以修改方法属性,也可以修改变量值啊,通过反射初始化后,再将count 对象值设置为0,一样不也可以初始化吗?防不胜防啊!!代码如下。


package com.monkeyjava.learn.basic.design;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

/**
 * @author monkeyjava
 * @description
 * @date 2021/09/23
 */
public class Singleton {

    private final static Singleton SINGLETON = new Singleton();
    private static int count;

    private Singleton(){
        synchronized (Singleton.class) {
            if (count > 0) {
                throw new RuntimeException("error:创建了两个实例!");
            }
            ++count;
        }
    }

    public static Singleton getInstance(){
        return SINGLETON;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
            InstantiationException, NoSuchFieldException {
        // 反射获取count变量
        Field countField = Singleton.class.getDeclaredField("count");
        countField.setAccessible(true);

        Singleton s1 = Singleton.getInstance();
        // 实例化对象后,再将count 设置为0
        countField.set(s1, 0);

        // 反射初始化时count 是0,所以可以正常初始化
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();

        System.out.println("单例对象" + s1);
        System.out.println("反射获取单例对象"+ s2);

    }
}

运行结果

单例对象com.monkeyjava.learn.basic.design.Singleton@511d50c0
反射获取单例对象com.monkeyjava.learn.basic.design.Singleton@60e53b93

此刻是不是感觉反射很讨厌,看来反射是解决不了单例破坏问题啊。

通过序列化破坏

package com.monkeyjava.learn.basic.design;

import java.io.*;

/**
 * 通过使用序列化的方式失效
 * @author monkeyjava
 * @description
 * @date 2021/09/23
 */
public class Singleton2 implements Serializable {

    private final static Singleton2 SINGLETON = new Singleton2();

    private Singleton2(){

    }

    public static Singleton2 getInstance(){
        return SINGLETON;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {


        Singleton2 s = Singleton2.getInstance();

        FileOutputStream fos = new FileOutputStream("Singleton2.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("Singleton2.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton2  s1 = (Singleton2)ois.readObject();
        System.out.println(s==s1);

    }
}

输出结果: false , 证明序列化后的对象已不是先前的对象,出现了2个实例,违法了单例原则。有什么方法解决吗?

增加readResolve方法被防止序列化破坏

package com.monkeyjava.learn.basic.design;

import java.io.*;

/**
 * 通过使用序列化的方式失效
 * @author monkeyjava
 * @description
 * @date 2021/09/23
 */
public class Singleton2 implements Serializable {

    private final static Singleton2 SINGLETON = new Singleton2();

    private Singleton2(){

    }

    public static Singleton2 getInstance(){
        return SINGLETON;
    }

    private Object readResolve(){
        return SINGLETON;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {


        Singleton2 s = Singleton2.getInstance();

        FileOutputStream fos = new FileOutputStream("Singleton2.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("Singleton2.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton2  s1 = (Singleton2)ois.readObject();

        System.out.println(s==s1);

    }
}

输出结果: true

防止单例破坏的正确姿势

总结,实现单例模式的唯一推荐方法,使用枚举类来实现。使用枚举类实现单例模式,在对枚举类进行序列化时,还不需要添加readRsolve方法就可以避免单例模式被破坏。序列化获取到的都是同一个对象,感兴趣同学可以验证下,并且无法通过反射获取实例。

package com.monkeyjava.learn.basic.design;

public enum EnumSingleton {
    INSTANCE;

   public void sayHellow() {
       System.out.println("枚举反单例测试");
   }

   public static void main(String[] args) {
       EnumSingleton.INSTANCE.sayHellow();
   }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值