单例模式

单例模式的几种形态

饿汉式

  • //饿汉式单例
    public class Hungry {
    
        private Hungry(){
    
        }
    
        private final static Hungry HUNGRY = new Hungry();
    
        public static Hungry getInstance(){
            return HUNGRY;
        }
    }

 

问题:

  • 可能浪费内存(饿汉式一开始就将程序中所有的东西都加载好,但有些东西暂时用不到,就会浪费内存)

DCL懒汉式

  • //懒汉式单例
    public class LazyMan {
    
        private LazyMan(){
            System.out.println(Thread.currentThread().getName()+"完成");
        }
    
        private volatile static LazyMan lazyMan;
    
    //    双重检测锁模式的懒汉式单例(DCL懒汉式)
        public static LazyMan getInstance(){
    //        加锁(此锁保证只有这个类只有一个)
            if(lazyMan==null){
                synchronized (LazyMan.class){
                    if(lazyMan==null){
                        /**
                         * 此代码会经过的步骤
                         * 1、分配内存空间
                         * 2、执行构造方法,初始化对象
                         * 3、把这个对象指向这个空间
                         * 平时我们期望的运行路线是123,但实际的运行路线可能是132
                         * A线路没问题,可以正常通过
                         * 但如果还有一个B线路,因为此对象已经先指向了这个空间
                         * B线路就会认为该构造对象不为null,但此时lazyMan还没有完成构造,可能会出现问题
                         */
                        //在极端情况下此部分代码是有问题的,因为它不是原子性操作
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    
    //    单线程下确实没问题
    //    多线程并发的情况下是有问题的,会同时实例化多个单例
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++){
                new Thread(()->{
                    LazyMan.getInstance();
                }).start();
            }
        }
    }

问题:

  • 一般的懒汉模式在单线程下没问题,但没有加synchronized锁的情况下在多线程环境下会出现生成不止一个对象的情况。

懒汉式单例实现思想为需要用到的时候再对程序中的东西进行加载,不需要时不加载,避免了浪费内存

静态内部类(炫技)

  • //静态内部类
    public class Holder {
        private Holder(){}
    
        public static Holder getInstance(){
            return InnerClass.HOLDER;
        }
    
        public static class InnerClass{
            private static final Holder HOLDER = new Holder();
        }
    }

单例最重要的思想是构造器私有

破坏单例模式

通过反射破坏单例模式(反射使用类的构造方法)

  • //懒汉式单例
    public class LazyMan {
    
        private LazyMan(){
            System.out.println(Thread.currentThread().getName()+"完成");
        }
    
        private volatile static LazyMan lazyMan;
    
    //    双重检测锁模式的懒汉式单例(DCL懒汉式)
        public static LazyMan getInstance(){
    //        加锁(此锁保证只有这个类只有一个)
            if(lazyMan==null){
                synchronized (LazyMan.class){
                    if(lazyMan==null){
                        /**
                         * 此代码会经过的步骤
                         * 1、分配内存空间
                         * 2、执行构造方法,初始化对象
                         * 3、把这个对象指向这个空间
                         * 平时我们期望的运行路线是123,但实际的运行路线可能是132
                         * A线路没问题,可以正常通过
                         * 但如果还有一个B线路,因为此对象已经先指向了这个空间
                         * B线路就会认为该构造对象不为null,但此时lazyMan还没有完成构造,可能会出现问题
                         */
                        //在极端情况下此部分代码是有问题的,因为它不是原子性操作
                        lazyMan = new LazyMan();
                    }
                }
            }
            return lazyMan;
        }
    
    //    在反射面前所有的单例模式都是不安全的
        public static void main(String[] args) throws Exception {
            LazyMan instance = LazyMan.getInstance();
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    //        Accessable属性是继承自AccessibleObject 类. 功能是启用或禁用安全检查。它不是让字段的访问权限变了,私有字段依然是私有字段,非本类对象依然是不能直接访问该字段的。它的作用是,不进行访问检查,当执行后面的操作的时候,JVM会直接忽略字段的访问控制符,直接进行操作。
            declaredConstructor.setAccessible(true);
            LazyMan instance2 = declaredConstructor.newInstance();
    
    //        此处获取的结果本该是一样的,因为他们是同一个单例模式,但实际上通过setAccessible绕过访问检查,通过declaredConstructor new了一个新的Instance,创建出了两个都指向单例模式的对象
            /**
             * 得出结果为:
             * LazyMan@16d3586
             * LazyMan@154617c
             * 得出结论:反射可以破坏单例模式
             */
    
            System.out.println(instance);
            System.out.println(instance2);
        }
    }

反射破坏单例模式的反制手段(构造方法加锁)

  •     private LazyMan(){
    //        反射破坏单例的反制手段
            synchronized (LazyMan.class){
    //            在此处判断类是否为空,为空正常创建,如果有值,则判定第二次创建是使用反射来破坏单例模式的
                if(lazyMan!=null){
                    throw new RuntimeException("拦截反射破坏单例");
                }
            }
            System.out.println(Thread.currentThread().getName()+"完成");
        }

因为反射走了单例模式的无参构造器,所以可以在构造器中加把锁进行判断

通过反射破坏单例模式(不使用类的构造方式)

  •     public static void main(String[] args) throws Exception {
    //        LazyMan instance = LazyMan.getInstance();
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            LazyMan instance = declaredConstructor.newInstance();
            LazyMan instance2 = declaredConstructor.newInstance();
    
            System.out.println(instance);
            System.out.println(instance2);

反射破坏单例模式的反制手段(信号灯)

信号灯模式:

生产者生产,消费者等待,生产完成后通知消费。

消费者消费,生产者等待,消费者消费完成后通知生产

private static boolean wenying = false;
private LazyMan(){
            if(wenying == false){
                wenying = true;
            }else {
                throw new RuntimeException("拦截反射破坏单例");
            }
        }
    }

通过反射破坏单例模式(知道标志位的名)

    public static void main(String[] args) throws Exception {

//        假设知道标志位的name,在此处获取标志位的值
        Field wenying = LazyMan.class.getDeclaredField("wenying");
//        再次通过setAccessible绕过访问检查
        wenying.setAccessible(true);


        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        
        LazyMan instance = declaredConstructor.newInstance();
//        更改标志位的值
        wenying.set(instance,false);

        LazyMan instance2 = declaredConstructor.newInstance();
        
        System.out.println(instance);
        System.out.println(instance2);
    }

如何才能保证单例模式不被破坏?

根据查看源码得知,如果反射传过来的是一个枚举常量,则会提示不能使用反射破坏枚举

举例枚举是否可以被反编译

通过target查看EnumSingle源码为

public enum EnumSingle {
    INSTANCE;

    private EnumSingle() {
    }

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

那么此时可以使用之前用过的,反射使用类的构造方法来破坏枚举:

//enum是一个什么?  本身也是一个class类
public enum  EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

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

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行结果为:Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>(),并不是源码中的Cannot reflectively create enum objects。翻译为EnumSingle中没有无参构造。

但是在target中这个类确实是有无参构造的,进入该类的文件夹地址,使用cmd通过javap进行反编译,能得到枚举类其实就是java类继承了枚举的结论,但此时编译出来,依然有无参构造方法,实际运行时报错明明已经说明了此类没有无参构造。

使用jad反编译工具再次进行编译,将该类转换为java类,此时原本类中的无参构造变为了构造方法(String s,int i)的方法

// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingle.java


public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

再次回到程序中进行试验

Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);

最终运行结果为Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects,运行正确

得出结论:枚举类型参数无法被反射破坏。

个人问题:

不清楚源码是否有方法修改,如果可以,将源码中限制枚举类型的代码去掉,这种方式也被破解了