单例模式

为什么要使用单例?什么时候使用单例?

当可能出现线程不安全、资源访问冲突以及某些类对象数据只应保存一份时应使用单例

1.处理资源访问冲突

我们自定义实现了一个往文件中打印日志的Logger类:

class FileWriter {};
class Logger
{private:fstream file;
public:Logger() {file.open("/Users/test/log.txt",ios::in | ios::out);}void log(string message){file.write(message.c_str(),message.length());}
};
class UserController {public:UserController(Logger logger_):logger(logger_){}void log(string id, string password) {logger.log(id + "login");}
private:Logger logger;
};class Order {};
class UserController {public:UserController(Logger logger_):logger(logger_){}void log(Order order) {logger.log("log" + order.toString());}
private:Logger logger;
};

在上面的代码中,我们注意到,所有的日志都写入到同一个文件/Users/test/log.txt 中。在 UserControllerOrderController 中,我们分别创建两个 Logger 对象。在多线程环境下,如果两个线程同时分别执行 login()函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。

那如何来解决这个问题呢?我们最先想到的就是通过加锁的方式:给 log() 函数加互斥锁,同一时刻只允许一个线程调用执行 log()函数。

不过,你仔细想想,这真的能解决多线程写入日志时互相覆盖的问题吗?答案是否定的。这是因为,这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题。

那我们该怎么解决这个问题?我们需要把对象级别的锁,换成类级别的锁就可以了。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用log() 函数,而导致的日志覆盖问题。

除了使用类级别锁之外,实际上,解决资源竞争问题的办法还有很多,分布式锁是最常听到的一种解决方案。不过,实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情。除此之外,并发队列也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。

相对于这两种解决方案,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄。将 Logger 设计成一个单例类,程序中只允许创建一个 Logger 对象,所有的线程共享使用的这一个 Logger 对象,也就避免了多线程情况下写日志会互相覆盖的问题。

2.表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。

比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。以及,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

单例存在哪些问题?

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。
但是单例模式也会存在一些问题,比如:

  1. 单例对 OOP 特性的支持不友好
    单例对继承、多态特性的支持也不友好。因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

  2. 单例会隐藏类之间的依赖关系
    我们知道,代码的可读性非常重要。在阅读代码的时候,我们希望一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。
    通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

  3. 单例对代码的扩展性不友好
    我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?
    实际上,这样的需求并不少见。我们拿数据库连接池来举例解释一下。
    在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
    如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

  4. 单例对代码的可测试性不友好
    单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
    除此之外,如果单例类持有成员变量(比如 IdGenerator 中的 id 成员变量),那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,它的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。

  5. 单例不支持有参数的构造函数
    单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。针对这个问题,我们来看下都有哪些解决方案。
    第一种解决思路是:创建完实例之后,再调用 init() 函数传递参数。需要注意的是,我们在使用这个单例类的时候,要先调用 init() 方法,然后才能调用 getInstance() 方法,否则代码会抛出异常。具体的代码实现如下所示:

class Singleton
{private:Singleton(int paraA, int paraB) :paraA(paraA), paraB(paraB) {}~Singleton() = default;Singleton (const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:static std::unique_ptr<Singleton> instance;static std::once_flag onceFlag;int paraA;int paraB;
public:static std::unique_ptr<Singleton> getInstance(int paraA, int paraB){std::call_once(onceFlag, [&] {instance.reset(new Singleton(paraA, paraB)); });return instance;}static void init(int paraA, int paraB){if (instance != nullptr){perror("Singleton has been created!");return;}instance.reset(new Singleton(paraA, paraB));}
};
std::unique_ptr<Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;

第二种解决思路是:将参数放到 getIntance() 方法中。不过这种实现方法的问题是。如果我们两次执行getInstance() 方法,那获取到的singleton1和singleton2 相同,第二次的参数没有起作用,而构建的过程也没有给与提示,这样就会误导用户。

第三种解决思路是:将参数放到另外一个全局变量中。具体的代码实现如下。Config 是一个存储了 paramA 和 paramB 值的全局变量。里面的值既可以像下面的代码那样通过静态常量来定义,也可以从配置文件中加载得到。

class Config
{public:static const int paraA = 123;static const int paraB = 456;
};class Singleton
{private:Singleton(int paraA, int paraB) :paraA(Config::paraA), paraB(Config::paraB) {}~Singleton() = default;Singleton (const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:static std::unique_ptr<Singleton> instance;static std::once_flag onceFlag;int paraA;int paraB;
public:static std::unique_ptr<Singleton> getInstance(int paraA, int paraB){std::call_once(onceFlag, [&] {instance.reset(new Singleton(paraA, paraB)); });return instance;}static void init(int paraA, int paraB){if (instance != nullptr){perror("Singleton has been created!");return;}instance.reset(new Singleton(paraA, paraB));}
};
std::unique_ptr<Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;

有什么替代单例模式的方案?

可以使用静态方法以依赖注入的方式。
基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。

//老的使用方式
long function()
{   //...long id = idGenerator.getInstance().getId();//...
}
//新的方式:依赖注入
long function(IdGenerator idGenerator)
{long id = idGenerator.getId();return id;
}
//外部调用function()的时候,传入idGenerator

OODOOP-单例模式相关推荐

  1. Java单例模式个人总结(实例变量和类变量)

    Java单例模式 背景知识:Static关键字. 在对于定义类的变量,分为两种,是否具有static修饰的变量: 没有static修饰的变量,通过类的实例化(对象)引用,改变量称为实例变量: 使用st ...

  2. GOF23设计模式(创建型模式)单例模式

    目录: 一:单例模式的核心作用.常见应用场景 二:五种单例模式及其实现 三:关于反射和反序列化破解单例模式的漏洞,以及相应的解决方案 四:测试五种单例模式的效率 一:核心作用及常见应用场景: 核心作用 ...

  3. Java设计模式:单例模式

    学而时习,稳固而之心, 好久没有复习java的知识了,今天有空温习了单例模式,这里记录一下 单例模式是常见的设计模式的一种,其特点就是 指一个类只有一个实例,且该类能自行创建这个实例  , 保证一个类 ...

  4. [Python设计模式] 第21章 计划生育——单例模式

    github地址:https://github.com/cheesezh/python_design_patterns 单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式 ...

  5. Python 精选笔试面试习题—sorted 与 sort 单例模式、统计字符个数Count、垃圾回收、lambda函数、静态方法、类方法、实例方法、分布式锁、

    1. 字典根据键从小到大排序? In[38]: dic = {"name": "Tom", "age": 30, "country ...

  6. 单例模式的两种实现方式对比:DCL (double check idiom)双重检查 和 lazy initialization holder class(静态内部类)...

    首先这两种方式都是延迟初始化机制,就是当要用到的时候再去初始化. 但是Effective Java书中说过:除非绝对必要,否则就不要这么做. 1. DCL (double checked lockin ...

  7. 基础设计模式:单例模式+工厂模式+注册树模式

    单例模式: 通过提供自身共享实例的访问,单例设计模式用于限制特定对象只能被创建一次. 使用场景: 一般数据库实例都会用单例模式 实现: 单例设计模式就是要一个类只能实例化一个对象. 要想让一个类只能实 ...

  8. 设计模式——单例模式(Singleton)

    保证一个类仅有一个实例,并提供一个访问它的全局访问点.--DP UML类图 模式说明 个人认为单例模式是所有设计模式中最为简单的一个模式,因为实现这个模式仅需一个类,而不像其他模式需要若干个类.这个模 ...

  9. 设计模式 之美 -- 单例模式

    为什么要使用单例? 一个类只允许创建一个对象或者实例. 背景简介:使用多线程并发访问同一个类,为了保证类的线程安全,可以有两种方法: 将该类定义为单例模式,即该类仅允许创建一个实例 为该类的成员函数添 ...

  10. 【C++】C/C++ 中的单例模式

    目录 part 0:单例模式3种经典的实现方式 Meyer's Singleton Meyers Singleton版本二 Lazy Singleton Eager Singleton Testing ...

最新文章

  1. json格式 转换的时候 注意是否是类还是数组 微信json为null
  2. 通过100个单词掌握英语语法(十八)did
  3. #面试!,一定要注意,避免踩这些雷!!
  4. Android 开源框架选择
  5. net framework 3.5 安装错误_PageAdmin CMS建站系统报http403错误的解决方案
  6. ocx控件 postmessage消息会消失_APP控件之二——弹框
  7. Python修饰符--函数修饰符 “@”
  8. CenterLoss
  9. java写关于温度的算法_摄氏温度和华氏温度的转换之java算法
  10. H3C网络设备模拟器显示交换机的MAC地址表
  11. 如何写 peer review
  12. SSR是什么?Vue中怎么实现?
  13. 三种查看文件MD5 SHA*等校验值的方法
  14. 中国五金行业B2B电商峰会3月24日将于郑州召开
  15. Unity 图片拼接中间有空隙问题详解
  16. idea maven无法从私服下载jar和plugin
  17. 阿里云高校计划免费领取半年服务器流程
  18. Hive窗口函数详解
  19. 平台+应用,能否助力移动第三方势力崛起
  20. 工具介绍-UltraSearch图文一键检索

热门文章

  1. 小程序textarea顶层显示的bug
  2. pNA修饰肽:H-D-Leu-Thr-Arg-pNA,H-D-Leu-Thr-Arg-pNA,CAS号: 122630-72-2/3326-63-4
  3. SAP QM 源检验的检验批特殊之处
  4. 真服了!java程序员表白代码
  5. CorelDraw2019-pojie版下载
  6. 小米旗舰机2999元?雷军说很痛苦 1
  7. android无线充电器推荐,无线充电手机哪款好 2018值得买的无线充电手机推荐
  8. Windows 10 电脑在播放声音后突然增大的解决办法
  9. java 代码注释搞笑_笑哭丨谁的代码注释我都不服,就服你的!
  10. 杨澜经典语录:与思想交朋友。