首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一起学习设计模式--01.单例模式

一起学习设计模式--01.单例模式

作者头像
独立观察员
发布于 2022-12-06 10:39:04
发布于 2022-12-06 10:39:04
60600
代码可运行
举报
运行总次数:0
代码可运行

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

单例模式是创建型模式的一种,是创建型模式中最简单的设计模式 用于创建那些在软件系统中独一无二的对象。虽然单例模式很简单,但是它的使用频率还是很高的。

学习难度:★☆☆☆☆

使用频率:★★★★☆

一、单例模式的动机

任务管理器相信大家都不陌生,大家可以用自己的电脑做个尝试,在Windows的任务栏的右键菜单中多次点击“任务管理器”,看能否打开多个任务管理器窗口。正常情况下,无论任务管理器启动多少次,Windows系统始终只会打开一个任务管理器窗口,也就是说,在一个Windows系统中,任务管理器存在唯一性。

在实际的开发中也经常遇到过类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功后,无法再创建一个同类型的其它对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。

二、单例模式的概述

1.单例模式的定义

单例模式(Singleton Pattern):确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类也称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

2.单例模式的3个要点
  1. 某个类只能有一个实例
  2. 它必须自行创建这个实例
  3. 它必须自行向整个系统提供这个实例
3.结构图

从上图可以看出,单例类模式结构图中只包含一个单例角色。Singleton(单例):

  1. 在单例类的内部实现只生成一个实例,同时它提供一个静态的GetInstance()方法,让客户可以访问它的唯一实例。
  2. 为了防止在外部对单例实例化,它的构造函数可见性为private。
  3. 在单例类的内部定义了一个Singleton类型的静态对象,作为供外部共享访问的唯一实例。

三、负载均衡器的设计

1.需求

A科技公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,即只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。

2.结构图

A科技公司的研发部开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器。结构如下图:

3.实现

在上边的结构图中,将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 负载均衡器:单例类,真实环境可能非常复杂,这里只列出部分与模式相关的代码
    /// </summary>
    public class LoadBalancer
    {
        //私有静态成员变量,保存唯一实例
        private static LoadBalancer instance = null;
        //服务器集合
        private List<string> serverList = null;

        /// <summary>
        /// 私有构造函数
        /// </summary>
        private LoadBalancer()
        {
            serverList = new List<string>();
        }

        /// <summary>
        /// 公有静态成员方法,返回唯一实例
        /// </summary>
        /// <returns></returns>
        public static LoadBalancer GetLoadBalancer()
        {
            if (instance == null)
                instance = new LoadBalancer();
            return instance;
        }

        //增加服务器
        public void AddServer(string server)
        {
            serverList.Add(server);
        }

        //删除服务器
        public void RemoveServer(string server)
        {
            serverList.Remove(server);
        }

        //使用Random类随机获取服务器
        public string GetServer()
        {
            var random = new Random();
            var i = random.Next(serverList.Count);
            return serverList[i];
        }
    }

客户端测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    class Program
    {
        static void Main(string[] args)
        {
            //创建4个LoadBalancer对象
            LoadBalancer balancer1, balancer2, balancer3, balancer4;
            balancer1 = LoadBalancer.GetLoadBalancer();
            balancer2 = LoadBalancer.GetLoadBalancer();
            balancer3 = LoadBalancer.GetLoadBalancer();
            balancer4 = LoadBalancer.GetLoadBalancer();

            //判断服务器负载均衡器是否相同
            if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4)
            {
                Console.WriteLine("服务器负载均衡器具有唯一性!");
            }

            //增加服务器
            balancer1.AddServer("server 1");
            balancer1.AddServer("server 2");
            balancer1.AddServer("server 3");
            balancer1.AddServer("server 4");

            for (int i = 0; i < 10; i++)
            {
                var server = balancer1.GetServer();
                Console.WriteLine("分发请求至服务器:" + server);
            }
        }
    }

编译并运行程序,结果如下:

从运行结果可以看出,虽然我们创建了4个LoadBalancer对象,但他们是同一个对象。因此,通过使用单例模式可以确保LoadBalancer对象的唯一性。

