首页 > 其他分享 >设计模式之单例模式

设计模式之单例模式

时间:2022-10-20 18:12:56浏览次数:77  
标签:模式 TaskManager instance 实例 线程 单例 设计模式 LazySignleton

简介

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

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

实现过程

如下代码模拟了 Windows 任务管理器:

class TaskManager {
public:
  TaskManager();
  void displayProcesses();
  void displayServices();
};

为了实现任务管理器的唯一性,通过以下 3 步对其进行重构:

(1) 由于每次实例化 TaskManager 对象时都会产生一个新的对象,为了确保唯一性,需要禁止类的外部直接使用 new 来创建对象,因此需要将其构造函数设为私有:

class TaskManager {
public:
  void displayProcesses();
  void displayServices();

private:
  TaskManager();
};

(2) 此时虽然类的外部不能再使用 new 来创建对象,但是在类内部还是可以创建对象的。因此,可以在 TaskManager 中创建并保存此唯一实例。为了让外界可以访问这个唯一实例,需要在 TaskManager 类中定义一个静态的 TaskManager 类型的私有成员变量:

class TaskManager {
public:
  void displayProcesses();
  void displayServices();

private:
  TaskManager();
  static TaskManager* tm;
};

TaskManager* TaskManager::tm = nullptr;

(3) 为了保证成员变量的封装性,将类中的 tm 对象设置为私有性,但是外界却无法访问该变量,为此需要增加一个共有的静态方法:

class TaskManager {
public:
  void displayProcesses();
  void displayServices();
  static TaskManager* getInstance() {
    if (tm == nullptr) {
      tm = new TaskManager();
    }
  }

private:
  TaskManager();
  static TaskManager* tm;
};

TaskManager* TaskManager::tm = nullptr;

需要注意 getInstance() 方法的修饰符,首先它是一个 public 方法,以便外界其他对象使用,其次它使用了 static 关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无需创建 TaskManager 对象。事实上,在类外也无法创建 TaskManager 对象,因为构造函数是私有的。

单例模式有 3 个要点:

  1. 某个类只能有一个实例;
  2. 它必须自行创建这个实例;
  3. 它必须自行向整个系统提供这个实例;

其结构如图所示:

饿汉式与懒汉式

单例类通常由两种不同的实现方式:

  • 饿汉式单例类
  • 懒汉式单例类

饿汉式

饿汉式单例类的结构如图所示:

从图中观察可知,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,其代码如下:

class EagerSingleton {
public:
  static EagerSingleton* getInstance() { return instance; }

private:
  EagerSingleton();
  static EagerSingleton* instance;
};

EagerSingleton* EagerSingleton::instance = new EagerSingleton();

当类被加载时,静态变量 instance 会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。

懒汉式与线程锁定

除了饿汉式单例,还有一种经典的懒汉式单例,其结构如图所示:

从图中可以看出,懒汉式单例在第一次调用 getInstance() 方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载技术,即在需要的时候再加载实例,为了避免多个线程同时调用,可以使用互斥锁来保证线程安全,代码如下:

class LazySignleton {
public:
  static LazySignleton* getInstance() {
    if (instance == nullptr) {
      mutex.lock();
      instance = new LazySignleton();
      mutex.unlock();
    }
    return instance;
  }

private:
  LazySignleton() {}
  static LazySignleton* instance;
  static pthread_mutex_t mutex;
};

LazySignleton* LazySignleton::instance = nullptr;
pthread_mutex_t LazySignleton::mutex = PTHREAD_MUTEX_INITIALIZER;

尽管在如上代码中使用互斥锁来保证线程安全,但是还是会存在单例对象不唯一的情况。

加入某一瞬间线程 A 和线程 B 都在调用 getInstance() 方法,此时 instance 对象为空,均能通过 instance == nullptr 的判断。此时线程 A 率先进入临界区,而线程 B 则阻塞等待互斥锁解锁。然后线程 A 创建单例对象,但是当线程 A 离开临界区时,线程 B 被唤醒,此时线程 B 并不知道单例对象已经创建了,于是继续创建新的实例,导致产生多个实例对象,违背了单例模式的设计思想,因此需要进一步改进,在临界区中再进行一次 instance == nullptr 判断,这种方式称为双重检查锁定。其代码如下:

