简介
在实际开发中,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式的定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
实现过程
如下代码模拟了 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 个要点:
- 某个类只能有一个实例;
- 它必须自行创建这个实例;
- 它必须自行向整个系统提供这个实例;
其结构如图所示:
饿汉式与懒汉式
单例类通常由两种不同的实现方式:
- 饿汉式单例类
- 懒汉式单例类
饿汉式
饿汉式单例类的结构如图所示:
从图中观察可知,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,其代码如下:
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。
总结
优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统性能。
- 允许可变数目的实例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,即节省系统资源,又解决了由于单例对象共享过多有损性能的问题。
缺点
- 由于单例模式没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一起。
适用场景
- 系统只需要一个实例对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了公共访问点外,不能通过其他途径访问该实例。
所有代码见 Kohirus-Github
原文地址:http://www.cnblogs.com/tuilk/p/16810149.html