单例模式属于创建型模式的一种,创建型模式是一类最常用的设计模式,在软件开发中应用非常广泛。创建型模式将对象的创建和使用分离,在使用对象时无需关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。每一个创建型模式都在视图回答3个问题:3W -> 创建什么(What)、由谁创建(Who)和何时创建(When)。

  本篇是创建型模式的第一篇,也是最简单的一个设计模式,虽然简单,但是其使用频率确是很高的。

单例模式(Singleton) 学习难度:★☆☆☆☆ 使用频率:★★★★☆

一、单例模式的动机

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

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

二、单例模式概述

2.1 要点

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

  单例模式有3个要点:

  • 某个类只能有一个实例
  • 它必须自行创建这个实例
  • 它必须自行向整个系统提供这个实例  

2.2 结构图

  从上图中可以看出,单例模式结构图中只包含了一个单例的角色。

  Singleton(单例):

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

三、负载均衡器的设计

3.1 软件需求

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

  如何确保负载均衡器的唯一性成为了这个软件成功地关键。

3.2 撸起袖子加油干

  成都分公司的开发人员通过分析和权衡,决定使用单例模式来设计这个负载均衡器,于是撸起袖子画了一个结构图如下:

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

    /// <summary>/// 假装自己是一个负载均衡器/// </summary>public class LoadBalancer{// 私有静态变量,存储唯一实例private static LoadBalancer instance = null;// 服务器集合private IList<CustomServer> serverList = null;// 私有构造函数private LoadBalancer(){serverList = new List<CustomServer>();}// 公共静态成员方法,返回唯一实例public static LoadBalancer GetLoadBalancer(){if (instance == null){instance = new LoadBalancer();}return instance;}// 添加一台Serverpublic void AddServer(CustomServer server){serverList.Add(server);}// 移除一台Serverpublic void RemoveServer(string serverName){foreach (var server in serverList){if (server.Name.Equals(serverName)){serverList.Remove(server);break;}}}// 获得一台Server - 使用随机数获取private Random rand = new Random();public CustomServer GetServer(){int index = rand.Next(serverList.Count);return serverList[index];}}/// <summary>/// 假装自己是一台服务器/// </summary>public class CustomServer{public string Name { get; set; }public int Size { get; set; }}

  现在我们在客户端代码中添加一些测试代码,看看结果:

    public class Program{public static void Main(string[] args){LoadBalancer balancer, balancer2, balancer3;balancer = LoadBalancer.GetLoadBalancer();balancer2 = LoadBalancer.GetLoadBalancer();balancer3 = LoadBalancer.GetLoadBalancer();// 判断负载均衡器是否相同if (balancer == balancer2 && balancer == balancer3 && balancer2 == balancer3){Console.WriteLine("^_^ : 服务器负载均衡器是唯一的!");}// 增加服务器balancer.AddServer(new CustomServer() { Name = "Server 1" });balancer.AddServer(new CustomServer() { Name = "Server 2" });balancer.AddServer(new CustomServer() { Name = "Server 3" });balancer.AddServer(new CustomServer() { Name = "Server 4" });// 模拟客户端请求的分发for (int i = 0; i < 10; i++){CustomServer server = balancer.GetServer();Console.WriteLine("该请求已分配至 : " + server.Name);}Console.ReadKey();}}

  运行客户端代码,查看运行结果:

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

3.3 饿汉式与懒汉式单例

  在进行测试时,成都分公司的测试人员发现负载均衡器在启动过程中用户再次启动负载均衡器时,系统无任何异常,但当客户端提交请求时出现请求分发失败,通过仔细分析发现原来系统中还是会存在多个负载均衡器的对象,从而导致分发时目标服务器不一致,从而产生冲突。

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

  So,如何解决这个问题?也就有了下面的饿汉式与懒汉式的解决方案。

 (1)饿汉式单例 

  懒汉式单例实现起来最为简单,在C#中,我们可以利用静态构造函数来实现。于是我们可以改写以上的代码块:

    public class LoadBalancer{// 私有静态变量,存储唯一实例private static readonly LoadBalancer instance = new LoadBalancer();......// 公共静态成员方法,返回唯一实例public static LoadBalancer GetLoadBalancer(){return instance;}}

C#的语法中有一个函数能够确保只调用一次,那就是静态构造函数。由于C#是在调用静态构造函数时初始化静态变量,.NET运行时(CLR)能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。

  饿汉式是在 .NET 中实现 Singleton 的首选方法。但是,由于在C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时发现第一次使用该类型的时候自动调用该类型的静态构造函数(也就是说在用到LoadBalancer时就会被创建,而不是用到LoadBalancer.GetLoadBalancer()时),这样会过早地创建实例,从而降低内存的使用效率。此外,静态构造函数由 .NET Framework 负责执行初始化,我们对对实例化机制的控制权也相对较少。

 (2)懒汉式单例

  除了饿汉式之外,还有一种懒汉式。最开始我们实现的方式就是一种懒汉式单例,也就是说,在第一个调用LoadBalancer.GetLoadBalancer()时才会实例化对象,这种技术又被称之为延迟加载(Lazy Load)。同样,我们的目标还是为了避免多个线程同时调用GetLoadBalancer方法,在C#中,我们可以使用关键字lock/Moniter.Enter+Exit等来实现,这里采用关键字语法糖lock来改写代码段:

    public class LoadBalancer{// 私有静态变量,存储唯一实例private static LoadBalancer instance = null;private static readonly object syncLocker = new object();......// 公共静态成员方法,返回唯一实例public static LoadBalancer GetLoadBalancer(){if (instance == null){lock (syncLocker){instance = new LoadBalancer();}}return instance;}}    

  问题貌似得以解决,但事实并非如此。如果使用以上代码来创建单例对象,还是会存在单例对象不一致。假设线程A先进入lock代码块内,执行实例化代码。此时线程B排队吃瓜等待,必须等待线程A执行完毕后才能进入lock代码块。但当A执行完毕时,线程B并不知道实例已经被创建,将继续创建新的实例,从而导致多个单例对象。因此,开发人员需要进一步改进,于是就有了双重检查锁定(Double-Check Locking),其改写代码如下:

    public class LoadBalancer{// 私有静态变量,存储唯一实例private static LoadBalancer instance = null;private static readonly object syncLocker = new object();......// 公共静态成员方法,返回唯一实例public static LoadBalancer GetLoadBalancer(){// 第一重判断if (instance == null){// 锁定代码块lock (syncLocker){// 第二重判断if (instance == null){instance = new LoadBalancer();}}}return instance;}}

 (3)一种更好的单例实现

  饿汉式单例不能延迟加载,懒汉式单例安全控制繁琐,而且性能受影响。静态内部类单例则将这两者有点合二为一。使用这种方式,我们需要在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过GetInstance()方法返回给外部使用,于是开发人员又改写了代码:

    public class LoadBalancer{......// 公共静态成员方法,返回唯一实例public static LoadBalancer GetLoadBalancer(){return Nested.instance;}// 使用内部类+静态构造函数实现延迟初始化class Nested{static Nested() { }internal static readonly LoadBalancer instance = new LoadBalancer();}......}

  该实现方法在内部定义了一个私有类型Nested。当第一次用到这个嵌套类型的时候,会调用静态构造函数创建LoadBalancer的实例instance。如果我们不调用属性LoadBalancer.GetLoadBalancer()

,那么就不会触发.NET运行时(CLR)调用Nested,也就不会创建实例,因此也就保证了按需创建实例(或延迟初始化)。

  可见,此方法既可以实现延迟加载,又可以保证线程安全,不影响系统性能。但其缺点是与具体编程语言本身的特性相关,有一些面向对象的编程语言并不支持此种方式。

四、单例模式总结

  单例模式目标明确,结构简单,在软件开发中使用频率相当高。

4.1 主要优点

  (1)提供了对唯一实例的受控访问。单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

  (2)由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。

  (3)允许可变数目的示例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了单例对象共享过多有损性能的问题。(Note:自行提供指定书目的实例对象的类可称之为多例类)例如,数据库连接池,线程池,各种池。

4.2 主要缺点

  (1)单例模式中没有抽象层,因此单例类的扩展有很大的困难。

  (2)单例类的职责过重,在一定程度上违背了单一职责的原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。不够,很多时候我们都需要取得平衡。

  (3)很多高级面向对象编程语言如C#和Java等都提供了垃圾回收机制,如果实例化的共享对象长时间不被利用,系统则会认为它是垃圾,于是会自动销毁并回收资源,下次利用时又得重新实例化,这将导致共享的单例对象状态的丢失。

4.3 适用场景

  (1)系统只需要一个实例对象。例如:系统要求提供一个唯一的序列号生成器或者资源管理器,又或者需要考虑资源消耗太大而只允许创建一个对象。

  (2)客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

  比如,在Flappy Bird游戏中,小鸟这个游戏对象在整个游戏中应该只存在一个实例,所有对于这个小鸟的操作(向上飞、向下掉等)都应该只会针对唯一的一个实例进行。

参考资料

  刘伟,《设计模式的艺术—软件开发人员内功修炼之道》

  何海涛,《剑指Offer—名企面试官精讲典型编程题》(题目1-实现Singleton模式)

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

设计模式的征途—1.单例(Singleton)模式相关推荐

  1. 设计模式C++描述----01.单例(Singleton)模式

    一.概念 单例模式:其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享. class CSingleton { //公有的静态方法,来获取该实例 public: s ...

  2. 设计模式学习笔记——单例(Singleton)模式

    设计模式学习笔记--单例(Singleton)模式 @(设计模式)[设计模式, 单例模式, Singleton, 懒汉式, 饿汉式] 设计模式学习笔记单例Singleton模式 基本介绍 单例案例 类 ...

  3. Ruby设计模式透析之 —— 单例(Singleton)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8868758 此为Java设计模式透析的拷贝版,专门为Ruby爱好者提供的,不熟悉R ...

  4. 设计模式--单例(Singleton)模式

    模式意图 保证一个类只用一个实例,并且提供一个全局访问点 类图 应用场景 1.需要更严格地控制全局变量时,使用单例模式: 2.重量级的对象如线程池对象,数据库连接池对象,不需要多个实例的对象如工具类等 ...

  5. 设计一个线程安全的单例(Singleton)模式

    在设计单例模式的时候.尽管非常easy设计出符合单例模式原则的类类型,可是考虑到垃圾回收机制以及线程安全性.须要我们思考的很多其它.有些设计尽管能够勉强满足项目要求,可是在进行多线程设计的时候.不考虑 ...

  6. Java设计模式透析之 —— 单例(Singleton)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8860649 写软件的时候经常需要用到打印日志功能,可以帮助你调试和定位问题,项目上 ...

  7. 单例/单体模式(Singleton)

    单例/单体模式(Singleton) 首先,单例模式是对象的创建模式之一,此外还包括工厂模式.单例模式的三个特点: 1,该类只有一个实例 2,该类自行创建该实例(在该类内部创建自身的实例对象) 3,向 ...

  8. 【设计模式】-创造篇-单例

    单例定义 单例模式(Singleton)是一种非常简单且容易理解的设计模式.顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中.统一的访问接口,以使系统行为保持协调一致 ...

  9. 单例测试模式中【饿汉式】与【懒汉式】的区别

    package day25.thread;/** /*** @author Mr Chen* @create 2018-10-09 18:37* 单例测试模式:保证类在内存中只有一个对象*/ publ ...

最新文章

  1. js选择checkbox值,组织成key-value形式,传值到后台
  2. 【错误记录】创建密钥报错 ( Key was created with errors: Warning: JKS 密钥库使用专用格式。建议使用 “ keyto “ 迁移到行业标准格式 PKCS12 )
  3. Java生成CRC16数据校验码
  4. 互联网下半场的角逐,玩转轻资产的大数据服务 | 阿里云栖开发者沙龙大数据专场(北京站)干货集锦...
  5. filterreader_Java FilterReader markSupported()方法与示例
  6. 深度残差收缩网络:(五)实验验证
  7. Spring Boot项目优雅的全局异常处理方式(全网最新)
  8. 【已解决】ModuleNotFoundError: No module named ‘web’的解决办法:
  9. 第二十四章:页面导航(六) 1
  10. JavaScript用法(1)
  11. WEB2.0 XHTML代码规范
  12. Can‘t update 分支名 has no tracked branch
  13. rpm的安装与卸载,常用命令记载
  14. 如何在中国大陆三大运营商申请公网IPV4地址
  15. unable to resolve column. This inspection performs unresolved sql references check.
  16. PHP+实验室安全系统 毕业设计 -附源码191610
  17. shell的正向和反向
  18. Windows 7 SP1整合补丁
  19. 机房环境监控系统品牌-深圳计通
  20. 【DataHub】 现代数据栈的元数据平台--如何将数据血缘关系写入DataHub

热门文章

  1. Python练习:星号三角形 I
  2. 4095: 韩信点兵
  3. 【C语言】fgets函数返回值
  4. ROS入门笔记(五):ROS中运行rqt_plot的问题(kinetic)
  5. 潜龙号开启水下机器人_国内首个智能绞吸机器人开展水下取土作业
  6. cron表达式 每天0点10分和30分_揭开考研阅卷的内幕,注意这些多得20分!
  7. c++接口调用外部类_Java基础:Java语法糖4之内部类
  8. Notepad++ 配置 Markdown
  9. MATLAB基础学习笔记02:掌握MATLAB运算
  10. Java学习笔记2.4.3 选择结构 - 多分支结构