单例模式-23种设计模式系列

什么是单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

饿汉式单例模式


//饿汉式单例
public class Hungry {
    //由于初始化的时候类里面的内容会全部加载,可能造成内存浪费!!
    private byte[] data1=new byte[1024*1024];
    private byte[] data2=new byte[1024*1024];
    private byte[] data3=new byte[1024*1024];
    private byte[] data4=new byte[1024*1024];

    private Hungry(){
    }

    private final static Hungry HUNGRY=new Hungry();

    private static Hungry getInstance(){
        return HUNGRY;
    }

}

懒汉式单例模式

//懒汉式单例
public class LazyMan {

    private LazyMan(){
    }

    //先不初始化
    private static LazyMan LAZY_MAN;

    //只有为空的时候初始化
    private static LazyMan getInstance(){
        if (LAZY_MAN==null){
            LAZY_MAN=new LazyMan();
        }
        return LAZY_MAN;
    }
}

这样的单利在单线程下是可以的,但是在多线程时就会失效。

测试用例

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

在这里插入图片描述

DCL懒汉式单例模式

由于在多线程条件下,上面的懒汉式不安全,所以我们可以加一层锁,把class锁住,这样就只有一个class了。这就是双重检测锁懒汉式,又叫DCL懒汉式。

//双重检测锁
    private static LazyMan getInstance(){
        if (LAZY_MAN==null){//第一层检测
            synchronized (LazyMan.class){//锁
                if (LAZY_MAN==null){//第二层检测
                    LAZY_MAN=new LazyMan();
                }
            }
        }
        return LAZY_MAN;
    }

在这里插入图片描述

volatile

上面这样就真的安全了吗?

LAZY_MAN=new LazyMan();

我们知道new一个对象不算原子性操作,是由三步组成的。
1、分配内存空间 2、执行构造方法,初始化对象 3、把这个对象指向分配的空间。
正常情况,CPU是按123的顺序执行的,但是也有可能是132的顺序。如果线程A的执行顺序是132,在执行完13后,线程B开启,判断LAZY_MAN不为已经存在,但是指向的空间是没有初始化的,就出现了问题。
所以,我们最好是在定义LAZY_MAN时加上volatile关键字。

private volatile static LazyMan LAZY_MAN;

静态内部类实现单例模式

除了以上的基本方式,我们还可以使用静态内部类实现单例模式,但是也不安全,不推荐使用。(可以用于装B和炫技)


//静态内部类实现
public class Holder {
    private Holder(){
    }

    private static Holder getInstance(){
        return InnerHolder.HOLDER;
    }
    //写一个内部类创造HOLDER
    private static class InnerHolder{
        private final static Holder HOLDER=new Holder();
    }
}

反射破解DCL单例

只要有反射,任何代码都是不安全的。
DCL单例在多线程下也是安全的,不会被破坏,但是我们还是要问一句
== 这样就真的安全了吗 ==

//反射破解DCL单例
    public static void main(String[] args) throws Exception{
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在这里插入图片描述
那遇到这种情况,我们又该怎么解决呢?
由于反射是通过得到我们构造器来创造对象,所以我们可以在构造器里面加一个锁!确定只有一个构造器被初始化。

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

在这里插入图片描述
这样就真的安全了吗?
上面我们是new了一个lazyman的对象,所以在构造器可以检测到,但是如果两个对象都是用反射创建的,这样的单例还安全吗?

//反射破解DCL单例
    public static void main(String[] args) throws Exception{
        //LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance = constructor.newInstance();
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在这里插入图片描述
遇到这种情况又该如何解决呢?
我们可以额外的设置一个变量,设置一个初始值,如何在构造器里改变这个值,如果这个值改变了,就说明已经执行过了,就抛出异常。

//定义一个随机变量,变量名一般比较复杂,我这里用“樱岛麻衣”
    private static boolean yingdaomayi=false;

    private LazyMan(){
        if (!yingdaomayi)
            yingdaomayi=true;
        else {
            throw  new RuntimeException("不要试图破坏单例!!!");
        }
    }

在这里插入图片描述

这样就真的安全了吗?

如果有人对你的源码进行了反编译,得到了这个变量名(无论你设置的在复杂,都会有破解的办法),不就可以在创建完第一个对象后,再把这个变量设置为初始化,这样单例还安全吗?

//反射破解DCL单例
    public static void main(String[] args) throws Exception{
        //LazyMan instance = LazyMan.getInstance();
        Field yingdaomayi = LazyMan.class.getDeclaredField("yingdaomayi");
        yingdaomayi.setAccessible(true);
        Constructor<LazyMan> constructor =LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan instance = constructor.newInstance();
        yingdaomayi.set(instance,false);
        LazyMan instance2 = constructor.newInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }

在这里插入图片描述

遇到这种情况又要怎么解决呢?

我们进入instance源码可知,可知枚举是不能用反射破坏的。所以我们要在好好地认识一下枚举!

在这里插入图片描述

枚举

我们先创建一个enum测试一下她的单例是不是真的不能被反射破坏。

//enum本身也是一个class类
public enum  EnumSingle {
    ENUM_SINGLE;
    private EnumSingle getInstance(){
        return ENUM_SINGLE;
    }
}

class Test {
    public static void main(String[] args) {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }
}

在这里插入图片描述
我们运行后看一下target下面的class文件,发现生成的构造器是无参的,然后我们可以用反射测试一下。
在这里插入图片描述

public static void main(String[] args) throws Exception {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        //EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        EnumSingle enumSingle2 = constructor.newInstance();
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }

但是运行测试后出现了问题,找不到无参构造器。
在这里插入图片描述

运行结果是不会骗人的,所以只能是idea骗了我。

经过反编译工具,我们看到了真正的源码

在这里插入图片描述

发现并不是无参构造器,而是有参构造,类型是string和int。所以我们修改了我们的测试代码

public static void main(String[] args) throws Exception {
        EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
        //EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingle enumSingle2 = constructor.newInstance();
        System.out.println(enumSingle);
        System.out.println(enumSingle2);
    }

在这里插入图片描述

终于出现了应该的结果,枚举果然不能被反射破坏单例。

但是枚举真的不能被破解吗?

序列化也是可以破解的,这里就不深究了其实我也不会

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章