cpp单例模式

单例模式概念

定义

单例模式 Singleton:该类只能实例化一个对象的设计模式。

分类

  1. 懒汉模式:延时加载,在这个唯一的对象使用时才加载。

    优点

    1. 资源利用率高,使用时才创建,不存在浪费。
    2. 加载类速度快,不需要创捷对象。

    缺点

    1. 存在线程安全问题,需要引入同步机制(锁、信号量)。创建唯一对象不是原子操作,判空也不是原子操作。
    2. 运行获取速度慢,多线程同步带来额外开销。
  2. 饿汉模式:贪婪加载,在类加载时就生成唯一的对象。

    优点

    1. 线程安全的单例模式。因为在加载的时候就创建了,所以不会有竞争。
    2. 获取对象速度快,因为已经创建好了,只需要获取就可以。

    缺点

    1. 存在资源浪费的可能。因为可能只是加载这个类却不使用他。
    2. 类加载速度慢,因为需要创建对象。

设计思想

如何控制,达到只生成一个对象的目的。

  1. 对生成对象进行控制,将构造函数设计为私有。这样类外就不能调用构造函数,达到控制对象生成的目的。
  2. 对拷贝构造函数进行控制,将其设为私有并不实现,或者进行删除(C++11)。

提供对外接口,生成唯一对象。

这个接口需要判断是否只生成了一个对象。

  1. 这个函数的返回值需要返回一个对象,分三种情况。
    1. 返回对象类名,存在临时对象,会发生拷贝,不符合单例模式要求。
    2. 返回对象引用,没有拷贝发生。
    3. 返回对象的指针,没有拷贝发生。
  2. 修饰关键字设置,从调用角度考虑。
    1. 依赖对象来调用,obj.func() 来生成。但是此时对象未生成,所以不符合要求。
    2. 设计为静态函数,不依赖对象调用。通过类作用域来调用 ClassName::func() 来生成。
  3. 有一个标识,来标识唯一的对象。
    1. 为了防止这个接口被调用很多次,定义一个唯一标识对象的指针,这个指针不依赖于对象。所以还是定义为静态变量,类外初始化为 nullptr。
    2. 指针为空,那就说明对象还没有生成。如果不为空,就说明已经生成了。

懒汉模式代码

单线程版本

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
#include <iostream>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
int main() {
LazySingleton *obj1 = LazySingleton::GetInstance();
LazySingleton *obj2 = LazySingleton::GetInstance();
LazySingleton *obj3 = LazySingleton::GetInstance();
cout << "obj1:" << obj1 << endl;
cout << "obj2:" << obj2 << endl;
cout << "obj3:" << obj3 << endl;
return 0;
}

输出:

1
2
3
4
5
@└────> # ./a.out 
create singleton object:0x1861eb0
obj1:0x1861eb0
obj2:0x1861eb0
obj3:0x1861eb0

这种懒汉模式的单例,在单线程下是没问题的,但是多线程下有竞争就会出现问题。比如两个线程都进行完了 instance_ 的判断,并且都为空,那么他们都会调用那个构造函数。

多线程版本

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
m_.lock();
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
m_.unlock();
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static mutex m_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
mutex LazySingleton::m_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果:

1
2
3
4
5
@└────> # ./a.out 
thread id:139862718146304 instance pointer:create singleton object:0x7f344c000f70
0x7f344c000f70
thread id:139862709753600 instance pointer:0x7f344c000f70
thread id:139862701360896 instance pointer:0x7f344c000f70

加锁,保证一个线程在对临界资源进行操作时不能被其他线程打断。用互斥锁即可,但是如果每次调用都需要进行加锁解锁,在第一个对象已经生成之后,在读的层面不需要进行加锁了。加锁解锁会导致效率低下。

可以把上面的加锁部分套在一个 if 判断里,这样下个线程进来的时候就不用试图加锁了。

改进版代码:

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
if (instance_ == nullptr) { // 套起来
m_.lock();
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
m_.unlock();
}
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static mutex m_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
mutex LazySingleton::m_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果和上面是一样的,这里就不展示了。

C++11 的多线程版本

在 C++11 中,引入了 std::call_once,可以更便捷的实现上述功能。如需使用,只需要 #include 即可,简单来说 std:call_once 的作用,确保函数或代码片段在多线程环境下,只需要执行一次,常用的场景如 Init() 操作或一些系统参数的获取等。

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
std::call_once(instnace_flag_, []() {
instance_ = new LazySingleton();
});
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static once_flag instnace_flag_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
once_flag LazySingleton::instnace_flag_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果:

1
2
3
4
5
@└────> # ./a.out 
thread id:140655525984000 instance pointer:create singleton object:0x7fece4000f70
0x7fece4000f70
thread id:140655517591296 instance pointer:0x7fece4000f70
thread id:140655509198592 instance pointer:0x7fece4000f70

饿汉模式代码

如果我们在线程开启前,已经将唯一的对象生成,就不会出现线程不安全的问题。即唯一的对象在main函数调用前已经生成,就不会产生线程安全问题。所以:

  1. 在未进入 main 函数时,就完成唯一对象的生成,那么就是在程序加载时已经完成,存放在 .data段 的数据有这样的特点,所以我们可以直接给静态成员变量生成对象。
  2. 接口只用返回生成好的对象指针即可。
    代码:
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
#include <iostream>
#include <thread>
using namespace std;

class HungerSingleton {
public:
static HungerSingleton *GetInstance() {
return instance_;
}
private:
HungerSingleton() {
cout << "create singleton object:" << this << endl;
}
HungerSingleton(const HungerSingleton& s) = delete;
static HungerSingleton *instance_;
};
HungerSingleton* HungerSingleton::instance_ = new HungerSingleton();

int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}
1
2
3
4
5
@└────> # ./a.out 
create singleton object:0x2343eb0
thread id:140504409663232 instance pointer:0x2343eb0
thread id:140504401270528 instance pointer:0x2343eb0
thread id:140504392877824 instance pointer:0x2343eb0