• 深入探讨单例模式


    最近学习了一下单例模式,看bilibili up主“狂神说Java”讲完后,发现大部分博客都少了一个很有趣的环节,不分享出来实在是太可惜了,原视频 https://www.bilibili.com/video/BV1K54y197iS

    1、了解单例

    这个部分小部分我相信很多博客都讲的很好,我就尽量精简了
    
    1. 注意:
    • 单例类只能有一个实例
    • 这个实例由自己创建
    • 这个实例必须提供给外界
    1. 关键:构造器私有化
    2. 创建方法:
    • 饿汉式
    • 懒汉式

    总结:我认为创建方法可以归根于两种,一种是饿汉式,我在类的加载的时候就创建;还有一种懒汉式,只有在我需要的时候才去创建

    2、思路及实现

    【饿汉模式最基本的实现】

    在类加载的时候就已经创建了,这个模式下,线程是安全的,不同的线程拿到的都是同一个实例,但是,这个也存在空间浪费的问题,我不需要的时候你也加载了。

    //饿汉模式
     public class HungerSingle {
        private static HungerSingle single = new HungerSingle();
        //构造器私有,外界不能通过构造方法new对象,保证唯一
        private HungerSingle() {
        }
        //提供外界获得该单例的方法,注意方法只能是static方法,因为没有类实例
        public static HungerSingle getInstance(){
            return single;
        }
    }
    

    【懒汉模式最基本的实现】

    为了解决上述那个空间浪费问题,这时候懒汉模式就起作用了,你需要我的时候我再去创建这个实例

    //懒汉模式
    public class LazySingle {
        private static LazySingle single;
        //构造器私有化,禁止外部new生成对象
        private LazySingle(){
        }
        //外界获得该单例的方法
        public static LazySingle getInstance(){
            if(single == null){
                single = new LazySingle();
            }
            return single;
        }
     }
    

    一位热心前辈的评论:“像你这样写单例,在我们公司是要被开除的。”
    趁我还是学生,怀着以后不被开除的心情,继续学习下去
    原来懒汉模式下,单例线程是不安全的。

    怎么测试呢?如下

    【测试懒汉模式线程不安全】

    //1、构造器
    private LazySingle(){
        System.out.println(Thread.currentThread().getName());
    }
    
    //创建十个线程
    for (int i = 0; i < 10; i++) {
        new Thread(()->{
             Singleton2.getInstance();
        }).start();
    }
    

    此时你会发现,构造方法调用了不止一次,说明没有实现预期的单例

    平时我们解决线程不安全的方法:不就是线程不安全嘛,那好办,加锁

    【双重检测锁/DCL】

    public class DCLSingle {
        private static DCLSingle single;
        private DCLSingle(){
        }
        public static DCLSingle getInstance(){
            //第一次判断,没有这个对象才加锁
            if(single == null){
                //哪个需要保护,就锁哪个
                synchronized (DCLSingle.class){
                    //第二次判断,没有就实例化
                    if(single == null){
                        single = new DCLSingle();
                    }
                }
            }
            return single;
        }
    
    }
    
    

    仔细和别人代码一比对,发现我少了个volatile关键字,这是啥玩意?
    不懂就问。

    【volatile】
    为了避免指令重排

    //上述代码声明上面加上volatile关键字
     private volatile static DCLSingle single;
    

    啥是volatile ?

    引用自别人博客
    https://www.cnblogs.com/YLsY/p/11295732.html

    加volatile是为了出现脏读的出现,保证操作的原子性

    1、原子性操作:不可再分割的操作
    例如:single = new DCLSingle();
    其实就是两步操作:
    ①new DCLSingle();//开辟堆内存
    ②singl指向对内存
    
    2、脏读
    Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。
    线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。
    并且每个线程不能访问其他线程的工作内存。
    变量的值何时从线程的工作内存写回主存,无法确定。
    
    3、指令重排
    single = new DCLSingle();
    先执行②
    后执行①
    //先指向堆内存,还未完成构造
    
    
    【模拟情况】
    ①线程1执行,在自己的工作内存定义引用,先指向堆内存,还未构造完成
    ②此时线程2执行,它进行判断,引用已经指向了内存,所以线程2,认为构造完成,实际还未构造完成
    

    还有一种差点忘记说了,也是菜鸟教程说建议使用的方式

    【静态内部类实现单例】

    public class Singleton {
        private Singleton(){}
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
        private Singleton getInstance(){
            return SingletonHolder .INSTANCE;
        }
    }
    

    你会发现它和前面讲的普通饿汉式很像,我把它也归于饿汉式一类,因为它也是直接就new Singleton,但是它却有着懒加载的效果,而这种方式是 Singleton 类被装载了,INSTANCE不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 INSTANCE。

    【建议】建议使用静态内部类实现


    3、如何破化单例(其它大部分博客没有的内容)

    在这里感谢b站up【狂神说java】

    在面试官面前装逼的时候来了

    java语言实现动态化的灵魂——反射,说:没有什么是我不能改变的,看我来如何操作。

    【反射破坏单例】

    public class DCLSingle {
        private static DCLSingle single;
        private DCLSingle(){
        }
        public static DCLSingle getInstance(){
            //第一次判断,没有这个对象才加锁
            if(single == null){
                //哪个需要保护,就锁哪个
                synchronized (DCLSingle.class){
                    //第二次判断,没有就实例化
                    if(single == null){
                        single = new DCLSingle();
                    }
                }
            }
            return single;
        }
        
        //通过反射破化单例
        public static void main(String[] args) throws Exception {
            LazySingle single = LazySingle.getInstance();
            Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            LazySingle single1 = constructor.newInstance();
            System.out.println(single == single1);//false
        }
    
    }
    

    得到单例类的构造器,然后通过newInstance的方法创建对象,很明显破化了单例

    【改进代码,防止你搞破化】

    既然这次你是通过得到构造器破化的,那我给构造器加个方法,如果你已经创建了实例,那就抛出异常

    private LazySingle(){
        synchronized(LazySingle.class){
            if(single!=null){
                throw new RuntimeException("破坏失败");
            }
        }
    }
    

    但是这个又有问题,这里的判断是private static DCLSingle single 是否有值,如果我们都不通过getInstance()方法创建对象,而是这样

    public static void main(String[] args) throws Exception {
     //   LazySingle single = LazySingle.getInstance();
        Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        
        //注意:这里的对象不是单例类中里面属性的那个对象
        LazySingle single = constructor.newInstance();
        LazySingle single1 = constructor.newInstance();
        System.out.println(single == single1);//false
    }
    

    这里根本不会抛出异常,而是又破坏了单例

    【继续改进代码,防止搞破化】
    简直就是相爱相杀呀,我们可以利用红路灯原理,防止破化
    改进构造方法

    //加个标志
    private static String sign = "password";
    private LazySingle(){
        synchronized(LazySingle.class){
            if(single!=null || !"password".equals(sign)){
                throw new RuntimeException("破坏失败");
            }else{
                sign = "no";
            }
        }
        
    }
    

    此刻你通过上述main()方法里面的内容测试,发现又会抛出异常。然而我们能通过反射获得构造方法,那我们同样也能通过反射获取对象的属性以及值吧

    【再度破化】

    public static void main(String[] args) throws Exception {
        Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Field field = LazySingle.class.getDeclaredField("sign");
        //此处省略通过反射获取该属性的类型和方法....
        LazySingle single1 = constructor.newInstance();
        //重新变回原标志位
        field.set("sign","password");
        LazySingle single2 = constructor.newInstance();
        System.out.println(single2 == single1);//false
    }
    

    又被破化了

    【再次改进】

    我们将目光抛向枚举,
    jdk1.5之后,出现枚举
    利用枚举实现不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化(菜鸟教程官方术语)

    public enum Singleton {  
        INSTANCE;  
        public Singleton getInstance() {  
            return INSTANCE
        }  
    }
    

    【反射能破化枚举的单例吗?】

    1. 我们先要了解枚举是啥,它的底层是怎么实现的
    2. 我们会发现枚举本身就是一个类
    3. 通过反编译工具,查看枚举底层的构造方法
    4. 通过反射获取构造方法
    5. 重复上述反射测试

    我们最终可以发现反射不能破化枚举的单例

    这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。(菜鸟教程官方)

    【总结】太难了

  • 相关阅读:
    关于iOS中页面启动加载的相关问题汇总
    文件上传与解析漏洞
    XSS跨站攻击
    SQL注入
    DOS&&Linux命令大全
    信息收集相关
    进制转化
    PYQT5 in Python
    将博客搬至CSDN
    Python报文操作模块scapy
  • 原文地址:https://www.cnblogs.com/yxm2020/p/12723418.html
Copyright © 2020-2023  润新知