设计模式的征途—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 = 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.Instance,那么就不会触发.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

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

时间: 2024-10-18 05:47:41

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

设计模式之----单体(单例)模式

设计模式之--单体(单例)模式 1.介绍 从本章开始,我们会逐步介绍在JavaScript里使用的各种设计模式实现,在这里我不会过多地介绍模式本身的理论,而只会关注实现.OK,正式开始. 在传统开发工程师眼里,单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象.在JavaScript里,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象. 2. 简单单体与闭包单体 在JavaS

Android与设计模式——单例(Singleton)模式

概念: java中单例模式是一种常见的设计模式.单例模式分三种:懒汉式单例.饿汉式单例.登记式单例三种. 单例模式有一下特点: 1.单例类仅仅能有一个实例. 2.单例类必须自己自己创建自己的唯一实例. 3.单例类必须给全部其它对象提供这一实例. 单例模式确保某个类仅仅有一个实例.并且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机能够有若干个打印机,但仅仅能有一个Pri

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

在设计单例模式的时候,虽然很容易设计出符合单例模式原则的类类型,但是考虑到垃圾回收机制以及线程安全性,需要我们思考的更多.有些设计虽然可以勉强满足项目要求,但是在进行多线程设计的时候.不考虑线程安全性,必然会给我们的程序设计带来隐患.此处,我们不介绍什么是单例模式,也不介绍如何设计简单的设计模式,因为你完全可以在书上或者在博客中找到.此处我们的目的就是设计一个使用的单例模式类.单例模式需要注意与思考的问题: (1)如何仅能实例化一个对象? (2)怎么样设计垃圾回收机制? (3)如何确保线程安全性

如何写一个比较严谨的单例Singleton模式

iOS真正意义上的单例模式: 我们在程序中很多时候要保证一个类只有一个唯一的实例,一般情况下初始化对象是通过[[Class alloc]init]来完成的,alloc是分配内存空间,init是对象的初始化,分配对象内存空间的另一个方法是allocWithZone方法,实际操作中当调用alloc方法来给对象分配空间时,默认也调用了allocWithZone方法.如果我们在初始化对象的时候没有使用alloc方法,而是直接使用allocWithZone方法的话,就没法保证这个单例是真正意义上的单例了,

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

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8860649 写软件的时候经常需要用到打印日志功能,可以帮助你调试和定位问题,项目上线后还可以帮助你分析数据.但是Java原生带有的System.out.println()方法却很少在真正的项目开发中使用,甚至像findbugs等代码检查工具还会认为使用System.out.println()是一个bug. 为什么作为Java新手神器的System.out.println(),到了

《连载 | 物联网框架ServerSuperIO教程》- 8.单例通讯模式开发及注意事项

1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架ServerSuperIO教程>2.服务实例的配置参数说明 <连载 | 物联网框架ServerSuperIO教程>- 3.设备驱动介绍 <连载 | 物联网框架ServerSuperIO教程>-4.如开发一套设备驱动,同时支持串口和网络通讯. <连载 | 物联网框架ServerSupe

java设计模式(1)-------单例,工厂,值对象,装饰模式

      java设计模式(1) 先简单的介绍下设计模式吧:是一种解决问题的一种行之有效的思想:用于解决特定环境下.重复出现的特定问题的解决方案. 那为什么我们需要学习设计模式呢? 1.设计模式都是一些相对优秀的解决方案,很多问题都是典型的.有代表性的问题,学习设计模式,我们就不用自己从头来解决这些问题,相当于在巨人的肩膀上,复用这些方案即可,站的高看到远,就是要站在巨人的肩膀上把他们踩下去,嘿嘿. 2.设计模式已经成为专业人士的常用词汇,不懂不利于交流,能让你变得很牛逼哦. 3.能让你设计的

python实现单例工厂模式

class CarFactory: '''python实现单例工厂模式''' __obj = None __flg_init = True def __new__(cls, *args, **kwargs): if cls.__obj is None: cls.__obj = object.__new__(CarFactory) return cls.__obj def __init__(self): if CarFactory.__flg_init: print('工厂产生了') CarFac

菜鸟学设计模式系列笔记之单例设计模式(Singleton模式)

特殊的类: (1)类和它的实例间一般是一对多的关系.对大多数的类而言,都可以创建多个实例. 在需要这些实例时创建它们,在这些实例不再有用时删除它们.这些实例的来去伴随着内存的分配和归还. (2)但是有一些类,应该只有一个实例. 这个实例似乎应该在程序启动时被创建出来,且只有在程序结束时才被删除. Intent : 一个类仅有一个实例,自行实例化并向整个系统提供一个访问它的全局访问点 Motivation : 对于一些类来说,只有一个实例是很重要的 Singleton (1)定义一个Instanc