单例模式是我们非常常用的一种设计模式之一。
单例模式属于创建型模式,这种模式涉及到一个单一的类,该类负责创建自己的对象,并确保只有单个唯一的对象被创建。
单例模式要满足两个条件:
- 构造器私有化。因为要避免外界调用构造直接创建任意的多实例
- 提供返回唯一实例的方法
总所周知,单列模式分为懒汉式和饿汉式。
饿汉式:
饿汉式饿汉式是类一加载就创建实例。正因为如此,所以它是线程安全的,不会存在多线程下的安全问题。但是它的弊端就是类一加载就创建实例,比较耗费内存资源。
public class Hungry {
private final static Hungry hungry=new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return hungry;
}
}
懒汉式:
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){}
public static LazyMan getInstance(){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
懒汉式和饿汉式最本质的区别就是,懒汉式的单例对象是需要的时候才创建;而饿汉式是类一加载就创建实例。懒汉式的优点是与懒汉式相比更节省内存资源,正是因为这一点,它在多线程并发条件下会有问题: 当对象还没有创建时,有多线程同时进来,都会进入到if语句里创建实例,这时就这个类的实例就不是唯一的了。
因此我们需要通过synchronized的同步机制来解决懒汉式的线程安全问题。
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
我们将创建实例的整个if语句都放入同步代码块中来保证安全。这个时候可能会有同学要问了,为什么不在整个方法上加上synchronized来保证同步呢?
原因就是二者都可以保证同步。但是同步方法会锁住整个方法,只要调用该方法的线程都要进行锁的竞争,而用同步代码块取代对整个方法的同步,仅对方法中操作共享变量的代码加同步锁,这样就能使没有获取到锁的线程能执行同步代码块之前的代码,进而提高程序性能。
一般我们还在同步代码块的前面在加上一层if判断。当这个类的实例被创建出来了,其他的线程再调用这个方法时因为不满足if条件,就不会走同步代码块了。这也是为性能做考虑。
双重if检测的懒汉式也被称为DCL懒汉式
尽管DCL懒汉式解决了多线程的并发问题,但它还会存在一个问题:CPU指令重排
什么是指令重排序:为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。
我们要知道,new一个对象并不是原子性的操作。CPU要分三步来完成这个操作
- 分配内存空间
- 调用构造方法初始化对象
- 将对象指向内存空间
当第三步完成了,才能说是创建好了一个对象。
因为CPU的指令重排,上面的DCL懒汉式可能存在这么一个问题:当一个线程进来的new对象的时候,这时,CPU进行指令重排,可能会按132的顺序执行,先分配内存,然后将对象指向内存空间,然而当最后一步还没有执行的时候,又有一个线程进来了,因为这时对象已经指向内存空间了,if判断不成立,会直接返回一个null对象。
因此,DCL懒汉式中的静态成员变量对象还需要加上volatile关键字来保证有序性,禁止指令重排
public class LazyMan {
private volatile static LazyMan lazyMan;
private LazyMan(){}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();//这不是一个原子性操作
/*
1、分配内存空间
2、执行构造方法,初始化对象
3、将对象指向内存空间
*/
}
}
}
return lazyMan;
}
}
这个时候,DCL懒汉式在多线程条件下就不会存在问题了。但是,道高一尺魔高一丈啊,还是可以通过反射来破坏单例模式
我们可以通过反射 LazyMan.class.getDeclaredConstructor() 获取到这个类的构造器,然后用构造器的 newInstance() 方法创建多个对象。
public class LazyMan {
private volatile static LazyMan lazyMan;
private LazyMan(){}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
LazyMan lazyMan1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1==lazyMan2);//false
}
}
改进:在构造方法中进行判断,如果静态成员变量对象已经存在,就抛出异常
public class LazyMan {
private volatile static LazyMan lazyMan;
private LazyMan(){
synchronized (LazyMan.class) {
if (lazyMan != null) {
throw new RuntimeException("不要试图使用反射破坏单例!");
}
}
}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
此时,如果使用 getInstance() 创建并获取了对象,在通过反射获取构造器创建对象就会抛出异常。但是,我们一开始就不调用 getInstance() 获取对象,而是直接通过反射来创建对象,这时静态成员变量对象lazyMan一直是null,判断失效,单例模式还是被破坏了。
继续改进:信号灯法。使用一个标志位成员变量,在构造方法中进行标志位判断。
public class LazyMan {
private volatile static LazyMan lazyMan;
private static boolean zhiqian=false;
private LazyMan(){
synchronized (LazyMan.class) {
if (!zhiqian){
zhiqian=true;
}else {
throw new RuntimeException("不要试图使用反射来破坏单例");
}
}
}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
当第一次通过构造方法创建实例时,标志位由false变为ture,之后在使用反射调用构造方法创建实例,就会抛出异常。因此,只要不知道标志位变量名,无法通过反射改变标志位的值,就不能破坏单例。我们还可以通过对标志位变量名加密来尽量保证安全。
但这也只是相对安全。它还是可能会被反编译等手段获取到标志位变量名,然后通过反射进行篡改来达到破坏单例的效果!
public class LazyMan {
private volatile static LazyMan lazyMan;
private static boolean zhiqian=false;
private LazyMan(){
synchronized (LazyMan.class) {
if (!zhiqian){
zhiqian=true;
}else {
throw new RuntimeException("不要试图使用反射来破坏单例");
}
}
}
public static LazyMan getInstance(){
//双重检测锁模式的懒汉式单例:DCL懒汉式
if (lazyMan==null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//LazyMan lazyMan1 = LazyMan.getInstance();
Field zhiqian = LazyMan.class.getDeclaredField("zhiqian");
zhiqian.setAccessible(true);//通过反射获取标志位成员变量
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();//创建第一个对象实例
zhiqian.set(lazyMan1,false);//修改标志位的值
LazyMan lazyMan2 = declaredConstructor.newInstance();//创建第二个对象实例
System.out.println(lazyMan1);
System.out.println(lazyMan2);
System.out.println(lazyMan1==lazyMan2);//false
}
}
所以,单例模式在理论上来说是没有绝对安全的,都可以使用反射来进行破坏。
真正安全的办法:枚举
枚举是无法通过反射来进行创建对象实例的。我们通过探究反射的 newInstance() 方法的源码就可以知道原因:
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)//枚举判断
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
在通过反射获取构造器调用 newInstance() 创建对象时,底层进行了一个判断,如果这个类是枚举类,则直接抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
测试:
public enum EnumSingle {
INC;
public EnumSingle getInc(){
return INC;
}
}
class Test{
public static void main(String[] args) throws Exception {
EnumSingle single1=EnumSingle.INC;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle single2 = constructor.newInstance();
System.out.println(single1==single2);
}
}
以上结果直接抛出异常
至此,over~