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

概述:

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

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

一、职责:

  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;

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

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

        }

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

        }

        /// <summary>
        /// 返回锅炉填满状态
        /// </summary>
        /// <returns>empty</returns>
        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设计模式》

时间: 2024-10-14 18:12:42

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

Java设计模式——线程安全的单件模式

单件模式,也称单例模式,用以创建独一无二的.只能有一个实例的对象. 单件模式的类图是所有模式的类图中最简单的--只有一个类.尽管从类设计的视角来看单件模式很简单,但是实现上还是会遇到一些问题,本文着重对这一点来进行分析解决. 最简单的单件模式的实现,代码如下: 1 /** 2 * Created by McBye King on 2016/10/23. 3 */ 4 public class Singleton { 5 private static Singleton singleton; 6

线程安全的单件模式(单例模式)

1.定义: 某一个类只有一个实例,并且这个实例是在类内部进行实例化,并向整个系统提供该实例. 2.单例模式的通用代码: public sealed class Singleton { private static sealed Singleton _singleton = new Singleton(); private Singleton() { } public static Singleton GetInstance() { return _singleton; } public stat

单例设计模式-(你确定自己写的懒汉单例真的是线程安全的吗)

1.单例设计模式的优缺点 优点: 1):只创建一个实例,就可以到处使用,加快创建实体的效率 缺点: 1):如果使用的频率比较低,实例会一直占据着内存空间,会造成资源浪费 2):可能会出现线程安全问题 2.单例设计模式的两种定法(饿汉.懒汉) 饿汉方法写法:(可能会造成资源浪费,类一被加载就创建了实例,但并不能确保这个实例什么时候会被用上) package com.zluo.pattern; /** * * 项目名称:single-instance <br> * 类名称:SingleInstan

iOS.常用设计模式.01.单例模式

使用单例模式的类: UIApplication UIAccelerometer NSUserDefaults NSNotificationCenter NSFileManager NSBundle等 Singleton.h #import <Foundation/Foundation.h> @interface Singleton : NSObject // 始终返回同一个Singleton的指针 + (Singleton *)sharedManager; @property (strong,

设计模式01

设计模式01 Table of Contents 1 设计模式 1.1 基本原则 1.2 开-闭原则 1.2.1 与其他原则关系 1 设计模式 1.1 基本原则 系统的可扩展性由开-闭原则.里氏代换原则.依赖倒转原则.组合/聚合复用原则保证:系 统的灵活性由开-闭原则.迪米特原则.接口隔离原则保证:系统的可插入性由开-闭原 则.里氏代换原则.依赖倒转原则.组合/聚合复用原则保证. 当一个软件复用有道.易于维护,新功能加入到系统,或修改一个已有的功能将是容易 的,因此,代码高手就没有用武之地:而当

设计模式(5)--单件模式

同步一个方法可能造成程序执行效率下降100倍. 静态初始化的控制权是在Java手上 一个类,一个责任 原则. ( 类应该做一件事,而且只做一件事)  但单件做了两件事, 一是 管理自己的实例(并提供全局访问变量) 二是在应用程序中担任角色, (1). 私有构造器.不提供对外访问   (2). 静态方法对外提供类本身实例.    (3)  . 只有一个实例 单例模式:  ( 管理共享资源 如数据库连接,或者线程池)  MVC  DAO Service 层 延迟实例化(懒汉式) 有线程安全问题  -

设计模式之单件模式(Singleton Pattern)

一.单件模式是什么? 单件模式也被称为单例模式,它的作用说白了就是为了确保“该类的实例只有一个” 单件模式经常被用来管理资源敏感的对象,比如:数据库连接对象.注册表对象.线程池对象等等,这种对象如果同时存在多个的话就会造成各种不一致的麻烦(你总不希望发生数据库重复连接的异常吧) 二.如何保证类的实例只有一个? (这个问题看似简单,但如果没有接触过单件模式的话,要自己想出来解决方案还是需要一些天赋的..不信的话,可以试着想想..) 1.类的实例可能只有一个吗?貌似只要知道类名就可以随便new了吧?

《Head First 设计模式》学习笔记——单件模式

设计模式 单件模式:确保一个类只有一个实例,并提供一个全局访问点. 要点 单件模式确保程序中一个类最多只有一个实例. 在Java中实现单件模式需要私有的构造器.一个静态方法.一个静态变量. 确定在性能和资源上的限制,然后小心翼翼的选择适当的方案来实现单件,以解决多线程问题. 全局变量缺点 如果将对象赋值给一个全局变量,那么必须在程序一开始就创建好对象.万一对象非常耗费资源,而程序在这次执行过程中并没有使用它,就形成了浪费. 单件模式 public class Singleton { //利用一个

Java设计模式——单件模式

概述: 有一些对象其实我们只需要一个,比方说:线程池(threadpool).缓存(cache).对话框.处理偏好设置和注册表(registry)的对象.日志对象,充当打印机.显卡等设备的驱动程序的对象.事实上,这类对象只能有一个实例,如果制造出多个对象,就会导致许多的问题产生,例如:程序的行为异常.资源使用过量,或者是不一致的结果.--<Head First设计模式> 使用环境: 当我们的对象在逻辑上只能有一个的时候,比如说打印机.想像一下,如果我们有两个指向同一台打印机的对象,这时我们应该