热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【C#设计模式】01.你真的知道线程安全的“单件模式”吗?

概述:单件模式的类图可以说是所有模式的类图中最简单的,事实上,它的类图上只有一个类。尽管从设计的视角来说它很简单,但是实现上还是会遇到相当多的波折。一、职责:1.保证一个类有且仅有

 

概述:

  单件模式的类图可以说是所有模式的类图中最简单的,事实上,它的类图上只有一个类。

  尽管从设计的视角来说它很简单,但是实现上还是会遇到相当多的波折。

一、职责:

  1.保证一个类有且仅有一个实例

  2.且提供一个全局访问点

 

二、代码中需要用到的地方

  线程池(Thread Pool)/缓存(cache)/对话框/处理偏好设置和注册表的对象/日志对象/充当打印机/显卡等设备的驱动程序的对象。

三、生活中用到的地方

  1.考勤记录仪可以有多台,但是时钟必须只有一个,所有的考勤记录必须根据这个时钟来生成打卡时间记录。且该时钟是唯一的时间访问入口。

  2.足球场上只能根据主裁判的手表来判断比赛进行了多长时间,比赛进行的时间是唯一的,

查看比赛进行的时间的访问入口时主裁判的手表上的时间。

 

四、对比全局静态变量

我们可以用全局静态变量指向一个对象。程序员工作的时候约定好,只用这个全局变量作为唯一的一个访问这个对象的入口。

优点:

  1.代码定义简单

  2.调用方便

缺点:

  1.程序员之间需要约定

  2.程序一开始就得创建好对象,如果该对象非常耗资源,而程序执行的过程中又一直没有用到它,就形成了资源的浪费。

  3.不能保证一个对象只能被实例化一次,如果程序员之间的约定并没有严格遵守,比如新来的同事并不知道有这个约定。

 

 

五、原理图:

Singleton

Static uniqueInstance

//其他有用的单件数据...

Static GetInstance()

//其他有用的单件方法...

六、代码示例

 

1.简单实现方式:

namespace SimpleSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }

        public static Singleton GetInstance()
        {
            //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象

            if (_singleton == null)
            {
                _singleton = new Singleton();
            }
            return _singleton;
        }

    }
}

优点:

  1.在单线程的程序中,对象只会被创建一次。

  2.实例的初始化延迟到了子类中,子类中可以判断是否已存在实例而进行初始化或直接返回已经初始化的实例。这种延迟初始化避免了不必要的创建实例

缺点:

  1.这种方式对于线程来说不是安全的,如果有两个线程同时进入到if 的处理代码中,就会造成创建了两个Singleton实例

 

 

2.安全的线程实现的方式

namespace SafetySingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;

        private static readonly object _padLock = new object();

        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }

        public static Singleton GetInstance()
        {

            //lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,
            //当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果试图进入临界区,则只能一直等待(即被阻止),
            //直到已经进入临界区的线程访问完毕,并释放锁标志。
            lock(_padLock)
            {
                //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象
                if (_singleton == null)
                {
                    _singleton = new Singleton();
                }
            }
            return _singleton;
        }

    }
}

优点:

  Singleton实例只会被创建一次,因为lock关键字只允许一个线程进入lock所包括的代码,阻塞其他的线程进入到lock所包括的代码中,所以实例只会被创建一次

缺点:

  不能实现多线程,必然降低了性能。

 

 

3.双重锁定

namespace TwiceLockSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;

        private static readonly object _padLock = new object();

        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }

        public static Singleton GetInstance()
        {
            //先判断_singleton是否为null,如果为null则创建,且创建的代码是独占式的。
            //如果_singleton不为null,则直接返回_singleton
            if (_singleton == null)
            {
                //lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,
                //当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果试图进入临界区,则只能一直等待(即被阻止),
                //直到已经进入临界区的线程访问完毕,并释放锁标志。
                lock (_padLock)
                {
                    //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象
                    if (_singleton == null)
                    {
                        _singleton = new Singleton();
                    }
                }
            }
            return _singleton;
        }

    }
}

优点:

  1.相对于方法二,性能上有提升,并不是每次都进行锁定

  2.Singleton实例只会被创建一次

缺点:

  1.该方式较复杂

  2.加了两次判断,对性能有损失

 

4.静态初始化实现方式

namespace StaticInitializeSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = new Singleton();

        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }

        public static Singleton GetInstance()
        {
            return _singleton;
        }

    }
}

优点:

  1.实现方式简单

  2.减少了判断,相比较于前面的三个例子,该实现方式在性能上有很大的提升。

缺点:

  1.由于创建实例交给了CLR公共语言运行时,所以没有实例化的控制权。

 

5.延迟初始化实现方式

namespace LazyInstantitionSingleton
{
    public sealed class Singleton
    {
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }

