目录

  • 设计模式——单例模式
    • 1. 模式简介
    • 2. 实例1-窗口的单例模式
    • 3. 实例2-读取设置文件(懒汉式)
    • 4. 重构实例2-对建立实例操作加锁
    • 5. 重构实例2-饿汉式
    • 6. 重构实例2-静态内部类
    • 7. 单例模式的扩展-有上限的多例模式
    • 8. 总结剖析
    • 9. 参考及源码
shanzm-2020年4月8日 22:37:28

1. 模式简介

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

实现单例模式的方式:私有化组织函数,添加一个静态的字段保留类的唯一实例,并提供一个接见该实例的静态方式GetInstance()

单例模式分为两种:“懒汉式单例模式”和“饿汉式单例模式”。

懒汉式单例模式:第一次挪用建立工具的方式GetInstance()时建立类的单例工具

饿汉式单例模式:类加载的时刻建立单例工具

单例模式UML如下:


2. 实例1-窗口的单例模式

2.1 靠山说明

示例源于《鬼话设计模式》,示例中完整源代码下载

建立一个WinForm项目,其中有一个FromParent窗口,该窗口作为其他的子窗口的容器

FromParent窗口中有一个菜单栏,其中有一个ToolBox按钮,点击该按钮建立一个FromToolBox子窗口

现在要求FromToolBox子窗口一次只能建立一个。

为了实现一个类只能实例化一个工具,我们可以将组织函数改为私有的,使得该类之外无法实例化该类,而将实例化工具的放在静态函数GetInstanc()中

完整的Demo代码下载

2.2 代码实现

①FromToolBox窗口代码

 public partial class FormToolBox : Form
{
    private FormToolBox()
    {
        InitializeComponent();
    }
    private static FormToolBox ftb = null;
    public static FormToolBox GetInstance()
    {
        //存储唯一工具的字段为空,或者窗口工具已经被释放
        if (ftb == null || ftb.IsDisposed)
        {
            ftb = new FormToolBox();
            ftb.MdiParent = FormParent.ActiveForm;
        }
        return ftb;
    }
}

②FromParent窗口代码

public partial class FormParent : Form
{
    public FormParent()
    {
        InitializeComponent();
    }

    private void FormParent_Load(object sender, EventArgs e)
    {
        //FormParent作为子窗口的容器
        this.IsMdiContainer = true;
    }

    //点击菜单-ToolBox按钮,建立FromToolBox按钮
    private void toolBoxToolStripMenuItem_Click(object sender, EventArgs e)
    {
        FormToolBox.GetInstance().Show();
    }
}

2.3 程序类图

只展示一下单例窗口FormToolBox的类图:


3. 实例2-读取设置文件(懒汉式)

3.1 靠山说明

这个实例是改编自《研磨设计模式》,示例中完整源代码下载

在程序中读取项目的设置文件App.Config

界说一个设置的类AppConfig,在该类中实现读取App.Config中的所有设置数据

每一项的设置数据在AppConfig类中设置一个只读属性,若是系统中需要使用设置,则建立一个AppConfig工具,读取该工具中的所有关于设置的只读属性。

这样的AppConfig实在就是封装着所有的设置数据。在系统中可能多处需要使用设置数据,若是每次使用设置数据,我们就建立一个AppConfig工具,则在系统的内存中会有多个AppConfig工具,异常的虚耗系统的内存资源,尤其是设置数据较多的时刻!

读取设置文件的实例在系统中使用一个即可,即通过单例模式显示实例唯一。

3.2 代码实现

①新建一个控制台项目

添加引用:System.Configuration

在项目的设置文件App.config中添加自界说的设置数据如下:

<configuration>
  <appSettings >
    <add key="server" value ="."/>
    <add key="database" value ="db_Test"/>
    <add key="uid" value ="shanzm"/>
    <add key="pwd" value ="123456"/>
  </appSettings>
</configuration>

注重此处我只是拿数据库毗邻字符串举一个例子,
我们通常会数据库毗邻字符串写在<connectionStrings>标签中

②新建一个AppConfig类

using System.Configuration;
public class AppConfig
{
    //界说私有字段,用于存储唯一实例
    private static AppConfig appConfig = null;

    //界说只读属性,对应设置中的key
    public string Server { private set; get; }
    public string DataBase { private set; get; }
    public string UserId { private set; get; }
    public string PassWord { private set; get; }