四、饿汉式单例和懒汉式单例

研发部的开发人员使用单例模式实现了负载均衡器的设计,但是在实际使用中出现了一个非常严重的问题。当负载均衡器在启动过程中用户再次启动负载均衡器时,系统无任何异常,但是当客户端提交请求时出现请求分发失败。通过仔细分析发现原来系统中还是存在多个负载均衡器对象,导致分发时目标服务器不一致,从而产生冲突。

现在对负载均衡器的实现代码进行再次的分析。当第一次调用 GetLoadBalancer() 方法创建并启动负载均衡器时,instance 对象为 null,因此系统将执行代码 instance=new LoadBalancer() ,在此过程中,由于要对 LoadBalancer 进行大量初始化工作,需要一段时间来创建 LoadBalancer 对象。而此时如果再一次调用 GetLoadBalancer() 方法(通常发生在多线程环境中),由于 instance 尚未创建成功,此时仍然为null,判断条件“instance==null”仍然为true,代码 instance=new LoadBalancer() 将被再次执行,最终导致创建了多个 instance 对象,这违背了单例模式的初衷,也导致系统发生运行错误。

如何解决该问题?至少有两种解决方案,这就是接下来的饿汉式单例类懒汉式单例类

1.饿汉式单例类(Eager Singleton)

饿汉式单例类是实现起来最简单的单例类。定义一个静态变量,并在定义的时候就实例化单例类,这样在类加载的时候就已经创建了单例对象。

代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 饿汉式单例
    /// </summary>
    public class EagerSingleton
    {
        //定义静态变量并实例化单例类
        private static readonly EagerSingleton instance = new EagerSingleton();

        //私有构造函数
        private EagerSingleton()
        {
        }

        //获取单例对象
        public static EagerSingleton GetInstance()
        {
            return instance;
        }
    }

如果使用饿汉式单例来实现负载均衡器LoadBalancer的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。

2.懒汉式单例类与线程锁定

除了饿汉式单例外,还有一种经典的懒汉式单例,也就是前边最开始提到的负载均衡器的实现方式。

懒汉式单例在第一次调用GetInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(lazy Load)技术,即需要的时候再加载实例。

为了避免多个线程同时调用GetInstance()方法,C#中可以使用 Lock 来进行线程锁定

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 懒汉式单例类
    /// </summary>
    public class LazySingleton
    {
        //私有静态成员变量,保存唯一实例
        private static LazySingleton instance = null;
        private static readonly object syncLocker = new object();

        private LazySingleton() {}

        //公有静态成员方法,返回唯一实例
        public static LazySingleton GetInstance()
        {
            if (instance == null)
            {
                //锁定代码块
                lock (syncLocker)
                {
                    instance = new LazySingleton();
                }
            }

            return instance;
        }
    }

问题似乎得到了解决,但事实并非如初。如果使用上边的代码来创建单例对象,仍然会出现单例对象不唯一的问题。原因如下:

假如某一瞬间线程A和线程B都在调用 GetInstance() 方法,此时 instance 对象为null,均能通过“instance==null”的判断。由于实现了加锁机制,线程A进入锁定的代码块中执行实例创建代码,那么此时线程B则处于排队等待状态,必须等线程A执行完毕后才可以进入lock代码块。但是当线程A执行完毕后,线程B并不知道实例已经创建,所以会继续进行新实例的创建,那么将会导致产生多个单例对象,违背了单例模式的设计思想。因此需要进一步改进,需要在锁定的代码块中再进行一次“instance==null”的判断,判断进入锁定代码块后是否有其它线程已经创建了单例类就可以了,这种方式称为双重检查锁定(Double-Check Locking)。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 懒汉式单例类
    /// </summary>
    public class LazySingleton
    {
        //私有静态成员变量,保存唯一实例
        private static LazySingleton instance = null;
        private static readonly object syncLocker = new object();

        private LazySingleton() {}

        /// <summary>
        /// 公有静态成员方法,返回唯一实例
        /// </summary>
        /// <returns></returns>
        public static LazySingleton GetInstance()
        {
            //第一重判读
            if (instance == null)
            {
                //锁定代码快
                lock (syncLocker)
                {
                    //第二重判断
                    if (instance == null)
                        instance = new LazySingleton();
                }
            }

            return instance;
        }
    }
