• 单例模式使用饿汉式和懒汉式创建一定安全?很多人不知


    概述

    单例模式大概是23种设计模式里面用的最多,也用的最普遍的了,也是很多很多人一问设计模式都有哪些必答的第一种了;我们先复习一下饿汉式和懒汉式的单例模式,再谈其创建方式会带来什么问题,并一一解决!还是老规矩,先上代码,不上代码,纸上谈兵咱把握不住。

    饿汉式代码

    复制代码
        public class SingleHungry
        {
            private readonly static SingleHungry _singleHungry = new SingleHungry();
            private SingleHungry()
            {
            }
            public static SingleHungry GetSingleHungry()
            {
                return _singleHungry;
            }
        }
    复制代码

    代码很简单,意思也很明确,接着我们写点代码测试验证一下;

    第一种测试: 构造函数私有的,new的时候报错,因为我们的构造函数是私有的。

     SingleHungry  _singleHungry=new SingleHungry();
    第二种测试: 比对创建多个对象,然后多个对象的Hashvalue
    复制代码
    public class SingleHungryTest
        {
            public static void FactTestHashCodeIsSame()
            {
                Console.WriteLine("单例模式.饿汉式测试!");
                var single1 = SingleHungry.GetSingleHungry();
                var single2 = SingleHungry.GetSingleHungry();
                var single3 = SingleHungry.GetSingleHungry();
                Console.WriteLine(single1.GetHashCode());
                Console.WriteLine(single2.GetHashCode());
                Console.WriteLine(single3.GetHashCode());
            }
        }
    复制代码
    测试下来,三个对象的hash值是一样的。如下图:

    饿汉式结论总结

    饿汉式的单例模式不推荐使用,因为还没调用,对象就已经创建,造成资源的浪费;

    懒汉式代码

    复制代码
        public class SingleLayMan
        {
            //1、私有化构造函数
            private SingleLayMan()
            {
    
            }
            //2、声明静态字段  存储我们唯一的对象实例
            private static SingleLayMan _singleLayMan;
            //通过方法 创建实例并返回
            public static SingleLayMan GetSingleLayMan1()
            {
                //这种方式不可用  会创建多个对象,谨记
                return _singleLayMan = new SingleLayMan();
            }
            /// 
            ///懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费
            /// 
            /// 
            public static SingleLayMan GetSingleLayMan2()
            {
                if (_singleLayMan == null)
                {
                    Console.WriteLine("我被创建了一次!");
                    _singleLayMan = new SingleLayMan();
                }
                return _singleLayMan;
            }
        }
    复制代码

    测试代码

    复制代码
     public class SingleLayManTest
        {
            /// 
            /// 会创建多个对象.hash值不一样
            /// 
            public static void FactTest()
            {
                Console.WriteLine("单例模式.懒汉式测试!");
                var singleLayMan1 = SingleLayMan.GetSingleLayMan1();
                var singleLayMan2 = SingleLayMan.GetSingleLayMan1();
                Console.WriteLine(singleLayMan1.GetHashCode());
                Console.WriteLine(singleLayMan2.GetHashCode());
            }
            /// 
            /// 单例模式.懒汉式测试:懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费,但会有线程安全问题
            /// 
            public static void FactTest1()
            {
                Console.WriteLine("单例模式.懒汉式测试!");
                var singleLayMan1 = SingleLayMan.GetSingleLayMan2();
                var singleLayMan2 = SingleLayMan.GetSingleLayMan2();
                Console.WriteLine(singleLayMan1.GetHashCode());
                Console.WriteLine(singleLayMan2.GetHashCode());
            }
            /// 
            /// 单例模式.懒汉式多线程环境测试!
            /// 
            public static void FactTest2()
            {
                Console.WriteLine("单例模式.懒汉式多线程环境测试!");
                for (int i = 0; i < 10; i++)
                {
                    new Thread(() =>
                    {
                        SingleLayMan.GetSingleLayMan2();
                    }).Start();
                }
    
                //Parallel.For(0, 10, d => {
                //    SingleLayMan.GetSingleLayMan2();
                //});
            }
        }
    复制代码

    懒汉式结论总结

    懒汉式的代码如上已经概述,上面GetSingleLayMan1()会创建多个对象,这个没什么好说的,肯定不推荐使用;GetSingleLayMan2()是大多数人经常使用的,可解决刚才因为饿汉式创建带来的缺点,但也带来了多线程的问题,如果不考虑多线程,那是够用了。



    话说回来,既然刚才饿汉式和懒汉式各有其优缺点,那我们该如何抉择呢?到底选择哪一种?

    其它方式创建单例—饿汉式+静态内部类

    复制代码
        public class SingleHungry2
        {
            public static SingleHungry2 GetSingleHungry()
            {
                return InnerClass._singleHungry;
            }       
            public static class InnerClass
            {
                public readonly static SingleHungry2 _singleHungry = new SingleHungry2();
            }
        }
    复制代码

    这个代码,用了饿汉式结合静态内部类来创建单例,线程也安全,不失为创建单例的一种办法。

    其它方式创建单例—懒汉式+反射

     首先我们解决一下刚才懒汉式创建单例的线程安全问题,上代码:

    复制代码
     /// 
        /// 通过反射破坏创建对象
        /// 
        public class SingleLayMan1
        { 
            //私有化构造函数
            private SingleLayMan1()
            {
            }
            //2、声明静态字段  存储我们唯一的对象实例
            private static SingleLayMan1? _singleLayMan;
            private static object _oj = new object();

    /// /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源 /// public static SingleLayMan1 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan1(); Console.WriteLine("我被创建了一次!"); } } } return _singleLayMan; } }
    复制代码

    具体描述,在代码里面已经说得足够清楚,一看肯定明白,我们还是写点测试代码,验证一下,上代码:

    复制代码
    public class SingleLayManTest1
        {
            public static void FactTestReflection()
            {
                var singleLayMan1= SingleLayMan1.GetSingleLayMan();
    
                var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan1");
                //获取私有的构造函数
                var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
                //执行构造函数
                SingleLayMan1 singleLayMan = (SingleLayMan1)ctors[0].Invoke(null);
                Console.WriteLine(singleLayMan1.GetHashCode());
                Console.WriteLine(singleLayMan.GetHashCode());
            }
        }
    复制代码

    上面的代码分别通过SingleLayMan1.GetSingleLayMan2()和反射创建对象,输出二者对象hash值比较,结果肯定是不一样的,重点是我们可以通过反射创建对象。

    通过上面的代码,不知道大家有没有意识到我们虽通过加锁解决了线程安全问题,但仍会出现问题;正常创建对象的顺序是:

    1、new 在内存中开辟空间
    2、 执行构造函数 创建对象
    3、 把空间指向我们的对像

    但如果因为我们的程序使用多线程,则会发生"指令重排",本应执行顺序为1、2、3,实际执行顺序为1、3、2,但这种情况很少,不过我们写程序嘛,肯定追求严谨一点准没错。

    如果需要解决该问题需要给定义的私有局部变量加关键字 加上volatile (意思不稳定的 ,可变的) ,加该关键字可以避免指令重排。具体代码主要是这句如下:

     private volatile static SingleLayMan? _singleLayMan;

     

    到这里,大家认为还有没有问题?答案是肯定的,不然我就不会写这篇文章了,通过反射既然可以创建对象,那么我们写的创建实例代码还有什么意义,有没有什么办法避免反射创建对象呢?

    如果认真看了之前的反射创建对象代码,肯定发现反射是通过构造函数来创建对象的,那么我们相应的就在构造函数处理一下。来,我们继续上代码:

    复制代码
     /// 
        /// 解决反射创建对象的问题
        /// 
        public class SingleLayMan3
        {
            //2、声明静态字段  存储我们唯一的对象实例
            private volatile static SingleLayMan3? _singleLayMan;
            private static object _oj = new object();
            //私有化构造函数
            private SingleLayMan3()
            {
                lock (_oj)
                {
                    if (_singleLayMan != null)
                    {
                        throw new Exception("不要通过反射来创建对像!");
                    }
                }
            }
    
            /// 
            /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
            /// 
            public static SingleLayMan3 GetSingleLayMan()
            {
                if (_singleLayMan == null)
                {
                    lock (_oj)
                    {
                        if (_singleLayMan == null)
                        {
                            _singleLayMan = new SingleLayMan3();
                            Console.WriteLine("我被创建了一次!");
                        }
                    }
                }           
                return _singleLayMan;
            }
           
        }
    复制代码

    下面继续上测试代码,验证一下:

    复制代码
    public class SingleLayManTest3
        {
            /// 
            /// 第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常
            /// 
            public static void FactTestReflection()
            {
                var singleLayMan1= SingleLayMan3.GetSingleLayMan();
    
                var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan3");
                //获取私有的构造函数
                var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
                //执行构造函数
                SingleLayMan3 singleLayMan = (SingleLayMan3)ctors[0].Invoke(null);
                Console.WriteLine(singleLayMan1.GetHashCode());
                Console.WriteLine(singleLayMan.GetHashCode());
            }
        }
    复制代码

    结论其实测试方法已经说明:第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常。

    其实到这里,有人肯定发现了问题,第一次通过去执行自己写的创建单例方法来创建对象,后面再执行反射时才会报异常,那有没有什么办法,只要有人第一次反射创建对象时就报异常呢?

    定义局部变量解决反射创建对象问题

    复制代码
     public class SingleLayMan4
        {
            //2、声明静态字段  存储我们唯一的对象实例
            private volatile static SingleLayMan4? _singleLayMan;
            private static object _oj = new object();
            private static bool _isOk = false;
            //私有化构造函数
            private SingleLayMan4()
            {
                lock (_oj)
                {
                    if (_isOk == false)
                    {
                        _isOk = true;
                    }
                    else
                    {
                        throw new Exception("不要通过反射来创建对像!只有第一次通过反射创建对象会成功!请做第一个吃葡萄的人!");
                    }
                }
            }
    
            /// 
            /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
            /// 
            public static SingleLayMan4 GetSingleLayMan()
            {
                if (_singleLayMan == null)
                {
                    lock (_oj)
                    {
                        if (_singleLayMan == null)
                        {
                            _singleLayMan = new SingleLayMan4();
                            Console.WriteLine("我被创建了一次!");
                        }
                    }
                }           
                return _singleLayMan;
            }
           
        }
    复制代码

    测试代码,验证一下:

    复制代码
    public static void FactTestReflection()
            {
                //第一次创建对象会成功
                var singleLayMan1 = GetReflectionSingleLayMan4Instance();
    
                //第二次创建对象会失败,报异常
               var singleLayMan2 = GetReflectionSingleLayMan4Instance();
    
                Console.WriteLine(singleLayMan1.GetHashCode());
            }
            private static SingleLayMan4 GetReflectionSingleLayMan4Instance()
            {
                var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
                //获取私有的构造函数
                var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
                //执行构造函数
                SingleLayMan4 singleLayMan = (SingleLayMan4)ctors[0].Invoke(null);
                return singleLayMan;
            }
    复制代码

    第一次创建对象会成功,因为执行构造函数时没有执行GetSingleLayMan(),跨过了new,导致_isOk赋值true,第二次反射创建执行构造函数时判断变量_isOk为true,走入异常逻辑。

    但这样做真的就安全了吗?既然可以通过反射执行构造函数来创建对象,那也可以通过反射改变局部变量_isOk 的值,上代码:

    复制代码
            /// 
            /// 通过反射也可以改变局部变量_isOk的值,继续创建对象
            /// 
            public static void FactTestReflection2()
            {
                Type type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
                //获取私有的构造函数
                var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
                //执行构造函数
                SingleLayMan4 singleLayMan1 = (SingleLayMan4)ctors[0].Invoke(null);
                FieldInfo fieldInfo =  type.GetField("_isOk", BindingFlags.NonPublic | BindingFlags.Static);
                fieldInfo.SetValue("_isOk", false);
                SingleLayMan4 singleLayMan2 = (SingleLayMan4)ctors[0].Invoke(null);
    
                Console.WriteLine(singleLayMan1.GetHashCode());
                Console.WriteLine(singleLayMan2.GetHashCode());
            }
    复制代码

    最后

    大家或许发现了,只要有反射存在,哪怕你的逻辑写的再严谨,它仍然可以反射创建对象,只因为它是反射!所以,单例模式的安全性也是相对而言的,具体选择用哪个,取决项目的业务场景了。如有发现问题,欢迎不吝赐教!

    源码地址:https://gitee.com/mhg/design-mode-demo.git

    
    
  • 相关阅读:
    微信小程序底层框架实现原理
    解决 webpack 4.X:autoprefixer 插件使用不起作用的两种解决方案
    软件压力测试:测试方法与步骤详解
    【算法】传智杯练习赛:平等的交易
    Leetcode 1572.矩阵对角线元素之和
    独立站运营中如何提升客户留存率?客户细分很重要!
    水稻育种技术全球领先海外市场巨大 国稻种芯百团计划行动
    ThreeJS-3D教学二基础形状展示
    GitHub上标星75k+的《Java面试突击版》到底有多牛?看完内容我服了!
    【算法】冒泡排序
  • 原文地址:https://www.cnblogs.com/mhg215/p/16548218.html