    //组织函数私有化
    private AppConfig()
    {
        Server = ConfigurationManager.AppSettings["server"];
        DataBase = ConfigurationManager.AppSettings["databaser"];
        UserId = ConfigurationManager.AppSettings["uid"];
        PassWord = ConfigurationManager.AppSettings["pwd"];   
    }

    //获取唯一实例
    public static AppConfig GetInstance()
    {
        if (appConfig == null)
        {
            appConfig = new AppConfig();
        }
        return appConfig;
    }
}

③客户端挪用

static void Main(string[] args)
{
    AppConfig appConfig1 = AppConfig.GetInstance();
    string connectionString = $"server={appConfig1.Server},databas={appConfig1.DataBase},uid ={appConfig1.UserId},pwd ={appConfig1.PassWord}";

    Console.WriteLine(connectionString);//print:server=.,database=db_Test,uid=shanzm,pwd=123456

    AppConfig appConfig2 = AppConfig.GetInstance();
    Console.WriteLine(object.ReferenceEquals(appConfig1, appConfig2));//print:true  
    //系统中所的appConfig工具都是同一个
    Console.ReadKey();
}

3.3 程序类图


4. 重构实例2-对建立实例操作加锁

实在多线程同时挪用AppConfig.GetInstance()方式的时刻,会在内存中建立多个实例。

好比说在实例2中,我们使用Parallel.Invoke(),并行挪用GetInstance(),会发现有可能在内存中建立多个AppConfig工具。

以是实在也就损坏了单例模式。

static void Main(string[] args)
{
    AppConfig appConfig1 = null;
    AppConfig appConfig2 = null;
    Action createA = () => appConfig1 = AppConfig.GetInstance();
    Action createB = () => appConfig2 = AppConfig.GetInstance();
    Parallel.Invoke(createA, createB);
    Console.WriteLine(object.ReferenceEquals(appConfig1, appConfig2));
    //print:false。即系统中的appConfig工具不是同一个
    //注重这里有时可能是true,即  Parallel.Invoke(createA, createB)中的委托执行可能存在时间差
    //注重你使用异步是无法模拟出false的,由于异步不是同时去执行GetInstanc()
    Console.ReadKey();
}

解决方式:编写线程平安的代码,对建立AppConfig工具的操作加锁!

private static readonly object asyncRoot = new object();
public static AppConfig GetInstance()
{
    if (appConfig == null)
    {
        lock (asyncRoot)
        {
            if (appConfig == null)
            {
                appConfig = new AppConfig();
            }
        }
    }
    return appConfig;
}

【代码说明】:

  • 这里先判断单例工具是否存后再对线程加锁,即不是对线程每次都加锁,只是单例工具不存在的时刻再加锁,这称之为双重锁定(double check lock)

  • 在加锁前先判断单例工具是否已经存在,在加锁后再次判断单例工具是否存在,是有需要的。好比当前单例工具尚不存在,两个线程中有一个通过线程A锁,又通过工具为空的判断,最先建立单例工具,而此时线程锁外有一个线程B守候,若是没有第二重的工具为空的判断,线程B可以继续建立一个新的工具,损坏了单例模式。


5. 重构实例2-饿汉式

那么问题来了,这里为什么要把存储单例工具的字段界说为只读的静态字段呢?

首先回首一下静态字段

  1. 区分静态字段和非静态字段:

    使用 static 修饰符声明的字段界说了一个静态字段 (static field)。一个静态字段只标识一个存储位置。无论对一个类建立多少个实例,它的静态字段永远都只有一个副本。

    不使用 static 修饰符声明的字段界说了一个实例字段 (instance field)。类的每个实例都为该类的所有实例字段包罗一个单独副本。

    简而言之,静态字段属于类,非静态字段属于类的实例工具。

  2. 静态字段和非静态字段初始化:

    对于静态字段,变量初始值设定项相当于在类初始化时代执行的赋值语句。
    对于实例字段,变量初始值设定项相当于建立类的实例时执行的赋值语句。

Java、C#等强类型语言提供了静态初始化的方式,通过静态初始化方式,静态的字段在内存中只允许有一个赋值,以是就不需要忧郁多线程同时挪用GetInstance()建立了多个单例类的工具。这样在这里就不需要程序员显示的编写线程平安代码,即可制止多线程下的单例模式被损坏的问题。

这种静态初始化的方式是在类自己被加载的时刻将自己实例化,被称之为“饿汉式单例类”,