3.饿汉式单例类与懒汉式单例类比较

饿汉式单例类:在类被加载时就将自己实例化。好处:

  1. 无需考虑多线程的访问问题,可以确保实例的唯一性。
  2. 由于单例对象一开始就被创建好了,所以在调用速度上和反应时间上无需等待,这点要优于懒汉式。

缺点:

  1. 无论系统在运行时是否需要使用该单例对象,但是它一开始就被创建好了,如果该单例对象只是在某个地方才用到,那么一开始就创建单例对象将会造成资源浪费。
  2. 如果单例类实例化需要的时间比较长,程序运行的时候又用不到,那么将会增加系统不必要的加载时间。

懒汉式单例类:在类第一次使用时创建。好处:

  1. 无需一直占用系统资源,实现了延迟加载。

缺点:

  1. 多线程同时访问时,如果单例类的实例化比较耗时,那么多个线程同时首次引用此类的概率就会变大,那么每个线程都需要经过双重检查锁定机制,这会给系统带来性能的影响。

五、一种更好的单例实现方法

饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据着内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。有没有一种方法能够同时将这两种方式的缺点都克服呢?有!那就是静态内部类单例

需要在单例类中增加一个静态(static)内部类。在该类内部中创建单例对象,再将该单例对象通过GetInstance()方法返回给外部使用,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /// <summary>
    /// 静态内部类单例,线程安全
    /// </summary>
    public class StaticSingleton
    {
        //私有构造函数,防止从外边实例化
        private StaticSingleton(){}

        //公有静态成员方法,返回唯一实例
        public static StaticSingleton GetInstance()
        {
            return InnerClass.instance;
        }

        //内部类,第一次调用GetInstance()时加载InnerClass
        class InnerClass
        {
            //在类被实例化或静态成员被调用的时候进行调用
            //这里也就是当instance被调用的时候,会执行静态函数,初始化成员变量
            static InnerClass(){}
            internal static readonly StaticSingleton instance = new StaticSingleton();
        }
    }

instance并没有作为StaticSingleton的成员变量直接实例化,所以在类加载的时候不会实例化StaticSingleton。第一次调用GetInstance()方法时,将加载内部类InnerClass,该内部类定义了一个static类型的变量instance,这时首先会初始化这个成员变量,由.NET框架来保证线程安全性,确保该成员变量只能初始化一次。由于GetInstance()并没有被任何线程锁定,因此不会造成任何性能影响。

静态构造函数:

  1. 是由.Net框架来执行的
  2. 没有参数,因为框架不知道我们要传什么参数
  3. 必须以static标识,并且没有 public 和 private
  4. 静态构造函数中不能初始化实例变量
  5. 静态构造函数的调用时机,是在类被实例化静态成员被调用的时候进行调用,并且由.NET框架来调用静态构造函数来初始化静态成员变量
  6. 一个类中只能有一个静态构造函数
  7. 无参的静态构造函数和无参的构造函数可以共同存在
  8. 静态构造函数只会被执行一次

六、单例模式的总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率非常高,在很多软件和框架中都得以广泛的应用。

1.主要优点
  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  2. 系统中只存在一个对象,因此可以节约系统资源。对于那些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  3. 允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,即节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供执行数目实例对象的类可称之为多例类)比如:数据库连接池、线程池等
