01、单例模式

GoF23

0、七大原则

第一个原则最重要。

其他的原则都有相应的优点和缺点,我们23种设计模式就是根据这些原则设置出来的

1、单例模式

程序员必会的,不会都丢人

1、饿汉式

比较饿,程序加载的时候就会创建!

优点:线程安全

缺点:如果类中的对象很多,会很消耗内存

package _01单例模式;


/**
 * @author zhangbingbing
 * @version 1.0
 * @date 2020/6/14 15:29
 */
public class Hungry {
    //饿汉式,无论是饿汉还是懒汉,万恶的反射都能破解
    private Hungry() {

    }
    //饿汉的缺点:这行代码解释很清晰的显示出来
    //类加载的时候就创建对象 则会加载类中的类变量到内存中 数组 集合等,很占内存
    private final static Hungry HUNGRY = new Hungry();  //线程安全
    //对外接口获取实例对象
    public static Hungry getInstance() {
        return HUNGRY;
    }
}
class MyYest{
    public static void main(String[] args) throws Exception {
        Hungry[] hungries = new Hungry[100];
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            new Thread(() -> {
                hungries[finalI] = Hungry.getInstance();
            }).start();
        }
        for (int i = 0; i < 100; i++) {
            System.out.println(i + ":" + hungries[i].hashCode());
        }
    }
}

2、懒汉式

我们需要的时候才创建对象

缺点:原始的线程不安全

所以我们要优化成 DCL :双重检测锁模式

有因为new对象的时候不是原子性操作所以我们要加上volatile关键字

这样才能把线程安全解决!

package _01单例模式;

/**
 * @author zhangbingbing
 * @version 1.0
 * @date 2020/6/9 22:57
 */
public class Lazy {
    //单例模式都必须把构造方法设置成私有的
    private Lazy() {
        //只要创建都会进入构造方法
        if (lazy == null) {
            System.out.println(Thread.currentThread().getName() + "-->");
        }
    }

    //懒汉自己不加载,等我们调用getInstance方法的时候才加载
    //缺点显而易见:当多个线程同时调用的时候会破坏单例
    private volatile static Lazy lazy;
    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                    //还是有问题
                    //因为在这里创建对象cpu内部其实是执行了三步操作
                    //也就是不是原子性的
                    //1、分配内存空间
                    //2、初始化对象
                    //3、把对象引用指向内存空间
                    //cpu三个顺序可以任意执行 132 出问题
                }
            }
        }

        return lazy;
    }
}
class MyTest {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            int num = i;
            new Thread(() -> {
                Lazy lazy = Lazy.getInstance();
            }).start();
        }
    }
}

3、反射破解

以上两种单例都会被反射破解,直接暴力破解构造函数

一下是最优的,不过还是不安全

package _01单例模式;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

/**
 * @author zhangbingbing
 * @version 1.0
 * @date 2020/6/14 16:17
 */
public class LazyAnno {
    //使用反射破解单例,加上反破解的方法
    //来个哨兵,这个哨兵只有我们知道,但是还是不安全,可以被破解,可以用反射设置成true
    private static boolean bingbing = true;
    private LazyAnno() {
        if (bingbing) {
            //没事
            bingbing = false;
        } else {
            throw new RuntimeException("不要试图用反射破坏单例!!");
        }
    }

    private volatile static LazyAnno lazyAnno;

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

class MyAnno{
    public static void main(String[] args) throws Exception{
        Constructor<LazyAnno> declaredConstructor = LazyAnno.class.getDeclaredConstructor(null);
        //暴力破解
        declaredConstructor.setAccessible(true);
        LazyAnno lazyAnno2 = declaredConstructor.newInstance();
        Field bingbing = LazyAnno.class.getDeclaredField("bingbing");
        bingbing.setAccessible(true);
        bingbing.set(lazyAnno2, true);
        LazyAnno lazyAnno1 = declaredConstructor.newInstance();
        System.out.println(lazyAnno1==lazyAnno2); //false

    }
}

当通过反编译知道那个哨兵的时候就可以破解!

所以我们迎来了天然的单例

枚举类型

package _01单例模式;

import java.lang.reflect.Constructor;

/**
 * @author zhangbingbing
 * @version 1.0
 * @date 2020/6/14 16:39
 */
public enum EnumSingle {
    //使用枚举来实现单例
    SINGLE;
    public static EnumSingle getInstance() {
        return SINGLE;
    }
}
class TestEnum{
    public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.getInstance();
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle enumSingle = declaredConstructor.newInstance();
        System.out.println(instance == enumSingle);

    }
}

这个报错:说没有这个无参构造

一顿分析发现idea骗了我们:

本身的构造器参数是String 和 int类型的

ok~得出最终答案

因为反射的底层不能破坏枚举,所以枚举是安全的单例

总结一下:

单例模式我们首先需要把构造器私有化,市面上主要有两种单例

  • 饿汉:我们直接在类的内部创建对象,类一加载就创建对象

缺点可能会很耗内存,但是线程安全

  • 懒汉

线程不安全所以我们一般使用DCL懒汉模式,双层检测锁模式,还要保证原则性

但是这两种都会被反射破解,还有一种天然安全的单例,就是枚举类型,因为反射源码里写了必能通过反射创建枚举类型的实例~

posted @ 2020-06-14 17:04  贝加尔湖畔╭  阅读(134)  评论(0编辑  收藏  举报