        public static Singleton GetInstance()
        {
            return Nested._singleton;
        }

        public class Nested()
        {
            static Nested()
            {
            }

            internal static readonly Singleton _singleton  = new Singleton();
        }

    }
}

优点:

  1.创建实例延迟到了Nested类里面

 

七、与单件模式的问答

1.单件模式只有一个类,应该是很简单的模式,但是问题似乎不少

答:固然正确地实现单件模式需要一点技巧,但是阅读完这篇文章之后,你已经具备了用正确的方式实现单件模式的能力。当你需要控制实例个数时,还是应当使用单件模式。

2.难道我不能创建一个类,把所有的方法和变量都定义为静态的,把类直接当作一个单件?

  答:如果你的类自给自足,而且不依赖于复杂的初始化,那么你可以这么做。但是,因为静态初始化的控制是在CLR受伤,这么做有可能导致混乱,特别是当有许多类牵涉其中的时候。这么做常常会造成一些微妙的,不容易发现的和初始uade次序有关的bug。除非你有绝对的必要使用类的单件,否则还是建议使用对象的单件

3.类应该做一件事,而且只做一件事。类如果能做两件事,就会被认为是不好的OO设计,单件有没有违反这样的观念?

  答:你说的是“一个类,一个责任”原则。没错,你似的对的,但见类不只负责管理自己的实例,并提供全局访问,还在应用程序中担当角色,所以也可以被视为是两个责任。尽管如此,由类管理自己的实例的做法并不少见。这可以让整体设计更简单。更何况,许多开发人员都已经熟悉了单件模式的这种做法。

4.我想把单件类当成超类,设计出子类,但是我遇到了问题,究竟可以不可以继承单件类?

  答:继承单件类会遇到一个问题,就是构造器是私有的。你不能用私有构造器来扩展类。所以你必须把单件的构造器改成公共的或受保护的。但是这么一来就不算真正的单件了,因为别的类也可以实例化他。

如果你果真把构造器的访问权限改了,还有另一个问题出现,单件的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量,这可能不是你想要的。

5.我还是不了解为何全局变量比单件模式差。

  答:在.net中,全局变量基本上就是对对象的静态引用。在这样的情况下使用全局变量会有一些缺点,我们已经提到了其中的一个:急切实例化VS延迟实例化。但是我们要记住这个模式的目的:确保类只有一个实例并提供全局访问,但是不能确保只有一个实例。全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成这样的现象,但单件仍然可能被滥用。

八、垃圾回收

如果没有一个全局变量引用单件模式的实例,该实例是否会被垃圾回收?

经过自己写代码的验证:不会被回收。

由下面的结果可知,两次调用GetInstance,只创建了一次Singleton实例

技术分享

九、总结

1.单件模式:确保一个类只有一个实例,并提供一个全局访问点

2.我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。要想取得单件实例,通过单件类是唯一的途径。

3.我们也提供这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。前面的例子利用延迟实例化的方式创建单件,这种做法对资源敏感的对象特别重要。

 

十、Demo程序

巧克力工厂

大家都知道,现代化的巧克力工厂具备计算机控制的巧克力锅炉。锅炉做的事,就是把巧克力和牛奶融在一起,然后送到下一阶段,以制造巧克力棒。

这里有一个巧克力公司的工业强度巧克力锅炉控制器。看看它的代码,你会发现大妈写得相当消息,他们在努力防止不好的事情发生。例如,锅炉已经满了还继续放原料。

 1.巧克力工厂类

using System;

namespace SingletonPattern
{
    public class ChocolateBoiler
    {
        private Boolean empty;

        public ChocolateBoiler()
        {
            empty = true;
        }

        public void fill()
        {
            if (isEmpty())
            {
                Console.WriteLine("Fill");
                empty = false;
            }
        }
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

2.简单的单件类

using System;

namespace SingletonPattern
{
    public class SingltonChocolateBoiler
    {
        //锅炉为空的标志位
        private Boolean empty;
        //指向创建的实例,返回给调用GetInstance()的方法
        public static SingltonChocolateBoiler uniqueChocolateBoiler = null;

        /// 
        /// SingltonChocolateBoiler的私有构造函数
        /// 
        private SingltonChocolateBoiler()
        {
            //开始时,锅炉是空的
            empty = true;
        }

        /// 
        /// 创建instance
        /// 
        /// SingltonChocolateBoiler instance
        public static SingltonChocolateBoiler GetInstance()
        {
            if (uniqueChocolateBoiler == null)
            {
                //创建实例
                uniqueChocolateBoiler = new SingltonChocolateBoiler();
                Console.WriteLine(uniqueChocolateBoiler.GetHashCode());
            }
            //返回实例
            return uniqueChocolateBoiler;

        }

