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

宁睿  金牌会员 | 2022-9-16 17:12:09 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 953|帖子 953|积分 2869

概述

单例模式大概是23种设计模式里面用的最多,也用的最普遍的了,也是很多很多人一问设计模式都有哪些必答的第一种了;我们先复习一下饿汉式和懒汉式的单例模式,再谈其创建方式会带来什么问题,并一一解决!还是老规矩,先上代码,不上代码,纸上谈兵咱把握不住。
饿汉式代码
  1.     public class SingleHungry
  2.     {
  3.         private readonly static SingleHungry _singleHungry = new SingleHungry();
  4.         private SingleHungry()
  5.         {
  6.         }
  7.         public static SingleHungry GetSingleHungry()
  8.         {
  9.             return _singleHungry;
  10.         }
  11.     }
复制代码
代码很简单,意思也很明确,接着我们写点代码测试验证一下;
第一种测试: 构造函数私有的,new的时候报错,因为我们的构造函数是私有的。
  1. SingleHungry  _singleHungry=new SingleHungry();
复制代码
  1. 第二种测试: 比对创建多个对象,然后多个对象的Hashvalue
复制代码
  1. public class SingleHungryTest
  2.     {
  3.         public static void FactTestHashCodeIsSame()
  4.         {
  5.             Console.WriteLine("单例模式.饿汉式测试!");
  6.             var single1 = SingleHungry.GetSingleHungry();
  7.             var single2 = SingleHungry.GetSingleHungry();
  8.             var single3 = SingleHungry.GetSingleHungry();
  9.             Console.WriteLine(single1.GetHashCode());
  10.             Console.WriteLine(single2.GetHashCode());
  11.             Console.WriteLine(single3.GetHashCode());
  12.         }
  13.     }
复制代码
  1. 测试下来,三个对象的hash值是一样的。如下图:<br><img src="https://img2022.cnblogs.com/blog/1046844/202208/1046844-20220803161551708-1324606779.png" alt="">
复制代码
饿汉式结论总结
  1. 饿汉式的单例模式不推荐使用,因为还没调用,对象就已经创建,造成资源的浪费;<br>
复制代码
懒汉式代码
  1.     public class SingleLayMan
  2.     {
  3.         //1、私有化构造函数
  4.         private SingleLayMan()
  5.         {
  6.         }
  7.         //2、声明静态字段  存储我们唯一的对象实例
  8.         private static SingleLayMan _singleLayMan;
  9.         //通过方法 创建实例并返回
  10.         public static SingleLayMan GetSingleLayMan1()
  11.         {
  12.             //这种方式不可用  会创建多个对象,谨记
  13.             return _singleLayMan = new SingleLayMan();
  14.         }
  15.         /// <summary>
  16.         ///懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费
  17.         /// </summary>
  18.         /// <returns></returns>
  19.         public static SingleLayMan GetSingleLayMan2()
  20.         {
  21.             if (_singleLayMan == null)
  22.             {
  23.                 Console.WriteLine("我被创建了一次!");
  24.                 _singleLayMan = new SingleLayMan();
  25.             }
  26.             return _singleLayMan;
  27.         }
  28.     }
复制代码
测试代码
  1. public class SingleLayManTest
  2.     {
  3.         /// <summary>
  4.         /// 会创建多个对象.hash值不一样
  5.         /// </summary>
  6.         public static void FactTest()
  7.         {
  8.             Console.WriteLine("单例模式.懒汉式测试!");
  9.             var singleLayMan1 = SingleLayMan.<strong>GetSingleLayMan1</strong>();
  10.             var singleLayMan2 = SingleLayMan.<strong>GetSingleLayMan1</strong>();
  11.             Console.WriteLine(singleLayMan1.GetHashCode());
  12.             Console.WriteLine(singleLayMan2.GetHashCode());
  13.         }
  14.         /// <summary>
  15.         /// 单例模式.懒汉式测试:懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费,但会有线程安全问题
  16.         /// </summary>
  17.         public static void FactTest1()
  18.         {
  19.             Console.WriteLine("单例模式.懒汉式测试!");
  20.             var singleLayMan1 = SingleLayMan.<strong>GetSingleLayMan2</strong>();
  21.             var singleLayMan2 = SingleLayMan.<strong>GetSingleLayMan2</strong>();
  22.             Console.WriteLine(singleLayMan1.GetHashCode());
  23.             Console.WriteLine(singleLayMan2.GetHashCode());
  24.         }
  25.         /// <summary>
  26.         /// 单例模式.懒汉式多线程环境测试!
  27.         /// </summary>
  28.         public static void FactTest2()
  29.         {
  30.             Console.WriteLine("单例模式.懒汉式多线程环境测试!");
  31.             for (int i = 0; i < 10; i++)
  32.             {
  33.                 new Thread(() =>
  34.                 {
  35.                     SingleLayMan.GetSingleLayMan2();
  36.                 }).Start();
  37.             }
  38.             //Parallel.For(0, 10, d => {
  39.             //    SingleLayMan.GetSingleLayMan2();
  40.             //});
  41.         }
  42.     }