2.主要缺点
  1. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
  2. 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类中即提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
  3. 现在很多面向对象语言(Java、C#)的运行环境都提供了自动垃圾回收技术,因此,如果实例化的共享对象长时间不被利用,系统就会认为它是垃圾,会自动销毁并回收资源,等到下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
  1. 系统只需要一个实例对象。例如,系统需要提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点。除了该公共访问点,不能通过其它途径访问该实例

示例代码:

https://github.com/crazyliuxp/DesignPattern.Simples.CSharp

参考资料:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 独立观察员博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
设计模式的征途—1.单例(Singleton)模式
  单例模式属于创建型模式的一种,创建型模式是一类最常用的设计模式,在软件开发中应用非常广泛。创建型模式将对象的创建和使用分离,在使用对象时无需关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。每一个创建型模式都在视图回答3个问题:3W -> 创建什么(What)、由谁创建(Who)和何时创建(When)。
Edison Zhou
2018/08/20
6950
设计模式的征途—1.单例(Singleton)模式
朋友问我单例模式是什么?
某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高了系统的整体处理能力,缩短了响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键,试使用单例模式设计服务器负载均衡器。
千羽
2021/12/29
5330
朋友问我单例模式是什么?
【设计模式】之单例模式
单例模式是23种设计模式中最简单、最常见的一种,也是各个公司面试题中必考的设计模式之一,是程序猿必备掌握的。
xcbeyond
2020/04/02
3900
设计模式~单例模式
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
Vincent-yuan
2020/08/11
2770
设计模式(一)之单例模式
关键代码:构造函数是私有的。 应用场景: 1.一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。 2.创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。 3.Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 UML图:
周杰伦本人
2023/10/12
2570
设计模式(一)之单例模式
Java设计模式-单例模式
作为对象的创建模式,单例模式确保其某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类。单例模式有以下特点:
小勇DW3
2018/08/30
4810
一天一个设计模式:单例模式
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例。
用户1134788
2018/12/05
3860
Java设计模式学习记录-单例模式
前言 已经介绍和学习了两个创建型模式了,今天来学习一下另一个非常常见的创建型模式,单例模式。 单例模式也被称为单件模式(或单体模式),主要作用是控制某个类型的实例数量是一个,而且只有一个。 单例模式 单例模式的实现方式  实现单例模式的方式有很多种,大体上可以划分为如下两种。 外部方式 在使用某些全局对象时,做一些“try-Use”的工作。就是如果要使用的这个全局对象不存在,就自己创建一个,把它放到全局的位置上;如果本来就有,则直接拿来使用。 内部实现方式 类型自己控制正常实例的数量,无论客户程序是否尝试过
纪莫
2018/07/04
4120
单例模式详解
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
孟君
2019/08/29
5800
单例模式详解
设计模式-单例模式
单例模式是保证系统实例唯一性的重要手段。单例模式首先通过将类的实例化方法私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例,用户只需要也只能通过调用该方法获取类的实例。
别团等shy哥发育
2023/10/17
3270
设计模式—— 七 :单例模式
Sngleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实 例,并且是自行实例化的(在Singleton中自己使用new Singleton())。
三分恶
2020/07/16
3160
23种设计模式之单例模式
单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。
用户2146693
2019/08/08
3040
23种设计模式之单例模式
单例模式【单例设计模式】
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
高大北
2022/06/14
1.9K0
单例模式【单例设计模式】
Java描述设计模式(01):单例模式
Singleton称为单例类,构造函数使用private修饰,确保系统中只能产生一个实例,并且自动生成的。上面代码也就是所谓的懒汉式加载:只有到使用该对象的时候才来创建,意思饿了才来做饭吃。
知了一笑
2019/07/19
3960
单例模式 创建型 设计模式(六)
可以借助于全局变量,但是类就在那里,你不能防止实例化多个对象,可能一不小心谁就创建了一个对象
noteless
2018/11/21
4860
GoF 23种经典的设计模式——单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
Andromeda
2024/01/14
1800
当Kotlin邂逅设计模式之单例模式(一)
简述: 从这篇文章开始,我将带领大家一起来探讨一下Kotlin眼中的设计模式。说下为什么想着要开始这么一个系列文章。主要基于下面几点原因:
bennyhuo
2020/02/20
1K0
C# 程序开发:设计模式之单例模式
1、定义:单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。
用户4831957
2019/07/02
6090
设计模式(五):单例模式
设计模式(五):单例模式
Java架构师必看
2021/05/14
2150
设计模式——单例模式详解
饿汉式:在类加载的时候已经创建好该单例对象。 懒汉式:在需要使用对象的时候才会去创建对象
小尘要自信
2023/10/10
1.3K0
设计模式——单例模式详解
相关推荐
设计模式的征途—1.单例(Singleton)模式
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验