        /// 
        /// 如果锅炉为空,用巧克力和牛奶填满锅炉的混合物
        /// 
        public void fill()
        {
            //如果锅炉是空的,则加满锅炉,并将empty标志置为false
            if (isEmpty())
            {
                Console.WriteLine("Fill------------------");
                //将empty标志置为false
                empty = false;
            }
            
        }
        

        /// 
        /// 返回锅炉填满状态
        /// 
        /// empty
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

3.线程安全的单件类

using System;

namespace SingletonPattern
{
    public class SyncSingletonChocolateBoiler
    {
        private Boolean empty;
        public static SyncSingletonChocolateBoiler uniqueChocolateBoiler = new SyncSingletonChocolateBoiler();

        private SyncSingletonChocolateBoiler()
        {
            Console.WriteLine("empty{}");
            empty = true;
        }

        public static SyncSingletonChocolateBoiler GetInstance()
        {
            Console.WriteLine(uniqueChocolateBoiler.GetHashCode());
            return uniqueChocolateBoiler;
        }

        public void fill()
        {
            if (isEmpty())
            {
                Console.WriteLine("Fill------------------");
                empty = false;
            }
        }
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

4.主程序

using System;
using System.Threading;

namespace SingletonPattern
{
    class Program
    {
        static void Main(string[] args)
        {
    
//1.普通的模式,会创建两个巧克力工厂,Fill方法会调用两次 ChocolateBoiler chocolateBoiler = new ChocolateBoiler(); chocolateBoiler.fill();//Fill chocolateBoiler = new ChocolateBoiler(); chocolateBoiler.fill();//Fill //2.单件模式,在单线程的代码中,只会创建一个巧克力工厂,Fill方法只会调用一次 SingltonChocolateBoiler uniqueChocolateBoiler1 = SingltonChocolateBoiler.GetInstance(); uniqueChocolateBoiler1.fill(); uniqueChocolateBoiler1 = SingltonChocolateBoiler.GetInstance(); uniqueChocolateBoiler1.fill(); //3.单件模式,在多线程的代码中,可能会创建两个巧克力工厂,Fill方法会被调用两次 Console.WriteLine("SimpleSingleton例子"); Thread thread1 = new Thread(new ThreadStart(Method1)); SingltonChocolateBoiler uniqueChocolateBoiler2 = null; thread1.Start(); Thread.Sleep(1); for (int i = 0; i <30; i++) { Console.WriteLine("Main {0}", i); uniqueChocolateBoiler2 = SingltonChocolateBoiler.GetInstance(); uniqueChocolateBoiler2.fill(); } //4.单件模式,在多线程中,也只会创建一个巧克力工厂,但是由于Fill方法不是线程安全的,所以Fill方法有可能会被调用两次 Console.WriteLine("StaticInitializeSingleton例子"); Thread thread2 = new Thread(new ThreadStart(Method2)); SyncSingletonChocolateBoiler uniqueChocolateBoiler3 = null; thread2.Start(); Thread.Sleep(1); for (int i = 0; i <50; i++) { Console.WriteLine("Main {0}", i); uniqueChocolateBoiler3 = SyncSingletonChocolateBoiler.GetInstance(); uniqueChocolateBoiler3.fill(); } Console.ReadKey(); } private static void Method1() { Thread.Sleep(1); SingltonChocolateBoiler uniqueChocolateBoiler = null; for (int i = 0; i <30; i++) { Console.WriteLine("Other {0}", i); uniqueChocolateBoiler = SingltonChocolateBoiler.GetInstance(); uniqueChocolateBoiler.fill(); } } private static void Method2() { Thread.Sleep(1); SyncSingletonChocolateBoiler uniqueChocolateBoiler = null; for (int i = 0; i <50; i++) { Console.WriteLine("Other {0}", i); uniqueChocolateBoiler = SyncSingletonChocolateBoiler.GetInstance(); uniqueChocolateBoiler.fill(); } } } }

结果如下:

例子3:单件模式,在多线程的代码中,可能会创建两个巧克力工厂,Fill方法会被调用两次

技术分享 

例子4:单件模式,在多线程中,也只会创建一个巧克力工厂,但是由于Fill方法不是线程安全的,所以Fill方法有可能会被调用两次

技术分享

本篇所有例子下载:

【设计模式】01_Singleton_博客园jackson0714.zip

 参考资料:

《Head First设计模式》

【C#设计模式】01.你真的知道线程安全的“单件模式”吗?


推荐阅读
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文内容为asp.net微信公众平台开发的目录汇总,包括数据库设计、多层架构框架搭建和入口实现、微信消息封装及反射赋值、关注事件、用户记录、回复文本消息、图文消息、服务搭建(接入)、自定义菜单等。同时提供了示例代码和相关的后台管理功能。内容涵盖了多个方面,适合综合运用。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
author-avatar
啊明的小蝴蝶_522
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有