DCL单例模式及如何防御反射破坏单例

647 阅读3分钟

单例模式的3种写法

1.直接在方法上添加synchronized


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

}

     这种方法的缺点是是,每次执行这段代码都要先获取锁,这样一定程度上影响效率;

2.DCL(双检测加锁)

public class Single{

private volatile static Single instance;
private Single(){}

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

}

     DCL相比于上面简单粗暴的直接上锁,有一个好处,就是不用每次获取实例的时候都要上锁;效率在高并发的时候会有一点提升吧;

     解释一下,为什么要做2次的null判断?

     试想一下,这样一个场景:假设现在有5个线程同时执行到这段代码,同时都检测到instance == null;

     他们会竞争锁,但是最终只会有一个线程获得锁,也就是说有4个线程没有得到锁而卡在这儿,等待获得锁的线程执行完这段代码,释放锁之后剩余4个线程继续竞争;

     第一个获得锁的线程得到了这块代码的执行权,肯定会完成instance实例的初始化;而后面的4个线程如果此时不做null判断则会继续给instance赋一个新的Model对象,导致后续的线程和前一个线程获取到的不是一个实例对象,从而出错;这就是为什么要双重检测的原因;

这里面其实还有一个问题,就是为什么要在变量前添加==volatile==;不加会有什么问题?

private volatile Single instance;

volatile关键字有什么作用?

《深入理解java虚拟机第二版》(周志明)的解释 被volatile修饰的变量具备两种特性:

1.保证此变量对所有线程的可见性,这里的可见性是指当前线程修改了这个变量的更新对于其他线程来说是立即可见的,但不是线程安全的;

2,volatile的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有结果正确,但是并不保证变量的赋值操作顺序与代码中的执行顺序一致

在Java实例化的并不是原子操作其实大概有下面几步:

类加载,分配对象空间,初始化对象变量,为对象的变量赋值,返回对象地址给变量

  没有使用volatile关键字代码代码可能没有完全的完成对象实例化,就返回了这个实例的地址就可能造成一些意想不到的错误;因此在使用这种双检测加锁时一定要添加volatile关键字;

3.使用DCL的隐患

  使用上面的方法创建的单例模式会被反射破坏掉,利用反射获取到构造方法就可以创建实例了,代码如下:


	Class<Single> dclClass = Single.class;
        Constructor<Single> declaredConstructor = dclClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Single refSingle  =declaredConstructor.newInstance();

  在构造方法中使用变量来判断,如果instance != null 就报错;这样也是不行的;也可以通过反射来修改instance的值;

		private Single(){
			if(instance != null)throw new RuntimeException(" don't use reflect create object");
		}

攻击代码:

Class<Single> dclClass = Single.class;
Field instance = dclClass.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(null,null);
Constructor<Single> declaredConstructor = dclClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Single refSingle  =declaredConstructor.newInstance();

如何防御通过反射机制创建实例对象呢?

创建静态内部类


public class HungryMan {
    private HungryMan() {
        if(InnerSingle.single != null){
            throw new RuntimeException(" don't use reflect create object");
        }
    }
    public static HungryMan  getInstance(){
        return InnerSingle.single;
    }
    private static class InnerSingle{
        private static final HungryMan single = new HungryMan();
    }
}

  反射修改不了InnerSingle.single的值,因为被static final(静态常量)修饰了,初始化之后就不会变;如果通过反射修改也会报错;(只是final修饰,还是可以被反射修改的)