你的单例真的写对了吗?能经受住反射的考验吗

在我们日常的用法以及面试过程中,说到写单例,绝大部分都是采用二段锁写的,代码如下:

public class Singleton {
    private volatile static Singleton singleton;

    /**
     * 私有构造器
     */
    private Singleton() {
    }

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

下面来看一段调用:

 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton);
        Singleton reflectSingleton = (Singleton) Class.forName("org.white.whiteroot.Singleton").newInstance();
        System.out.println(reflectSingleton);
        System.out.println(singleton.equals(reflectSingleton));
  }

运行结果如下:

org.white.whiteroot.Singleton@4a574795
org.white.whiteroot.Singleton@f6f4d33
false

可以看到我们常自以为没问题的单例,很容易就被反射攻破了。

那么解决方案如何呢,我们都知道,反射其实还是会调用私有的无参数构造器,那么我们做如下改动如何(很多网上的结论都是下面这种方式):

// 新增属性
private static boolean constructed;
// 改写私有构造方法
private Singleton() {
        if (constructed) {
            throw new RuntimeException("Singleton has been constructed");
        }
        constructed = true;
}

此时再调用上面的测试方法,执行结果如下:

org.white.whiteroot.Singleton@4a574795
Exception in thread "main" java.lang.RuntimeException: Singleton has been constructed

可以看到通过反射构造失败了。
不过既然我们在用反射进行破坏,那么上面代码能经受住反射的考验吗?此处敲黑板。我们修改测试方法如下:

 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton);
        Class reflectSingletonClass = Class.forName("org.white.whiteroot.Singleton");
        Field field = reflectSingletonClass.getDeclaredField("constructed");
        field.setAccessible(true);
        field.set(singleton, false);
        Singleton reflectSingleton = (Singleton) reflectSingletonClass.newInstance();
        System.out.println(reflectSingleton);
        System.out.println(singleton.equals(reflectSingleton));
  }

再次运行,结果如下:

org.white.whiteroot.Singleton@4a574795
org.white.whiteroot.Singleton@23fc625e
false

我们发现,我们的单例再次被破坏了!
在我们日常编码中,很多事情都要动手去做,凡事多想一步。网上很多方案中都没有去尝试通过反射修改属性,这其实是一种不负责任的做法。
另外在overstackflow上有一种取巧的做法,不过也会存在反射的问题,其做法如下:

    private Singleton() {
        if (singleton != null) {
            throw new RuntimeException("Singleton has been constructed");
        }
    }

这种做法依旧会存在一个问题,这个问题其实在以上所有方法中都存在:

Class reflectSingletonClass = Class.forName("org.white.whiteroot.Singleton");
Field field = reflectSingletonClass.getDeclaredField("singleton");
field.setAccessible(true);
field.set(singleton, null);

通过如上方法在反射中讲singleton的值置为空,这样相当于完全破换掉单例,每次置空后通过调用getInstance方法都会生成新的对象

那么有没有不会被破坏的方式呢?答案当然是有的,从如下两个方案:

  1. 既然破坏单例都是通过反射破坏属性后调用无参构造器,那么我们可以把属性放到第三方去托管,比如把singleton放到redis托管,每次调用序列化回来,这样除非去攻击托管的地方,否则是改变不了咱们的单例的。
  2. 反射必定还是会调用构造器,那么咱们可以通过不调用构造器的方式来做单例对象,那就是枚举。

总结一句:目前来讲,通过枚举来防止反射是最优解决方案。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值