目录
一、引言
假设有一个大型的网站,该网站的流量非常大,需要通过一个服务器集群来处理用户的请求。为了合理分配请求到不同的服务器,我们需要一个负载均衡器。这个负载均衡器必须是全局唯一的,所有请求都通过这个负载均衡器来分配,避免多个负载均衡器同时工作导致混乱。单例模式可以很好的适配这个场景。
二、单例模式概述
单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
Singleton Pattern:Ensue a class has only one instance,and provide a global point of access to it.
1)为了确保只有一个实例,构造函数必须私有,这样在类外部就创建不了对象了。同时,也要将拷贝构造函数和赋值重载函数禁掉。
2)在类内部声明一个类型为自身的静态私有成员变量。
3)在类内部定义一个公有的静态成员方法,用于获取唯一的实例。
光说理论很抽象,下面进入实战。
三、单例模式的结构与实现
3.1 单例模式结构
单例模式的结构很简单,看下面的实现就知道了。
3.2 实现单例模式
例子:假设有一个大型的网站,该网站的流量非常大,需要通过一个服务器集群来处理用户的请求。为了合理分配请求到不同的服务器,我们需要一个负载均衡器。
下面我们就用单例模式来设计。
//.hpp
#include <vector>
#include <string>
class LoadBalancer {
private:
LoadBalancer() {}//私有构造函数
LoadBalancer(const LoadBalancer&) = delete;//禁止拷贝构造函数
LoadBalancer& operator=(const LoadBalancer&) = delete;//禁止赋值操作符
public:
//公有静态方法,返回唯一的实例
static LoadBalancer* getLoadBalancer() {
if (_instance == nullptr) {
_instance = new LoadBalancer();
}
return _instance;
}
//添加服务器
void addServer(std::string server) {
_servers.push_back(server);
}
//选择服务器
std::string getServer() {
if (_servers.empty()) {
return "";
}
int index = rand() % _servers.size();
return _servers[index];
}
//删除服务器
void removeServer(std::string server) {
for (auto it = _servers.begin(); it != _servers.end(); it++) {
if (*it == server) {
_servers.erase(it);
break;
}
}
}
private:
static LoadBalancer* _instance;//在类内声明
std::vector<std::string> _servers;//服务器列表
};
LoadBalancer* LoadBalancer::_instance = nullptr;//在类外定义
//test.cpp
#include <iostream>
#include "singleton.hpp"
int main()
{
LoadBalancer* lb = LoadBalancer::getLoadBalancer();
LoadBalancer* lb2 = LoadBalancer::getLoadBalancer();
// 检查两个实例是否相同
if (lb == lb2) {
std::cout << "两个实例是相同的" << std::endl;
}
lb->addServer("server-1");
lb->addServer("server-2");
lb->addServer("server-3");
lb->addServer("server-4");
lb->addServer("server-5");
for (int i = 0; i < 10; i++) {
std::string server = lb->getServer();
std::cout <<"分发请求至服务器: " << server << std::endl;
}
return 0;
}
四、单例模式的两种实现方式
4.1 饿汉式单例模式
class Singleton {
private:
Singleton() {}//私有构造函数
Singleton(const Singleton&) = delete;//禁止拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
//公有静态方法,返回唯一的实例
static Singleton* getInstance() {
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = new Singleton();
当类被加载时,静态变量_instance就会被初始化,此时就会创建单例类的唯一实例。
4.2 懒汉式单例模式
class Singleton {
private:
Singleton() {}//私有构造函数
Singleton(const Singleton&) = delete;//禁止拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
//公有静态方法,返回唯一的实例
static Singleton* getInstance() {
if (_instance == nullptr) {
_instance = new Singleton();
}
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = nullptr;
懒汉式单例模式采用的是延迟加载,首次需要的时候才创建唯一实例。
懒汉模式存在一个很大的问题,就是在多线程并发的场景下,如果按上述的方式编写代码,可能会创建出多个实例。这就违背了单例模式的初衷了。
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class Singleton {
private:
Singleton() {}//私有构造函数
Singleton(const Singleton&) = delete;//禁止拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
//公有静态方法,返回唯一的实例
static Singleton* getInstance() {
pthread_mutex_lock(&mutex);//加锁
if (_instance == nullptr) {
//_instance 并不像饿汉式那样,在类加载的时候就创建实例,而是在第一次使用的时候创建实例
//所以在多线程环境下,_instance就是共享资源,能被多个线程同时判断为nullptr,导致创建多个实例
//所以要加锁,保证只有一个线程能创建实例
_instance = new Singleton();
}
pthread_mutex_unlock(&mutex);//解锁
return _instance;
}
private:
static Singleton* _instance;
};
Singleton* Singleton::_instance = nullptr;
上面的代码在访问临界区时进行加锁,确保了只会创建一个实例。但是这种做法并不高效,因为在每次调用get方法时都需要锁定判断,所以引入了双重 if 判断。
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class Singleton {
private:
Singleton() {}//私有构造函数
Singleton(const Singleton&) = delete;//禁止拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
//公有静态方法,返回唯一的实例
volatile static Singleton* getInstance() {
if (_instance == nullptr) {
pthread_mutex_lock(&mutex);//加锁
if (_instance == nullptr) {
_instance = new Singleton();
}
pthread_mutex_unlock(&mutex);//解锁
}
return _instance;
}
private:
volatile static Singleton* _instance;
};
volatile Singleton* Singleton::_instance = nullptr;
//关键字:volatile
//volatile 关键字的作用是告诉编译器,不要对该变量进行优化,
//每次都从内存中读取该变量的值,而不是从寄存器中读取
在创建了唯一实例后,多线程再调用get方法时,直接就会因为_instance不为空而直接返回,不用在去竞争锁,提高了效率。同时,在多线程环境下,一个线程可能会修改某个变量的值,而其他线程需要读取该变量的最新值,volatile关键字可以确保每次访问变量时都是从内存中读取最新值。
4.3 饿汉式与懒汉式的对比
饿汉式因为在类加载的时候就创建了唯一的实例,所以不存在线程安全问题,但是,一上来就创建实例,会导致加载速度变慢,另外,就算后续这个唯一的实例用不到也被创建出来了,一定程度上造成了资源的浪费。
懒汉式,存在线程安全问题,即使使用双重 if 判断,在一定程度上确实挽回了一点效率,但是由于在访问_instance变量时,每一次都要去内存中读取,效率是会比较低的。它的优点在于,采用的是延迟加载,需要的时候在创建出实例,后续用不到的话就不会创建,不会造成资源的浪费,同时在类加载的时候,不会拖累效率。
五、单例模式总结
5.1 单例模式的优点
1)全局唯一实例
- 单例模式确保一个类只有一个实例,并提供一个全局访问点。这在需要严格控制资源或状态时非常有用,例如数据库连接池、线程池等场景。
2)节省系统资源
- 由于单例模式避免了重复创建实例,减少了内存开销和系统性能消耗,特别适合频繁创建和销毁的对象。
3)避免状态冲突
- 单一实例可以避免多个实例导致的资源竞争或状态不一致问题,例如配置管理、日志记录等场景。
5.2 单例模式的缺点
1)难以扩展
- 没有抽象层,扩展有很大的困难。
2)耦合度高
- 单例类既提供了业务方法,有提供创建对象的方法,将对象的创建和对象本身的功能耦合在了一起。
5.3 单例模式的使用场景
当某个类只需要一个实例对象的时候,就是单例模式出场的时候。
六、结语
在20多种设计模式中,单例模式的结构是最简单的,它只有一个单例类。在软件设计的道路上,我们仍需掌握其他设计模式,“路漫漫其修远兮,吾将上下而求索。”
完~