从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)

摘要:
好了,不卖关子了,下面我们来看看每种单例模式存在的问题以及解决办法。在此期间,在另一个CPU上并发运行的另一个线程可能已经从主存储器中读取了相同的数据位并使用了过时的数据版本。对于解决办法1:可以使用volatile关键字,它可以禁止重排序以及缓存的问题。同理你可以尝试使用该方法来攻击模式五。
一、前言

这篇文章是学习单例模式的第二篇,之前的文章一下子就给出来看起来很高大上的实现方法,但是这种模式还是存在漏洞的,具体有什么问题,大家可以停顿一会儿,思考一下。好了,不卖关子了,下面我们来看看每种单例模式存在的问题以及解决办法。

二、每种Singleton 模式的演进
  • 模式一
public classLazySingleton
    {
        private static LazySingleton lazySingleton = null;
        privateLazySingleton()
        {
        }
        public staticLazySingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lazySingleton = newLazySingleton();
            }
            returnlazySingleton;
        }
    }

问题:该模式下在多线程下就会存在问题,因为你不知道线程执行的先后顺序,不信看下面的调试,如下。

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第1张

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第2张

我们现在让线程Two执行,它会进入到if里面,因为线程one已经被冻结,调试结果:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第3张

接着,我们把冻结的线程one解冻,执行完成的结果如下:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第4张

发现,竟然产生了两个实例,这也就说明了上面实现单例模式在多线程下确实存在问题,为了解决在多线程的问题,引出了下面的单例模式。

  • 模式二:DoubleCheck双重检查

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第5张

问题:上面的代码已经加上了lock,可以解决多线程的问题,但是这样还是会出现问题,出现问题的地方在上面的两处断点处。多线程在多核CPU上执行时寄存器缓存和指令的重新排序【也就是new关键字步骤2和步骤3交换】虽然出现的概率很小,但是这种隐患一定要消除。如果出现指令重排的话,一个线程还没来得及把分配对象的指针复制给变量lazySingleton,另外一个线程就会进入到第一个断点的if逻辑里面。下面分别贴出寄存器缓存和指令重新排序的示意图:

缓存数据示意图:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第6张

(注意:图片来源自https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/)

现代计算机中的内存很复杂,有多级缓存,处理器寄存器和多个处理器共享主内存等。处理器可能会从主内存中读取数据缓存到寄存器中,另一个线程可能会使用缓存的数据,并且如果修改仅更新主内存,再次期间并发运行在另外一个CPU上的线程,可能读取的还是之前的值。 在此期间,在另一个CPU上并发运行的另一个线程可能已经从主存储器中读取了相同的数据位并使用了过时的数据版本。

指令重排示意图(下面的示意图来自:geely老师的Java设计模式课程):

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第7张

对于单线程来说既是指令重排也不会影响,但是对于多线程就会有影响,如下图所示:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第8张

为了解决上面的问题有两种做法:1)不允许2和3进行指令重排序。2)允许线程0可以重排序但是不允许线程1重排序。

对于解决办法1:可以使用volatile关键字,它可以禁止重排序以及缓存的问题。

对于解决办法2:静态内部类-基于类初始化的延迟加。

  • 模式三:解决办法1示例代码:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第9张

  • 模式四:解决办法2示例代码:
 public classStaticInnerClassSingleton
    {
        private static classInnerClass
        {
            internal static StaticInnerClassSingleton staticInnerClassSingleton = newStaticInnerClassSingleton();
        }
        public staticStaticInnerClassSingleton GetInstance()
        {
            returnInnerClass.staticInnerClassSingleton;
        }
    }
static voidGetInstancev5()
        {
            var hashCode =StaticInnerClassSingleton.GetInstance().GetHashCode();
            Console.WriteLine(hashCode);
        }
for (int i = 0; i < 10; i++)
            {
                Thread thread = newThread(GetInstancev5);
                thread.Start();
                if (i%2==0)
                {
                    Thread.Sleep(1000);
                }
            }

验证结果:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第10张

模式五:饿汉模式

public classCurrentSingleton
    {
        private static CurrentSingleton uniqueInstance = newCurrentSingleton();
        privateCurrentSingleton() {
        }
        public staticCurrentSingleton Instance
        {
            get { returnuniqueInstance; }
        }
    }

聊到这里,关于单例模式的几种模式已经差不多了,该聊的已经聊完了,大多小伙伴们可能就了解到这里就结束了,先舒口气,再继续往下看,你会有意向不到的收获。

三、单例模式下的问题解决办法
  • 问题一:反射攻击单例模式三

单例模式三(懒汉模式)代码:

 public classLazyDoubleCheckSingleton
    {
        private volatile static LazyDoubleCheckSingleton lazySingleton = null;
        private static readonly object _threadSafetyLock = new object();
        privateLazyDoubleCheckSingleton(){}
        public staticLazyDoubleCheckSingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lock(_threadSafetyLock)
                {
                    if (lazySingleton == null)
                    {
                        //注意:new关键字做了下面三步的工作:
                        //1、分配内存给这个对象
                        //2、初始化对象
                        //3、设置lazySingleton指向刚分配的内存地址
                        lazySingleton = newLazyDoubleCheckSingleton();
                    }
                }
            }
            returnlazySingleton;
        }
    }

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第11张

看到没,我们通过反射也可以创建类的实例,那怕你的构造函数是private的,我通过反射都可以来创建对象的实例。同理你可以尝试使用该方法来攻击模式五(饿汉模式)。

那我们该如何防御?对于饿汉模式、基于静态类模式的单例,我们可以通过下面的方法来防御:

