单例模式是我们非常常用的一种设计模式之一。

单例模式属于创建型模式,这种模式涉及到一个单一的类,该类负责创建自己的对象,并确保只有单个唯一的对象被创建。

单例模式要满足两个条件:

  • 构造器私有化。因为要避免外界调用构造直接创建任意的多实例
  • 提供返回唯一实例的方法

总所周知,单列模式分为懒汉式和饿汉式。

饿汉式:
饿汉式饿汉式是类一加载就创建实例。正因为如此,所以它是线程安全的,不会存在多线程下的安全问题。但是它的弊端就是类一加载就创建实例,比较耗费内存资源。

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要分三步来完成这个操作

  1. 分配内存空间
  2. 调用构造方法初始化对象
  3. 将对象指向内存空间

当第三步完成了,才能说是创建好了一个对象。
因为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~

最后修改:2021 年 07 月 18 日 03 : 49 PM
如果觉得我的文章对你有用,请随意赞赏