复制代码
懒汉式结论总结

懒汉式的代码如上已经概述,上面GetSingleLayMan1()会创建多个对象,这个没什么好说的,肯定不推荐使用;GetSingleLayMan2()是大多数人经常使用的,可解决刚才因为饿汉式创建带来的缺点,但也带来了多线程的问题,如果不考虑多线程,那是够用了。
话说回来,既然刚才饿汉式和懒汉式各有其优缺点,那我们该如何抉择呢?到底选择哪一种?
其它方式创建单例—饿汉式+静态内部类
  1.     public class SingleHungry2
  2.     {
  3.         public static SingleHungry2 GetSingleHungry()
  4.         {
  5.             return InnerClass._singleHungry;
  6.         }      
  7.         public static class InnerClass
  8.         {
  9.             public readonly static SingleHungry2 _singleHungry = new SingleHungry2();
  10.         }
  11.     }
复制代码
这个代码,用了饿汉式结合静态内部类来创建单例,线程也安全,不失为创建单例的一种办法。
其它方式创建单例—懒汉式+反射

 首先我们解决一下刚才懒汉式创建单例的线程安全问题,上代码:
  1. /// <summary>
  2.     /// 通过反射破坏创建对象
  3.     /// </summary>
  4.     public class SingleLayMan1
  5.     {
  6.         //私有化构造函数
  7.         private SingleLayMan1()
  8.         {
  9.         }
  10.         //2、声明静态字段  存储我们唯一的对象实例
  11.         private static SingleLayMan1? _singleLayMan;
  12.         private static object _oj = new object();<br><br>         /// <summary>
  13.         /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
  14.         /// </summary>
  15.         public static SingleLayMan1 GetSingleLayMan()
  16.         {
  17.             if (_singleLayMan == null)
  18.             {
  19.                 lock (_oj)
  20.                 {
  21.                     if (_singleLayMan == null)
  22.                     {
  23.                         _singleLayMan = new SingleLayMan1();
  24.                         Console.WriteLine("我被创建了一次!");
  25.                     }
  26.                 }
  27.             }           
  28.             return _singleLayMan;
  29.         }        
  30.     }
复制代码
具体描述,在代码里面已经说得足够清楚,一看肯定明白,我们还是写点测试代码,验证一下,上代码:
  1. public class SingleLayManTest1
  2.     {
  3.         public static void FactTestReflection()
  4.         {
  5.             var singleLayMan1= SingleLayMan1.GetSingleLayMan();
  6.             var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan1");
  7.             //获取私有的构造函数
  8.             var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
  9.             //执行构造函数
  10.             SingleLayMan1 singleLayMan = (SingleLayMan1)ctors[0].Invoke(null);
  11.             Console.WriteLine(singleLayMan1.GetHashCode());
  12.             Console.WriteLine(singleLayMan.GetHashCode());
  13.         }
  14.     }
复制代码
上面的代码分别通过SingleLayMan1.GetSingleLayMan2()和反射创建对象,输出二者对象hash值比较,结果肯定是不一样的,重点是我们可以通过反射创建对象。
通过上面的代码,不知道大家有没有意识到我们虽通过加锁解决了线程安全问题,但仍会出现问题;正常创建对象的顺序是:
1、new 在内存中开辟空间
2、 执行构造函数 创建对象
3、 把空间指向我们的对像
但如果因为我们的程序使用多线程,则会发生"指令重排",本应执行顺序为1、2、3,实际执行顺序为1、3、2,但这种情况很少,不过我们写程序嘛,肯定追求严谨一点准没错。
如果需要解决该问题需要给定义的私有局部变量加关键字 加上volatile (意思不稳定的 ,可变的) ,加该关键字可以避免指令重排。具体代码主要是这句如下:
  1. private volatile static SingleLayMan? _singleLayMan;
复制代码
 
到这里,大家认为还有没有问题?答案是肯定的,不然我就不会写这篇文章了,通过反射既然可以创建对象,那么我们写的创建实例代码还有什么意义,有没有什么办法避免反射创建对象呢?
如果认真看了之前的反射创建对象代码,肯定发现反射是通过构造函数来创建对象的,那么我们相应的就在构造函数处理一下。来,我们继续上代码:
  1. /// <summary>
  2.     /// 解决反射创建对象的问题
  3.     /// </summary>
  4.     public class SingleLayMan3
  5.     {
  6.         //2、声明静态字段  存储我们唯一的对象实例
  7.         private volatile static SingleLayMan3? _singleLayMan;
  8.         private static object _oj = new object();
  9.         //私有化构造函数
  10.         private SingleLayMan3()
  11.         {
  12.             lock (_oj)
  13.             {
  14.                 if (_singleLayMan != null)
  15.                 {
  16.                     throw new Exception("不要通过反射来创建对像!");
  17.                 }
  18.             }
  19.         }
  20.         /// <summary>
  21.         /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
  22.         /// </summary>
  23.         public static SingleLayMan3 GetSingleLayMan()
  24.         {
  25.             if (_singleLayMan == null)
  26.             {
  27.                 lock (_oj)
  28.                 {
  29.                     if (_singleLayMan == null)
  30.                     {
  31.                         _singleLayMan = new SingleLayMan3();
  32.                         Console.WriteLine("我被创建了一次!");
  33.                     }
  34.                 }
  35.             }           
  36.             return _singleLayMan;
  37.         }
  38.       
  39.     }