而之前需要在类在第一次被引用的时刻将自己实例化的方式称之为“懒汉式单例类

对AppConfig类举行修改,实现懒汉式单例模式,如下:

public sealed class AppConfig//类界说为密封类,防止派生类建立工具
{
    //界说字段,用于存储唯一实例
    private static readonly AppConfig appConfig = new AppConfig();
    //用于存储实例的字段界说为只读的,则只能在类静态初始化给其赋值
    //给 readonly 字段的赋值只能作为字段声明的组成部分泛起,或在同一个类中的组织函数中泛起。

    //对应设置文件设置响应的属性,注重是只读的
    public string Server { private set; get; }
    public string DataBase { private set; get; }
    public string UserId { private set; get; }
    public string PassWord { private set; get; }
    //组织函数私有化
    private AppConfig()
    {
        Server = ConfigurationManager.AppSettings["server"];
        DataBase = ConfigurationManager.AppSettings["databaser"];
        UserId = ConfigurationManager.AppSettings["uid"];
        PassWord = ConfigurationManager.AppSettings["pwd"];
    }
    //获取唯一实例
    public static AppConfig GetInstance()
    {
        return appConfig;
    }
}

懒汉式和饿汉式的区别

  • 饿汉式单例模式:

    饿汉式即静态初始化的方式,它是在类一加载的时刻就实例化工具,以是要提前占用系统资源。即饿汉式没有实现延迟加载,无论是否挪用,都市占用系统资源

  • 懒汉式单例模式:

    懒汉式体现了延迟加载(Lazy Load),所谓延迟加载就是当在真正需要数据的时刻,才真正执行数据加载操作,这样可以尽可能的节约内存资源。

    懒汉式线程不平安,需要做双重锁定这样的处置才可以保证平安。


6. 重构实例2-静态内部类

懒汉式线程不平安,饿汉式没有实现延迟加载,虚耗资源

以是可以继续对懒汉式和饿汉式举行修改,使用静态内部类,既可以实现线程平安又可以实现延迟加载。

首先看一下静态内部类的界说:

  • 类级内部类:在类(这个类称之为外部类)内部界说一个静态类,该类内部的静态类称之为类级内部类,也称之为静态内部类

  • 工具级内部类:在类内部界说一个非静态类,则该内部类称之为工具级内部类

在静态内部类中建立外部类的单例工具,这样一来既实现了静态初始化(保证了线程平安),又确保在不使用外部类的时刻是不会建立单例工具(实现了延迟加载)

修改AppConfig代码如下:

仔细想想实在是异常巧妙的

public sealed class AppConfig
{
    //静态内部类
    private static class AppConfigHolder
    {
        //注重将内部静态类的默认组织函数改为静态的
        static AppConfigHolder()
        {
        }
        //使用静态初始化器,保证了线程平安。
        internal static AppConfig appConfig = new AppConfig();
    }
    //对应设置文件设置响应的属性,注重是只读的
    public string Server { private set; get; }
    public string DataBase { private set; get; }
    public string UserId { private set; get; }
    public string PassWord { private set; get; }
    //组织函数私有化
    private AppConfig()
    {
        Server = ConfigurationManager.AppSettings["server"];
        DataBase = ConfigurationManager.AppSettings["databaser"];
        UserId = ConfigurationManager.AppSettings["uid"];
        PassWord = ConfigurationManager.AppSettings["pwd"];
    }
    //获取唯一实例
    public static AppConfig GetInstance()
    {
        //此时挪用静态内部类中的静态字段,保证了只有在使用到AppConfig工具的时刻才建立工具
        return AppConfigHolder.appConfig;
    }
}

7. 单例模式的扩展-有上限的多例模式

单例模式的本质就是控制实例工具的个数,以是当系统中需要某个类只有一个实例工具的时刻,我们可以使用单例模式。

而若是一个类需要有几个实例化工具共存,则我们可以对单例模式举行扩展,限制一个类发生牢固数目的实例化工具。

这种限制类发生牢固数目的实例工具的模式就叫做有上限的多例模式,它是单例模式的一种扩展。

接纳有上限的多例模式,我们可以在设计时决议在内存中有多少个实例,利便系统举行扩展,修正单例可能存在的性能问题,提供系统的响应速率。

修改AppConfig类,允许AppConfig类最多有三个实例工具,修改如下:

