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

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

单例模式(SingletonPattern):确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。单例模式是创建型模式的一种,是创建型模式中最简单的设计模式用于创建那些

单例模式(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中随机选择一台服务器来响应客户端的请求,实现代码如下:

///


/// 负载均衡器:单例类,真实环境可能非常复杂,这里只列出部分与模式相关的代码
///

public class LoadBalancer
{
//私有静态成员变量,保存唯一实例
private static LoadBalancer instance = null;
//服务器集合
private List serverList = null;
///
/// 私有构造函数
///

private LoadBalancer()
{
serverList = new List();
}
///
/// 公有静态成员方法,返回唯一实例
///

///
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];
}
}

客户端测试代码:

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)

饿汉式单例类是实现起来最简单的单例类。

定义一个静态变量,并在定义的时候就实例化单例类,这样在类加载的时候就已经创建了单例对象。

代码:

///


/// 饿汉式单例
///

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 来进行线程锁定

///


/// 懒汉式单例类
///

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)。代码如下:

///


/// 懒汉式单例类
///

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)
{
//第二重判断
if (instance == null)
instance = new LazySingleton();
}
}
return instance;
}
}

3.饿汉式单例类与懒汉式单例类比较

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

好处:



  1. 无需考虑多线程的访问问题,可以确保实例的唯一性。

  2. 由于单例对象一开始就被创建好了,所以在调用速度上和反应时间上无需等待,这点要优于懒汉式。

缺点:



  1. 无论系统在运行时是否需要使用该单例对象,但是它一开始就被创建好了,如果该单例对象只是在某个地方才用到,那么一开始就创建单例对象将会造成资源浪费。

  2. 如果单例类实例化需要的时间比较长,程序运行的时候又用不到,那么将会增加系统不必要的加载时间。

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

好处:



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

缺点:



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


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

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

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

///


/// 静态内部类单例,线程安全
///

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


参考资料:



推荐阅读
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了在使用Python中的aiohttp模块模拟服务器时出现的连接失败问题,并提供了相应的解决方法。文章中详细说明了出错的代码以及相关的软件版本和环境信息,同时也提到了相关的警告信息和函数的替代方案。通过阅读本文,读者可以了解到如何解决Python连接服务器失败的问题,并对aiohttp模块有更深入的了解。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • ASP.NET2.0数据教程之十四:使用FormView的模板
    本文介绍了在ASP.NET 2.0中使用FormView控件来实现自定义的显示外观,与GridView和DetailsView不同,FormView使用模板来呈现,可以实现不规则的外观呈现。同时还介绍了TemplateField的用法和FormView与DetailsView的区别。 ... [详细]
  • 本文讨论了如何在dotnet桌面(Windows)应用程序中添加图标。作者提到可以使用dotnet命令行工具与resource.rc文件一起使用来为标准.NET核心应用程序添加图标。作者还介绍了在创建控制台应用程序时如何编辑projeto1.csproj文件来添加图标。 ... [详细]
  • 本文介绍了在Linux下安装和配置Kafka的方法,包括安装JDK、下载和解压Kafka、配置Kafka的参数,以及配置Kafka的日志目录、服务器IP和日志存放路径等。同时还提供了单机配置部署的方法和zookeeper地址和端口的配置。通过实操成功的案例,帮助读者快速完成Kafka的安装和配置。 ... [详细]
author-avatar
叮叮当叮叮当叮叮当_212
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有