在对应的private构造函数中添加一下代码:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第12张

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第13张

对于懒汉模式的单例这种方法还适用吗?不一定,请看下面的代码:

基于模式三【见上】的代码修改:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第14张

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第15张

验证结果:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第16张

发现该方式处理不起作用。对于这个问题我们该怎么解决?尝试的方法如下:

 public classLazyDoubleCheckSingleton
    {
        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 staticLazyDoubleCheckSingleton GetInstance()
        {
            if (lazySingleton == null)
            {
                lock(_threadSafetyLock)
                {
                    if (lazySingleton == null)
                    {
                        //注意:new关键字做了下面三步的工作:
                        //1、分配内存给这个对象
                        //2、初始化对象
                        //3、设置lazySingleton指向刚分配的内存地址
                        lazySingleton = newLazyDoubleCheckSingleton();
                    }
                }
            }
            returnlazySingleton;
        }
    }
Type type = typeof(LazyDoubleCheckSingleton);
            object sobj = Activator.CreateInstance(type, true);
            Console.WriteLine(LazyDoubleCheckSingleton.GetInstance().GetHashCode());
            Console.WriteLine(sobj.GetHashCode());

验证结果:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第17张

这种方法看似解决了懒汉模式的问题,但是!它真的能解决这个问题吗?大家可以想一下,为什么解决不了?我也就不卖关子了,原因就是反射,反射的威力太强了,上面演示的,即使你的构造函数是private我也能创建对象,区区一个字段,反射修改你的值不是很轻松吗。

反射攻击演示:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第18张

所以懒汉模式的单例,是防御不了反射攻击的,至于Java中有一个叫枚举模式的单例,可以解决这个问题,至于C#目前我还没想出好的解决办法,如果大家有好的解决办法可以贡献到评论区。好了问题一讲到这里已经差不多了,下面我们来介绍问题二。

  • 问题:序列化破坏单例模式

背景:在某些场景下我们需要把类序列化到文件当中,正好这个类是单例的,正常的情况应该是:序列化到文件中,再从文件反序列化,应该是同一个类,但一般的处理方法真的能得到同一个类吗?

实例代码:

[Serializable]
public
classStaticInnerClassSingleton { private static classInnerClass { internal static StaticInnerClassSingleton staticInnerClassSingleton = newStaticInnerClassSingleton(); } public staticStaticInnerClassSingleton GetInstance() { returnInnerClass.staticInnerClassSingleton; } }
//序列化到文件:
            var obj =StaticInnerClassSingleton.GetInstance();
            var formatter = newBinaryFormatter();
            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());

验证结果:

从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)第19张

看到没,竟然是两个不同的实例,如果大家遇到这样的场景可以使用下面的方法来保障反序列化出来的是同一个对象,我们只需要修改单例模式的类。代码如下:

[Serializable]
    public classStaticInnerClassSingleton: ISerializable
    {
        privateStaticInnerClassSingleton()
        {
        }
        private static classInnerClass
        {
            internal  static StaticInnerClassSingleton staticInnerClassSingleton = newStaticInnerClassSingleton();
        }
        public staticStaticInnerClassSingleton GetInstance()
        {
            returnInnerClass.staticInnerClassSingleton;
        }
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.SetType(typeof(SingletonHelper));
        }
        [Serializable]
        private class SingletonHelper : IObjectReference
        {
            public object GetRealObject(StreamingContext context)
            {
                returnInnerClass.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/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

免责声明:文章转载自《从源码中学习设计模式系列——单例模式序/反序列化以及反射攻击的问题(二)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇NLP相关期刊和会议Android系统如何管理自己内存的?下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

高性能缓存架构

极客时间:《从 0 开始学架构》:高性能缓存架构 1、引言 前几章节分别从读写分离、分库分表以及数据库的选择等方面来提升系统的性能,但在某些复杂的业务场景下,单纯的提高存储系统的性能是不够的,典型的场景如下: 需要经过复杂运算后得出的数据,存储系统无能为力 读多写少的数据,存储系统有心无力。如写一次,读多次 缓存就是为了弥补存储系统在这些复杂业务场景下...

Java常考面试题

Java常考面试题,整理自牛客网和程序员面试宝典,有的题不太好。 1. 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”? 答:Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自...

IOS 多线程的一些总结

IOS 多线程 有三种主要方法 (1)NSThread (2)NSOperation (3)**   下面简单介绍这三个方法  1.NSThread       调用方法如下:          如函数需要输入参数,可从object传进去。     (1) [NSThread detachNewThreadSelector:@selector(th...

线程属性--十分重要的概念

http://blog.chinaunix.net/uid-23193900-id-3346199.html 一.线程属性 线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。我们用pthread_attr_init函数对其初始化,用pthread_attr_destroy对其去除初始化。...

Java审计之CMS中的那些反序列化漏洞

Java审计之CMS中的那些反序列化漏洞 0x00 前言 过年这段时间比较无聊,找了一套源码审计了一下,发现几个有意思的点拿出来给分享一下。 0x01 XStream 反序列化漏洞 下载源码下来发现并不是源代码,而是一个的文件夹,里面都已经是编译过的一个个class文件。 在一个微信回调的路由位置里面找到通过搜索类名 Serialize关键字找到了一个工具...

10个经典的Android开源项目(附源码包)

      最近在抽空学习Android系统开发,对Android学习也比较感兴趣,刚开始学就试着在网上找几个项目源码研究看下,以下就将找到的Android项目源码列出,希望对正在或准备学习Android系统开发开发的能有些帮助!       1、Android团队提供的示例项目  如果不是从学习Android SDK中提供的那些样例代码开始,可能没有更好...