class LazySignleton {
public:
  static LazySignleton* getInstance() {
    if (instance == nullptr) {
      mutex.lock();
      if (instance == nullptr) {
        instance = new LazySignleton();
      }
      mutex.unlock();
    }
    return instance;
  }

private:
  LazySignleton() {}
  static LazySignleton* instance;
  static pthread_mutex_t mutex;
};

LazySignleton* LazySignleton::instance = nullptr;
pthread_mutex_t LazySignleton::mutex = PTHREAD_MUTEX_INITIALIZER;

饿汉式与懒汉式比较

饿汉式单例类在类加载时就将自己实例化,它的优点在于无需考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应速度来看,由于单例对象一开始就的一创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很可能耗费大量时间,这意味着出现多线程同时首次引用此类的几率变得很大,需要通过双检锁等级制进行控制,这将导致系统性能收到一定影响。

更好的单例实现方法

饿汉式单例类不能实现延迟加载,不管将来用不用,它始终占据内存;懒汉式单例类线程安全控制繁琐,而且性能受影响。可见无论是饿汉式单例还是懒汉式单例都存在问题。

而有一种称为 Initalization on Demand Holder(IoDH) 的技术可以克服上述两种方式的缺点。它在实现时,需要在单例类中增加一个静态的局部类,在该局部类中创建单例对象,再将该单例对象通过 getInstance() 方法返回给外部使用:

class Singleton {
public:
  static Singleton* getInstance() {
    static Singleton instance;
    return &instance;
  }

private:
  Singleton() {}
};

通过该方式,既可以实现延迟加载,又可以保证线程安全,不影响系统性能。其缺点是与编程语言本身的特性有关,很多面向对象语言不支持 IoDH。

总结

优点

  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统性能。
  3. 允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,即节省系统资源,又解决了由于单例对象共享过多有损性能的问题。

缺点

  1. 由于单例模式没有抽象层,因此单例类的扩展有很大的困难。
  2. 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一起。

适用场景

  1. 系统只需要一个实例对象。
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了公共访问点外,不能通过其他途径访问该实例。

所有代码见 Kohirus-Github

标签:模式,TaskManager,instance,实例,线程,单例,设计模式,LazySignleton
From: https://www.cnblogs.com/tuilk/p/16810149.html

相关文章

  • Tutorial 6_原型模式
    向量的原型用C++完成数学中向量的封装,其中,用指针和动态申请支持向量长度的改变,使用浅克隆和深克隆复制向量类,比较这两种克隆方式的异同。浅克隆  深克隆  ......
  • Tutorial 7_单例模式
    学号的单一仿照课堂的身份证的例子,实现每个同学仅有一个学号这一问题。类图  代码  2.1StudentID.javapackagedanli;publicclassStudentID{......
  • Tutorial 8_适配器模式
    双向适配器实现一个双向适配器,使得猫可以学狗叫,狗可以学猫抓老鼠。  代码  ICat.javapackageshipeiqi;publicinterfaceICat{publicvoideat()......
  • Tutorial 9_桥接模式
    两个维度的桥接模式用桥接模式实现在路上开车这个问题,其中,车可以是car或bus,路可以是水泥路或沥青路。类图  代码Vehicle.javapackageqiaojie;publicinterfa......
  • Collections 类中设计模式的应用
    装饰器模式Collections类是一个集合容器的工具类,提供了很多静态方法,用来创建各种集合容器,比如通过unmodifiableColletion()静态方法,来创建UnmodifiableCollection类......
  • Calendar 类中设计模式的应用
    包名:java.util.Calendar工厂模式Calendar类提供了大量跟日期相关的功能代码,同时,又提供了一个getInstance()工厂方法,用来根据不同的TimeZone和Locale创建不同的Ca......
  • 享元模式
    实验13:享元模式本次实验属于模仿型实验,通过本次实验学生将掌握以下内容:1、理解享元模式的动机,掌握该模式的结构;2、能够利用享元模式解决实际问题。   ......
  • 9000字,唠唠架构中的设计模式
    1设计模式概述​ 软件设计模式(SoftwareDesignPattern),俗称设计模式,设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设......
  • MybatisPlus对租户模式的支持(一)
    前言最近接到一个任务,要将现有的用户系统改成租户模式。改造成租户模式最简单的方式就是为需要进行数据隔离的表加上租户id字段,然后前端调接口查询数据时,根据当前用户的租......
  • 设计模式之UML类图
    UML图示简介在UML中,类使用包含类名、属性和操作且带有分割线的长方形来表示,如图所示,定义一个Student类,它包含属性name、age和id,以及操作modifyInfo()。其对应的......