一、前言
这篇文章是学习单例模式的第二篇,之前的文章一下子就给出来看起来很高大上的实现方法,但是这种模式还是存在漏洞的,具体有什么问题,大家可以停顿一会儿,思考一下。好了,不卖关子了,下面我们来看看每种单例模式存在的问题以及解决办法。
二、每种Singleton 模式的演进
- 模式一
public class LazySingleton { private static LazySingleton lazySingleton = null; private LazySingleton() { } public static LazySingleton GetInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
问题:该模式下在多线程下就会存在问题,因为你不知道线程执行的先后顺序,不信看下面的调试,如下。
我们现在让线程Two执行,它会进入到if里面,因为线程one已经被冻结,调试结果:
接着,我们把冻结的线程one解冻,执行完成的结果如下:
发现,竟然产生了两个实例,这也就说明了上面实现单例模式在多线程下确实存在问题,为了解决在多线程的问题,引出了下面的单例模式。
- 模式二:DoubleCheck双重检查
问题:上面的代码已经加上了lock,可以解决多线程的问题,但是这样还是会出现问题,出现问题的地方在上面的两处断点处。多线程在多核CPU上执行时寄存器缓存和指令的重新排序【也就是new关键字步骤2和步骤3交换】虽然出现的概率很小,但是这种隐患一定要消除。如果出现指令重排的话,一个线程还没来得及把分配对象的指针复制给变量lazySingleton,另外一个线程就会进入到第一个断点的if逻辑里面。下面分别贴出寄存器缓存和指令重新排序的示意图:
缓存数据示意图:
(注意:图片来源自https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/)
现代计算机中的内存很复杂,有多级缓存,处理器寄存器和多个处理器共享主内存等。处理器可能会从主内存中读取数据缓存到寄存器中,另一个线程可能会使用缓存的数据,并且如果修改仅更新主内存,再次期间并发运行在另外一个CPU上的线程,可能读取的还是之前的值。 在此期间,在另一个CPU上并发运行的另一个线程可能已经从主存储器中读取了相同的数据位并使用了过时的数据版本。
指令重排示意图(下面的示意图来自:geely老师的Java设计模式课程):
对于单线程来说既是指令重排也不会影响,但是对于多线程就会有影响,如下图所示:
为了解决上面的问题有两种做法:1)不允许2和3进行指令重排序。2)允许线程0可以重排序但是不允许线程1重排序。
对于解决办法1:可以使用volatile关键字,它可以禁止重排序以及缓存的问题。
对于解决办法2:静态内部类-基于类初始化的延迟加。
- 模式三:解决办法1示例代码:
- 模式四:解决办法2示例代码:
public class StaticInnerClassSingleton { private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } }
static void GetInstancev5() { var hashCode = StaticInnerClassSingleton.GetInstance().GetHashCode(); Console.WriteLine(hashCode); }
for (int i = 0; i < 10; i++) { Thread thread = new Thread(GetInstancev5); thread.Start(); if (i%2==0) { Thread.Sleep(1000); } }
验证结果:
模式五:饿汉模式
public class CurrentSingleton { private static CurrentSingleton uniqueInstance = new CurrentSingleton(); private CurrentSingleton() { } public static CurrentSingleton Instance { get { return uniqueInstance; } } }
聊到这里,关于单例模式的几种模式已经差不多了,该聊的已经聊完了,大多小伙伴们可能就了解到这里就结束了,先舒口气,再继续往下看,你会有意向不到的收获。
三、单例模式下的问题解决办法
- 问题一:反射攻击单例模式三
单例模式三(懒汉模式)代码:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null; private static readonly object _threadSafetyLock = new object(); private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton GetInstance() { if (lazySingleton == null) { lock(_threadSafetyLock) { if (lazySingleton == null) { //注意:new关键字做了下面三步的工作: //1、分配内存给这个对象 //2、初始化对象 //3、设置lazySingleton指向刚分配的内存地址 lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }
看到没,我们通过反射也可以创建类的实例,那怕你的构造函数是private的,我通过反射都可以来创建对象的实例。同理你可以尝试使用该方法来攻击模式五(饿汉模式)。
那我们该如何防御?对于饿汉模式、基于静态类模式的单例,我们可以通过下面的方法来防御:
在对应的private构造函数中添加一下代码:
对于懒汉模式的单例这种方法还适用吗?不一定,请看下面的代码:
基于模式三【见上】的代码修改:
验证结果:
发现该方式处理不起作用。对于这个问题我们该怎么解决?尝试的方法如下:
public class LazyDoubleCheckSingleton { private volatile static LazyDoubleCheckSingleton lazySingleton = null; private static readonly object _threadSafetyLock = new object(); private static bool flag = true; private LazyDoubleCheckSingleton(){ if (flag) { flag = false; } else { throw new Exception("单例构造器进制反射调用"); } } public static LazyDoubleCheckSingleton GetInstance() { if (lazySingleton == null) { lock(_threadSafetyLock) { if (lazySingleton == null) { //注意:new关键字做了下面三步的工作: //1、分配内存给这个对象 //2、初始化对象 //3、设置lazySingleton指向刚分配的内存地址 lazySingleton = new LazyDoubleCheckSingleton(); } } } return lazySingleton; } }
Type type = typeof(LazyDoubleCheckSingleton); object sobj = Activator.CreateInstance(type, true); Console.WriteLine(LazyDoubleCheckSingleton.GetInstance().GetHashCode()); Console.WriteLine(sobj.GetHashCode());
验证结果:
这种方法看似解决了懒汉模式的问题,但是!它真的能解决这个问题吗?大家可以想一下,为什么解决不了?我也就不卖关子了,原因就是反射,反射的威力太强了,上面演示的,即使你的构造函数是private我也能创建对象,区区一个字段,反射修改你的值不是很轻松吗。
反射攻击演示:
所以懒汉模式的单例,是防御不了反射攻击的,至于Java中有一个叫枚举模式的单例,可以解决这个问题,至于C#目前我还没想出好的解决办法,如果大家有好的解决办法可以贡献到评论区。好了问题一讲到这里已经差不多了,下面我们来介绍问题二。
- 问题:序列化破坏单例模式
背景:在某些场景下我们需要把类序列化到文件当中,正好这个类是单例的,正常的情况应该是:序列化到文件中,再从文件反序列化,应该是同一个类,但一般的处理方法真的能得到同一个类吗?
实例代码:
[Serializable]
public class StaticInnerClassSingleton { private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } }
//序列化到文件: var obj = StaticInnerClassSingleton.GetInstance(); var formatter = new BinaryFormatter(); var stream = new FileStream("D:\Example.txt", FileMode.Create, FileAccess.Write); formatter.Serialize(stream, obj); stream.Close(); //从文件读取出来反序列化 stream = new FileStream("D:\Example.txt", FileMode.Open, FileAccess.Read); var obj2 = (StaticInnerClassSingleton)formatter.Deserialize(stream); Console.WriteLine(obj.GetHashCode()); Console.WriteLine(obj2.GetHashCode());
验证结果:
看到没,竟然是两个不同的实例,如果大家遇到这样的场景可以使用下面的方法来保障反序列化出来的是同一个对象,我们只需要修改单例模式的类。代码如下:
[Serializable] public class StaticInnerClassSingleton: ISerializable { private StaticInnerClassSingleton() { } private static class InnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton GetInstance() { return InnerClass.staticInnerClassSingleton; } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.SetType(typeof(SingletonHelper)); } [Serializable] private class SingletonHelper : IObjectReference { public object GetRealObject(StreamingContext context) { return InnerClass.staticInnerClassSingleton; } } }
如果想知道为什么要这样写我就不在解释了,大家可以参考这篇文章:http://geekswithblogs.net/maziar/archive/2012/07/19/serializing-singleton-objects-c.aspx 好了讲到这里基本上单例这种设计模式,你已经掌握的非常好了,希望对你有帮助,谢谢,如果觉得不错的话,可以推荐一下。之前一直想写这个系列的博客,希望把自己平时学的和工作中的经验分享出来,共同进步,这个系列的标题是“从源码中学习设计模式
这里的源码主要就是ASP.Net Core2.1的源码,现在.Net Core 3.0已经是预览版,还没有正式版,也希望.Net Core 越来越好。也希望我的文章能对你有帮助。
四、总结
单例这种设计模式,具体使用哪种要看你的使用场景,并不是那种模式一定就好,这是需要权衡的,希望看完本篇文章,你在使用该模式能得心应手。另外大家不要和依赖注入中的单例混淆,之前再介绍依赖注入最佳实践的文章中有园友就混淆了。
参考资料:
geely老师的《Java设计模式精讲》
作者:郭峥
出处:http://www.cnblogs.com/runningsmallguo/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。