• 设计模式(2) 单例模式


    • 单例模式
    • 线程安全的Singleton
    • 会破坏Singleton的情况
    • 线程级Singleton

    单例模式是几个创建型模式中最独立的一个,它的主要目标不是根据客户程序调用生成一个新的实例,而是控制某个类型的实例数量只有一个。
    GOF对单例的描述为:
    Ensure a class only has one instance, and provide aglobal point of access to.
    —Design Patterns : Elements of Reusable Object-Oriented Software

    单例模式

    单例模式的应用场景不必赘述,先来一个最简单的实现方式:

    public class Singleton
    {
        private Singleton() { }
        private static Singleton instance;
        public static Singleton Instance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    这里采用的是Lazy方式,也可以在静态变量被创建的时候直接初始化实例。
    这段代码已经可以满足最初Singleton模式的设计要求,在大多数情况下可以很好地工作。但在多线程环境下这种实现方式是存在缺陷的,当多个线程几乎同时调用Singleton类的Instance静态属性的时候,instance成员可能还没有被实例化,因此它被创建了多次,而且最终Singleton类中保存的是最后创建的那个实例,各个线程引用的对象不同。

    线程安全的Singleton

    为了保证多线程环境下instance实例只有一个,对代码进行了优化:

    public class Singleton
    {
        private static volatile Singleton instance;
        public static Singleton Instance()
        {
            if (instance == null)
            {
                lock (typeof(Singleton))
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    相比最初的实现,改变的地方有这几处:

    • instance使用volatile关键字修饰,它表示字段可能被多个并发执行的线程修改。
    • 在实例化前lock Singleton类型,避免了多个线程同时实例化的问题。
    • 第一个if加在了lock之前,是为了避免每次调用都锁定Singleton类型带来的效率下降。
    • lock后再次判断instance是否为空,是因为在高并发场景下,在第一个线程锁定并实例化期间,仍然可能会有别的线程进入到第一层if内,这样如果不再次判空,就会重复实例化。

    会破坏Singleton的情况

    有些情况会破坏Singleton的封装,跳过“只能有一个实例”的限制,在实际应用中要注意规避。

    • 第一种情况就是实现ICloneable接口或继承自其相关的子类,这样客户程序借助ICloneable接口就可以跳过已经被隐藏起来的构造函数

    • 另外通过二进制、Json之类序列化、反序列化的方式也可以产生新的对象。

    线程级Singleton

    前面讨论的是线程安全的Singleton实现,但有时需要的是更细粒度的Singleton,比如线程级的Singleton,只要保证在一个线程内只有一个实例即可,这就类似Asp.NET Core 自带的IOC提供的AddScope注册方式,可以保证一个HttpContext内只有一个实例。

    虽然Asp.NET Core提供类似的现成实现,但如果在非Web环境下也需要线程级的实例控制该怎么办呢? 结合C#提供的System.ThreadStaticAttribute可以完成

    通过System.ThreadStaticAttribute可以将某个静态变量限定为仅在本线程内部是静态的。
    实现如下:

    public class ThreadSingleton
    {
        private ThreadSingleton() { }
    
        [ThreadStatic] //instance只在当前线程内为静态
        private static ThreadSingleton instance;
        public static ThreadSingleton Instance()
        {
            if (instance == null)
            {
                instance = new ThreadSingleton();
            }
            return instance;
        }
    }
    

    这里再不需要线程锁了,因为线程级的单例不需要考虑线程安全。
    为了验证实现的准确性,首先构造一个线程内执行的目标对象:

    class Work
    {
        public static IList<int> Log = new List<int>();
    
        /// <summary>
        /// 每个线程的执行部分
        /// </summary>
        public void Procedure()
        {
            ThreadSingleton s1 = ThreadSingleton.Instance();
            ThreadSingleton s2 = ThreadSingleton.Instance();
    
            //证明可以正常构造实例
            Assert.IsNotNull(s1);
            Assert.IsNotNull(s2);
    
            //验证当前线程执行体内两次获取的是同一个实例
            Assert.AreEqual(s1.GetHashCode(), s2.GetHashCode());
    
            //记录当前线程所使用对象的HashCode
            Log.Add(s1.GetHashCode());
        }
    }
    

    这个类会在每个线程内部执行,并验证线程内多次获取的Instance是同一个实例,并记录这个实例的HashCode,以便与别的线程实例对比。
    接下来开启多个线程同时执行Procedure()方法:

    [Test]
    public void ThreadSingletonTest()
    {
        int threadCount = 4;
        Thread[] threads = new Thread[threadCount];  //创建4个线程
        for (int i = 0; i < threadCount; i++)
        {
            ThreadStart work = new ThreadStart(new Work().Procedure);
            threads[i] = new Thread(work);
        }
    
        //执行线程
        foreach (var thread in threads)
        {
            thread.Start();
        }
    
        Thread.Sleep(10000);
        Assert.AreEqual(threadCount, Work.Log.Distinct().Count());
    }
    

    Work类的静态变量Log中记录了每个线程中实例的HashCode,这些HashCode彼此不相同,且与线程的数量一致,证明每个线程间的实例是不相同的。

    参考书籍:
    王翔著 《设计模式——基于C#的工程化实现及扩展》

  • 相关阅读:
    输入形如"a-b,b-c,b-d"的字符串,当作相邻路径,计算最远路径
    即时通信软件后端API文档
    Django中,使用redis作为缓存
    Django中,websocket实时通信设置概要
    在Django中,自定义User模型,使用token鉴权,实现注册、登录、修改密码等API
    Django中,用rest_framework写API
    偶尔练习一下,go语言做题目
    关于邮政储蓄卡内钱被盗的思考
    浅谈图片上传之剪切
    JavaScript学习笔记
  • 原文地址:https://www.cnblogs.com/zhixin9001/p/13227527.html
Copyright © 2020-2023  润新知