复制代码
下面继续上测试代码,验证一下:
  1. public class SingleLayManTest3
  2.     {
  3.         /// <summary>
  4.         /// 第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常
  5.         /// </summary>
  6.         public static void FactTestReflection()
  7.         {
  8.             var singleLayMan1= SingleLayMan3.GetSingleLayMan();
  9.             var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan3");
  10.             //获取私有的构造函数
  11.             var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
  12.             //执行构造函数
  13.             SingleLayMan3 singleLayMan = (SingleLayMan3)ctors[0].Invoke(null);
  14.             Console.WriteLine(singleLayMan1.GetHashCode());
  15.             Console.WriteLine(singleLayMan.GetHashCode());
  16.         }
  17.     }
复制代码
结论其实测试方法已经说明:第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常。
其实到这里,有人肯定发现了问题,第一次通过去执行自己写的创建单例方法来创建对象,后面再执行反射时才会报异常,那有没有什么办法,只要有人第一次反射创建对象时就报异常呢?
定义局部变量解决反射创建对象问题
  1. public class SingleLayMan4
  2.     {
  3.         //2、声明静态字段  存储我们唯一的对象实例
  4.         private volatile static SingleLayMan4? _singleLayMan;
  5.         private static object _oj = new object();
  6.         private static bool _isOk = false;
  7.         //私有化构造函数
  8.         private SingleLayMan4()
  9.         {
  10.             lock (_oj)
  11.             {
  12.                 if (_isOk == false)
  13.                 {
  14.                     _isOk = true;
  15.                 }
  16.                 else
  17.                 {
  18.                     throw new Exception("不要通过反射来创建对像!只有第一次通过反射创建对象会成功!请做第一个吃葡萄的人!");
  19.                 }
  20.             }
  21.         }
  22.         /// <summary>
  23.         /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
  24.         /// </summary>
  25.         public static SingleLayMan4 GetSingleLayMan()
  26.         {
  27.             if (_singleLayMan == null)
  28.             {
  29.                 lock (_oj)
  30.                 {
  31.                     if (_singleLayMan == null)
  32.                     {
  33.                         _singleLayMan = new SingleLayMan4();
  34.                         Console.WriteLine("我被创建了一次!");
  35.                     }
  36.                 }
  37.             }           
  38.             return _singleLayMan;
  39.         }
  40.       
  41.     }
复制代码
测试代码,验证一下:
  1. public static void FactTestReflection()
  2.         {
  3.             //第一次创建对象会成功
  4.             var singleLayMan1 = GetReflectionSingleLayMan4Instance();
  5.             //第二次创建对象会失败,报异常
  6.            var singleLayMan2 = GetReflectionSingleLayMan4Instance();
  7.             Console.WriteLine(singleLayMan1.GetHashCode());
  8.         }
  9.         private static SingleLayMan4 GetReflectionSingleLayMan4Instance()
  10.         {
  11.             var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
  12.             //获取私有的构造函数
  13.             var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
  14.             //执行构造函数
  15.             SingleLayMan4 singleLayMan = (SingleLayMan4)ctors[0].Invoke(null);
  16.             return singleLayMan;
  17.         }
复制代码
第一次创建对象会成功,因为执行构造函数时没有执行GetSingleLayMan(),跨过了new,导致_isOk赋值true,第二次反射创建执行构造函数时判断变量_isOk为true,走入异常逻辑。
但这样做真的就安全了吗?既然可以通过反射执行构造函数来创建对象,那也可以通过反射改变局部变量_isOk 的值,上代码:
  1.         /// <summary>
  2.         /// 通过反射也可以改变局部变量_isOk的值,继续创建对象
  3.         /// </summary>
  4.         public static void FactTestReflection2()
  5.         {
  6.             Type type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
  7.             //获取私有的构造函数
  8.             var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
  9.             //执行构造函数
  10.             SingleLayMan4 singleLayMan1 = (SingleLayMan4)ctors[0].Invoke(null);
  11.             FieldInfo fieldInfo =  type.GetField("_isOk", BindingFlags.NonPublic | BindingFlags.Static);
  12.             fieldInfo.SetValue("_isOk", false);
  13.             SingleLayMan4 singleLayMan2 = (SingleLayMan4)ctors[0].Invoke(null);
  14.             Console.WriteLine(singleLayMan1.GetHashCode());
  15.             Console.WriteLine(singleLayMan2.GetHashCode());
  16.         }
复制代码
最后

大家或许发现了,只要有反射存在,哪怕你的逻辑写的再严谨,它仍然可以反射创建对象,只因为它是反射!所以,单例模式的安全性也是相对而言的,具体选择用哪个,取决项目的业务场景了。如有发现问题,欢迎不吝赐教!
源码地址:https://gitee.com/mhg/design-mode-demo.git
[code][/code]
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表