彻底玩转单例模式___单例模式下的并发问题,暴力反射破坏单例,枚举如何拒绝反射破坏


首先是饿汉式单例(Hungry)

1. 私有单例对象。

2. 私有构造函数,拒绝其他类构造实例。

3. 提供getInstance方法供外部获取到这个单例。

//饿汉式单例
//一上来就加载对象了,就出现了懒汉式
public class Hungry {

    //可能会浪费空间
    private byte[] data0 = new byte[1024];
    private byte[] data1 = new byte[1024];
    private byte[] data2 = new byte[1024];
    private byte[] data3 = new byte[1024];

    private Hungry(){
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
    
}

但是饿汉式单例因为直接就new了实例对象,但是我外界并没有说要用到他,所以有可能造成空间的浪费。


这时就出现了懒汉式单例(LazyMan):

1. 声明对象变量,但是先不去分配空间。

2. 私有构造函数,拒绝其他类构造实例。

3. 等外界要用到这个单例时,才去分配内存。

public class LazyMan {

    private static LazyMan lazyMan;

    private LazyMan(){

    }

    public LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

但是,懒汉式单例也是有问题的,编写如下代码,添加了一个循环,开辟了很多的线程来获取单例,看看有什么问题 :

public class LazyMan {

    private static LazyMan lazyMan;

    private LazyMan(){

    }

    public static LazyMan getInstance(){
        if(lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                System.out.println(LazyMan.getInstance());
            },String.valueOf(i)).start();
        }
    }
}

让我们看看运行结果:

我们可以看到,居然会获取到两种单例!也就是说,在多线程并发情况下,单例可能不再是“单”例!这是因为同时有多个线程进入getInstance方法,都判断了 lazyMan 为 null ,这时他们都会进入if选择语句,去创建lazyMan对象!

那么我们都可以想到加锁操作,那么这里要如何去加锁呢?

这里,我们用synchronized锁class对象,并且使用双重检测机制:

    //双重检测锁模式
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

结果上看确实解决了并发问题,这就是DCL懒汉式单例。

但是这样就真的没问题了吗?

其实DCL懒汉式单例还是会有并发问题,因为在锁中的 lazyMan = new LazyMan();语句,不是一个原子性操作,他的操作分为三步:

1. 分配内存空间

2. 执行构造方法,初始化对象

3. 把对象指向分配的空间

 这里详细说说并发问题的出现原因,这里化为步骤来说:

1. A线程进入getInstance方法,判断为null,并且获取锁,获取锁后在进入二次判断,为null,进入语句

2. 不巧的是,此时系统对new的指令进行了指令重排先去分配空间,然后把对象指向这个空间,此时空对象已经占用了空间,所以有了地址值,在还没有来得及去执行构造方法时,

B线程进入方法。

3. 此时B进行第一层判断,发现layzMan不为空,那么他就会跳出选择语句,直接去执行下面的return返回!

但是,此时的lazyMan,还没有完成构造!

 所以说还是会有安全性问题,所以要在声明对象时加上volatile关键词,来防止指令重排。

所以说,这样就解决了这类安全性问题:


//懒汉式单例
public class LazyMan {

    private static volatile LazyMan lazyMan;

    private LazyMan(){

    }

    //双重检测锁模式
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

}

感觉此时的安全性已经得到了很高的保证,但是,真的还是安全吗?

答案是否定的,因为我们忘了Java中的一大恶霸,没错,他就是反射

    //反射
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyMan instance1 = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//反检测,取消 Java 语言访问检查,提升反射时间,也可以让我们访问到私有变量
        LazyMan instance2 = declaredConstructor.newInstance();//用反射去new单例对象
        System.out.println(instance1);
        System.out.println(instance2);
    }

先来一个简单的暴力反射,利用反射获取到class,并且通过反射的方式创造实例,让我们看看运行结果:

没错,又是两个“单”例!

遇到问题解决问题,我们就去尝试一下,是否能够去反制反射来对我们的破坏。

因为反射的切入点在获取到我们的构造函数,那么我们就在这里下手: 

    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan != null){
                throw new RuntimeException("不要试图用反射破坏单例");
            }
        }
    }

 先加锁,当单例已经存在时,我们就抛出反射破坏异常。

 

 看起来成功的阻止了反射再去创建单例,我们把双重检测升级成了三重检测,安全性能更上一层楼。

但是,道高一尺魔高一丈,我们只是解决了一种反射的破坏方式,那么,如果我根本就不去先通过getInstance方法去new这个实例,每次都只通过反射来newInstance,一直没有正式的实例对象,那么单例还是被破坏的。

 此外,我们还可以通过创建一个布尔类型实例变量来作为一个信号量,初值为false,当第一次进入构造函数时,执行构造并将信号量其置为true,此后再有别人进入构造方法时,发现信号量为true,就会抛出异常。

但是,这种还是可以通过反射来破坏,因为信号量属性也属于类的信息,我能获取到类的所有东西,甚至可以去修改你的信号量,这样还是能够拿捏你。


//懒汉式单例
public class LazyMan {

    private static volatile LazyMan lazyMan;

    private static boolean flag = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if(flag == false){
               flag = true;
            }else{
                throw new RuntimeException("不要使用反射去破坏!");
            }
        }
    }

    //双重检测锁模式
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    //反射
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        Field field = LazyMan.class.getDeclaredField("flag");
        field.setAccessible(true);//破坏私有权限
        declaredConstructor.setAccessible(true);//反检测,取消 Java 语言访问检查,提升反射时间,也可以让我们访问到私有变量
        LazyMan instance1 = declaredConstructor.newInstance();

        field.set(instance1,false);

        LazyMan instance2 = declaredConstructor.newInstance();//用反射去new单例对象
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

这样就又出现了多个“单”例。

所以说,道高一尺魔高一丈。

那么我们究竟要如何去解决反射造成的安全问题呢?

这时候我们就要到恶霸的老巢去看看了,newInstance源码:

原来如此,原来在恶霸里面我们做了判断,如果你要用反射去搞枚举类,那我就不会让你好过,我就会抛出异常,就相当于给猴子带金箍,你要是敢有邪念,我就念紧箍咒。

所以我们能看到,我们可以通过枚举类来反制反射的破坏。

那么,事不宜迟,我们就去创建一个枚举类。

枚举类的构造方法本来就是私有的,所以我们可以不用去写,用默认的就可以。

//enum枚举本身也是一个class类
//枚举单例
public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

这就是一个简单的枚举了,让我们通过反射去试试破坏。

 注意,这里我们要用反射获取的构造函数其实是有参数的,并不是表面上看到的无参,是一个String和int类的参数。

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle enumSingle1 = EnumSingle.INSTANCE;
        EnumSingle enumSingle2 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle3 = declaredConstructor.newInstance();

        System.out.println(enumSingle1);
        System.out.println(enumSingle2);
        System.out.println(enumSingle3);
        //这里报错,枚举不会被反射去破坏,其实枚举里面没有无参构造,
        //它里面是有参构造,通过getDeclaredConstructor(String.class,int.class)反射获取到有参构造
        //然后发现Cannot reflectively create enum objects异常,证明枚举类不允许反射来破坏
    }
}

那么编写完测试类,运行,我们发现了我们在newInstance里面看到的异常,也就是我们尝试通过反射去破坏枚举类的异常。

这样就证明了 枚举单例是如何去拒绝反射的侵入的。


此处完结,另外,还有一种单例模式,内部类单例模式,这里也写一下,通过在内部类里写上单例初始化语句实现。


//静态内部类
public class Holder {

    private Holder(){

    }

    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }


    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

感谢观看,感谢提出问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值