设计模式的征途—1.单例(Singleton)模式
单例模式属于创建型模式的一种,创建型模式是一类最常用的设计模式,在软件开发中应用非常广泛。创建型模式将对象的创建和使用分离,在使用对象时无需关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。每一个创建型模式都在视图回答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;
- }
- // 添加一台Server
- public void AddServer(CustomServer server)
- {
- serverList.Add(server);
- }
- // 移除一台Server
- public 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 = ; i < ; 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模式)
设计模式的征途—1.单例(Singleton)模式的更多相关文章
- 设计模式C++描述----01.单例(Singleton)模式
一.概念 单例模式:其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享. class CSingleton { //公有的静态方法,来获取该实例 public: s ...
- JavaScript 设计模式之----单体(单例)模式
设计模式之--单体(单例)模式 1.介绍 从本章开始,我们会逐步介绍在JavaScript里使用的各种设计模式实现,在这里我不会过多地介绍模式本身的理论,而只会关注实现.OK,正式开始. 在传统开发工 ...
- 漫谈设计模式(二):单例(Singleton)模式
1.前言 实际业务中,大多业务类只需要一个对象就能完成所有工作,另外再创建其他对象就显得浪费内存空间了,例如web开发中的servlet,这时便要用到单例模式,就如其名一样,此模式使某个类只能生成唯一 ...
- Android与设计模式——单例(Singleton)模式
概念: java中单例模式是一种常见的设计模式.单例模式分三种:懒汉式单例.饿汉式单例.登记式单例三种. 单例模式有一下特点: 1.单例类仅仅能有一个实例. 2.单例类必须自己自己创建自己的唯一实例. ...
- JAVA中实现单例(Singleton)模式的八种方式
单例模式 单例模式,是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例.即一个类只有一个对象实例. 基本的实现思路 单 ...
- 【Java学习笔记之三十】详解Java单例(Singleton)模式
概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建 ...
- 设计一个线程安全的单例(Singleton)模式
在设计单例模式的时候.尽管非常easy设计出符合单例模式原则的类类型,可是考虑到垃圾回收机制以及线程安全性.须要我们思考的很多其它.有些设计尽管能够勉强满足项目要求,可是在进行多线程设计的时候.不考虑 ...
- 单例Singleton模式的两种实现方法
在设计模式中,有一种叫Singleton模式的,用它可以实现一次只运行一个实例.就是说在程序运行期间,某个类只能有一个实例在运行.这种模式用途比较广泛,会经常用到,下面是Singleton模式的两种实 ...
- Java设计模式透析之 —— 单例(Singleton)
写软件的时候经常需要用到打印日志功能,可以帮助你调试和定位问题,项目上线后还可以帮助你分析数据.但是Java原生带有的System.out.println()方法却很少在真正的项目开发中使用,甚至像f ...
随机推荐
- 初识 BFC、 IFC、GFC、FFC
首先本文中介绍的 BFC. IFC.GFC.FFC 均为 CSS 中常见问题的解读,如没兴趣,可以绕道了. 然后在介绍这么多的 *FC 之前,我们得了解 一下 Box 和 Formatting Con ...
- WebAppBuilder自定义主题
WebAppBuilder自定义主题 by 李远祥 基本步骤: 创建新主题的文件夹 注册新的主题到manifest.json 文件 覆盖HeaderController 部件的颜色. 覆盖panel的 ...
- 最新升级的火狐38.0.6识别ajax调用返回的""空值可能有异常。
自已在调用一段ajax开发中,返回的是空值 string result = string.Empty;return result; 但在页面进行$.ajax调用 时 输出alert(result);应 ...
- supervisor踩坑记录
线上一直以来都在用supervisor管理各项服务,感觉非常舒心,supervisor管理`gunicorn`和`celery`进程,web服务和异步任务各司其职,跑起来一直很稳定. 前段时间却不小心 ...
- 新注册第一帖----------------------乱码新手自学.net 之Linq 入门篇
作为一个业余开发,断断续续学.net/c#也有不少日子了, 学习过程中,不断忘了学,学了忘,这让我很苦恼. 以前学习过程中,我总是在笔记本中记录下来知识要点,这么久下来,笔记本都写了四五本了. 然而, ...
- MegCup 2017 极客挑战赛 初赛试题
看着像八卦,数数不是八卦,是29卦 每卦又有29个小弧 所以是29×29个bit 这29×29个bit怎么理解呢?并且从哪一卦开始到哪一卦结束?是先环向层层向里走还是先径向逐卦走? 我想不出来. 我猜 ...
- could not resolve host: github.com 问题解决办法
向github提交代码时出现问题,如图: 代码push失败,提示could not resolve host: github.com 解决办法: 1.打开终端,输入:ping github ...
- Android Monkey压力测试介绍
monkey:通过Monkey程序模拟用户触摸屏幕.滑动Trackball. 按键等操作来对设备上的程序进行压力测试,检测程序多久的时间会发生异常. Monkey的构架 Monkey的参数 Monke ...
- 不用搭环境的10分钟AngularJS指令简易入门01(含例子)
不用搭环境的10分钟AngularJS指令简易入门01(含例子) `#不用搭环境系列AngularJS教程01,前端新手也可以轻松入坑~阅读本文大概需要10分钟~` AngularJS的指令是一大特色 ...
- mysql 添加登录用户
一, 创建用户: 命令:CREATE USER 'username'@'host' IDENTIFIED BY 'password'; 说明:username - 你将创建的用户名, host - 指 ...