0%

设计模式-创建型-单例

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

意图

单例模式(Singleton)是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

结构

使用下三部分来实现:

  • 私有构造函数:私有构造函数保证了不能通过构造函数来创建对象实例
  • 一个私有静态变量:私有静态变量用于指向实例化的对象,全局唯一
  • 一个公有静态函数:只能通过公有静态函数返回唯一的私有静态变量

实现

主要分为懒汉式 和 饿汉式 两种实现方式。区别就是实例化唯一的对象的位置。而懒汉式又有两种实现,一个线程安全,一个线程不安全

  • 懒汉式:延迟实现,只有当调用公用静态函数,且静态指针为空的时候才实例化对象,存在线程不安全问题
    • 线程安全的修改
  • 饿汉式:当程序运行的时候就 初始化 私有静态指针,因此线程安全

C++ 代码实现

饿汉式 / 懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Singleton
{
protected:
Singleton(const std::string value): value_(value){
}
static Singleton* singleton_;
std::string value_;
public:
Singleton(Singleton &other) = delete;
void operator=(const Singleton &) = delete;
// 全局静态函数 用于实例化唯一的对象
static Singleton *GetInstance(const std::string& value){
if(singleton_==nullptr){
singleton_ = new Singleton(value);
}
return singleton_;
};
string value(){return value_;}
};
// 如果是饿汉模式,在初始化的时候就 用 new 分配内存,如果懒汉模式,初始静态指针设置为 null 只有调用静态函数的时候才new
// 由于 singleton_ 指针是 私有静态变量,因此只能在函数外部,全局初始化,而不能放在任何一个函数里 去修改(包括mian函数),会编译报错
Singleton* Singleton::singleton_= new Singleton("Init");


void ThreadFoo(){
// Following code emulates slow initialization.
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
Singleton* singleton = Singleton::GetInstance("FOO");
std::cout << singleton->value() << "\n";
}

void ThreadBar(){
// Following code emulates slow initialization.
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
Singleton* singleton = Singleton::GetInstance("BAR");
std::cout << singleton->value() << "\n";
}
// 当两个线程输出的 结果 不一样时,说明产生了线程不安全的问题
int main()
{
std::thread t1(ThreadFoo);
std::thread t2(ThreadBar);
t1.join();
t2.join();
return 0;
}

同过上述代码可以看出,懒汉式饿汉式的区别:

  • 饿汉式:在 main 函数的执行之前,就调用了 new 实例化了对象,因此是线程安全的;缺点是由于一开始就申请了空间,会有资源浪费的问题。
  • 懒汉式:只有在程序 调用 公有静态函数,需要获取对象时,才会判断该对象有没有实例化,没的话再实例化。优点是 延迟实例化带来的节约资源的好处。缺点是 这是线程不安全的。
    • 例如当两个线程同时 调用 GetInstance函数,并且都同时通过了 singleton_指针空的判断,就会实例化出两个对象,所以上述代码输出的现象就是,两个线程输出的value值不一样。

懒汉式-线程安全

为了解决上述 懒汉式 线程不安全的问题就是 对 GetInstance 函数加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次。但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 Singleton已经被实例化了(即 singleton_指针不为空)。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。可以使用 双重校验锁 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 简单的加锁
Singleton* Singleton::GetInstance(const std::string& value){
std::lock_guard<std::mutex> lock(mutex_);
if(singleton_==nullptr){
singleton_ = new Singleton(value);
}
return singleton_;
};
// 双重校验锁
Singleton* Singleton::GetInstance(const std::string& value){
if(singleton_!=nullptr)
return singleton_;
else{
std::lock_guard<std::mutex> lock(mutex_);
if(singleton_==nullptr)
singleton_ = new Singleton(value);
}
};

从上述代码实现可以看出,双重校验锁的原理是,只有当判断 singleton_为空,即还未实例化的时候,才需要加锁操作,因为这时候肯定是要进行对象实例化操作,即写操作。如果已经实例化了,直接返回地址即可,不用加锁读。但是加锁后又判断了一遍是否指针空,因为还是可能存在两个线程同时进入了第一个 指针判断 空的语句,由于有锁,一个线程实例化对象,并修改指针为非空,完毕后释放锁。此时如果另一个线程不再判断一次为空就会 再次 new 实例化,还是存在线程不安全的问题。

或者直接这么理解,双重校验就是在 原本的基础上 先判断指针是否为空,空的化直接返回指针,不必校验。而不为空,才加锁判断。

优点:双重校验锁,即是延迟实例化的,可以节省资源,还是线程安全的,且不会因为锁的存在使得线程阻塞时间过长。

volatile 关键字

singleton_ 采用 volatile 关键字修饰也是很有必要的,singleton_ = new Singleton(value) 这段代码其实是分为三步执行:

  1. Singleton分配内存空间
  2. 初始化 Singleton
  3. singleton_指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

C++ volatile关键字作用