class AppConfigExtend
{
    //界说字典类型字段保留所有的实例
    private static Dictionary<int, AppConfigExtend> dic = new Dictionary<int, AppConfigExtend>();
    //界说key
    private static int key = 1;
    //界说最大的实例数
    private static int MaxInstance = 3;

    //对应设置文件设置响应的属性
    public string Server { private set; get; }
    public string DataBase { private set; get; }
    public string UserId { private set; get; }
    public string PassWord { private set; get; }
    //私有化组织函数
    private AppConfigExtend()
    {
        Server = ConfigurationManager.AppSettings["server"];
        DataBase = ConfigurationManager.AppSettings["database"];
        UserId = ConfigurationManager.AppSettings["uid"];
        PassWord = ConfigurationManager.AppSettings["pwd"];
    }
    //类外方式接见实例的接见点
    public static AppConfigExtend GetInstance()
    {
        if (!dic.ContainsKey(key))
        {
            dic.Add(key, new AppConfigExtend());
        }
        AppConfigExtend appConfig = dic[key];
        key++;
        if (key > MaxInstance)
        {
            key = 1;
        }
        return appConfig;
    }
}

注:这里实现的有上限多例模式并不是线程平安的,只做演示。

客户端通过HashSet存储AppConfigExtend实例工具,举行测试:

static void Main(string[] args)
{
    HashSet<AppConfigExtend> hashset = new HashSet<AppConfigExtend>();
    for (int i = 0; i < 10; i++)//多次获取AppConfigExtend工具
    {
        hashset.Add(AppConfigExtend.GetInstance());
    }
    Console.WriteLine(hashset.Count());//print:3 即全局中AppConfigExtend就只有3个实例工具
    Console.ReadKey();
}

8. 总结剖析

8.1 注重事项

  • 单例模式中一样平常使用GetInstance()方式作为获取单例工具的方式,这个方式作为单例模式中全局唯一接见类实例的接见点。

    该方式必须为静态,为何?由于该方式就是建立(获取)工具的方式,若是非静态的,那么怎么先建立一个工具再挪用该方式呢?以是一定要静态的方式。

  • 单例类中保留唯一工具的字段instance也必须为静态的。为何?由于GetInstance()方式是静态的,而该方式中使用了instance字段。(静态方式属于类级方式,所有其操作的变量也就必须是类级变量)

8.2 优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个工具需要频仍地建立、销毁时,而且建立或销毁时性能又无法优化,单例模式的优势就异常显著。

  • 提供了对唯一实例的受控接见。由于单例类封装了它的唯一实例,以是它可以严格控制客户怎样以及何时接见它,并为设计及开发团队提供了共享的观点。

8.3 瑕玷

  • 单例类的职责过重,在一定程度上违反了“单一职责原则”。由于单例类既充当了工厂角色(即建立工具),同时又充当了产物角色(即实例工具),包罗一些营业方式,将产物的建立和产物的自己的功效融合到一起。

  • 滥用单例将带来一些负面问题,如为了节约资源将数据库毗邻池工具设计为单例类,可能会导致共享毗邻池工具的程序过多而泛起毗邻池溢出;现在许多面向工具语言(如Java、C#)的运行环境都提供了自动垃圾接纳的手艺,因此,若是实例化的工具长时间不被行使,系统会以为它是垃圾,会自动销毁并接纳资源,下次行使时又将重新实例化,这将导致工具状态的丢失。

8.4 顺应场所

  • 笼统的说:某类要求只能天生一个工具的时刻就可以使用单例模式。

  • 当工具需要被共享的场所。由于单例模式只允许建立一个工具,共享该工具可以节约内存,并加速工具接见速率。如 Web 中的设置工具、数据库的毗邻池等。

  • 当某类需要频仍实例化,而建立的工具又频仍被销毁的时刻,如多线程的线程池、网络毗邻池等


9. 参考及源码

  • 示例中源代码下载

  • 设计模式——面向工具的设计原则

  • 设计模式——简朴工厂模式

  • 设计模式——工厂方式模式

  • 《鬼话设计模式》

    《设计模式实训教程-第二版》

    《研磨设计模式》

    《图说设计模式》

,

sunbet

www.0577meeting.com提供官方APP下载,游戏火爆,口碑极好,服务一流,一直是sunbet会员的首选。

发布评论

分享到:

伊春人才网:【泰达】无法来华与天津泰达队齐集,施蒂利克:不能让俱乐部失望
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。