首页 单例模式
文章
取消

单例模式

单例模式

优点:

只有一个实例所以不需要每次都创建和销毁,可以在启动的时候就创建对象然后永久驻留在内存中。

缺点:

有的实现方式是线程不安全的。

懒汉式:

懒汉式的意思是,只有类被实例化的时候才会创建这个单例实例。意思就是你不能在类里面放一个静态实例了,那样就是饿汉式了。普通的懒汉式是线程不安全的。

线程不安全版本:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <string>
#include <memory>
#include <queue>
#include <vector>
#include <functional>
#include <unordered_map>
#include <algorithm>
#include <numeric>
#include <unordered_map>
#include <map>
#include <set>
using namespace std;

class Student{
	private:
		Student() :age(99) {									//私有化构造
			cout << "lan_han construct!this age=" << age << endl;
		};

		~Student() {											//私有化析构
			cout << "lan_han destory!" << endl; 
		};

		Student(const Student&);								//私有化拷贝构造
		Student& operator=(const Student&);						//私有化拷贝赋值
		int age;
		static Student* myInstance;								// 这里是单例对象指针,不是一个实例化对象了。因为是懒汉式,只有用到的时候才会实例化。

	public:
		static Student* getInstance()							//静态函数,返回一个单例实例的指针。
		{
			if (myInstance == nullptr)							//!此处线程不安全。因为可能有多个线程同时到达这一行后时间片切换,其他线程可能已经实例化单例,切换回去后其余线程又会new出来实例。
			{
				myInstance = new Student();							
			}
			return myInstance;//返回对象指针
		}
		void printAge()
		{
			cout << this->age << endl;
		}
	private:
		// 定义一个内部类
		class CGarbo {
		public:
			CGarbo() {};
			// 定义一个内部类的静态对象
			static CGarbo m_garbo;													

			~CGarbo()//对象程序结束析构时对主类指针释放内存
			{
				if (myInstance != nullptr)
				{
					delete myInstance;
					myInstance = nullptr;
				}
			}
		};
};
//记得要初始化静态对象,里面只是定义了他,没有分配内存空间
Student* Student::myInstance = nullptr;						//为静态对象分配内存
Student::CGarbo Student::CGarbo::m_garbo;					//为静态对象分配内存,类的私有静态变量可以通过作用域访问运算符直接访问
int main()
{
	Student* stu_1 = Student::getInstance();
	stu_1->printAge();

	Student* stu_2 = Student::getInstance();
	stu_2->printAge();

	return 0;
}

代码步骤:

  • 私有化构造,析构,拷贝赋值,拷贝构造。
  • 类内放置一个静态的对象指针。
  • 类内放置一个静态的成员函数,用作接口来使用户调用,来实例化单例。
  • 类内放置一个辅助内部类用于回收单例实例资源。
    • 辅助类内部有一个静态辅助类实例。
    • 类外初始化该辅助类实例。
    • 由于单例的意义是,从程序开始到结束,只有一个实例而且一般不提前销毁。静态变量符合该需求。所以开始时初始化,结束时静态对象被析构。静态对象析构的同时判断单例实例是否已经被销毁,如果没有被销毁则去销毁单例实例。
  • 类外初始化单例的静态对象指针。
  • 注意,这个版本线程不安全。
    • 首先判断我们类外初始化的静态变量是否是nullptr,如果是的话证明该单例从未被实例化过,则可以new出实例然后返回指针。
    • 因为可能有多个线程同时到达这一行后时间片切换,其他线程可能已经实例化单例,切换回去后其余线程又会new出来实例。所以不安全。
    • 所以,我们直观地想到,加锁不就完了吗?对,但是不全对。

线程安全版本:

我们只需要修改一下获取实例的代码部分即可:

1
2
3
4
5
6
7
8
9
10
11
static Student* getInstance()							//静态函数,返回一个单例实例的指针。
{	
    my_mutex.lock();									//加锁
    if (myInstance == nullptr)							//此时线程安全。
    {
        myInstance = new Student();							
    }
    my_mutex.unlock();									//解锁
    return myInstance;//返回对象指针
}

但是我们发现一个问题,也就是我们使用实例也是通过getinstance来的。也就是我们如果每次想要新建一个指针来获取到指向实例的指针,都需要调用getInstance。然后每次都加锁释放锁是非常非常浪费资源的。

我们同时发现,出现线程不安全的时机几乎只是一开始创建的时候。所以我们发明了双检锁

线程安全版本,双检锁:

我们只需要修改获取示例代码的部分即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
static Student* getInstance()							//静态函数,返回一个单例实例的指针。
{	
    if (myInstance == nullptr)							//先判断是否为空
    {
        my_mutex.lock();								//确定空后再加锁
        if(myInstance == nullptr){						//再次判断是否为空
            myInstance = new Student();					//实例化
        }	
        my_mutex.unlock();								//解锁
    }

    return myInstance;//返回对象指针
}

我们分析一下这段代码。首先,我们外层的判断目的是为了在已经实例化后,直接返回对象指针。意思就是,我们已经实例化后,不需要加锁去检查是否已经实例化了。再次加锁就是浪费资源。所以如果已经实例化了,我们再次调用的时候发现非空就可以直接返回对象指针了。

然后内层的原因就是,我们会有多个线程想去实例化单例。这时候加锁,实例化,解锁。第二个等候锁的线程拿到锁后,发现已经为非空了,此时可以直接返回。所以线程安全。

但是,双检锁依旧可能失效,原因就是指令重排。

1
myInstance = new Student();	

这一行代码,背后是三件事:

  1. 为单例对象分配空间
  2. 调用单例对象的构造函数来构造对象。
  3. myInstance 指向分配的空间。

但是编译器并不保证这三者的顺序。2和3可能会被交换顺序。

所以有可能发生下面的事情:线程A先分配了空间,然后交换了2和3导致直接让指针指向了分配的空间,这时时间片到期!此时分配的空间并没有实际构造出单例的实例!然后线程B进来了!它直接判断了现在指针是否为空,非常可惜,此时指针不为空。所以线程B直接返回了一个指向空内存的指针。

解决这个的办法有很多,比如pthread_once,内存屏障(atomic + std::memory_order_acquire),volatile(非跨平台)等等。

但是这种内存屏障的代码会非常复杂,有没有什么其他的方式允许我们实现一个线程安全的单例模式呢?

饿汉式:

饿汉式的意思就是,一开始就有一个实例被创建出来,而不是等待类被实例化的时候才创建。意思就是你要在类里面放一个该类的静态实例。这样程序一旦启动就会有实例创建。线程安全

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
class Student
{
private:
    Student() :age(99) {                                            //私有化构造
        cout << "e_han construct! age=" << age << endl;             
    };

    ~Student() {                                                    //私有化析构
        cout << "e_han destory!" << endl; 
    };

    
    Student(const Student&);                                        //私有拷贝构造
    Student& operator=(const Student&);                             //私有拷贝赋值
    static Student myInstance;                                      //单例对象在这里!


    int age;

public:
    static Student* getInstance()
    {
        return &myInstance;                                         //返回单例实例的指针,此处是取地址
    }
    void printAge()
    {
        cout << this->age << endl;
    }
};

Student Student::myInstance;                                        //为静态对象分配内存
int main()
{
    Student* stu_1 = Student::getInstance();
    stu_1->printAge();
    //对象是全局一开始就建立好的,两个函数获取到的是同一个对象(地址相同)
    Student* stu_2 = Student::getInstance();
    stu_2->printAge();
    return 0;
}

这个就简单很多,因为不涉及到new所以也不涉及资源回收。而且是线程安全的。

但是潜在问题在于 no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例,比如一个全局变量的构造函数中调用了此方法。

Meyers Singleton (也是懒汉式的一种)

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
#include<iostream>
using namespace std;
class Student
{
private:
    Student() :age(99) { 									//私有构造
        cout << "Student construct! age=" << age << endl; 
    };
    ~Student() { 											//私有析构
        cout << "Student destory!" << endl; 
    };
    int age;
    Student(const Student&) = delete;								//禁用拷贝构造
    Student& operator=(const Student&) = delete;					//禁用拷贝赋值

public:
    static Student& getInstance()				//注意是返回单例实例对象的引用
    {   
        static Student myInstance;              //注意这里,这里是局部静态变量而不是类成员变量。局部静态变量的生存周期是从声明起至程序结束。 		
        return myInstance;					//返回实例对象
    }
    void printAge()
    {
        cout << this->age << endl;
    }
};
int main()
{
    Student& stu_1 = Student::getInstance();	//注意这里要用引用去接。
    stu_1.printAge();
    Student& stu_2 = Student::getInstance();
    stu_2.printAge();
    return 0;
}
  • 注意。这里私有析构会造成如果实例化对象,编译时会提示对象无法析构
  • 这里禁用拷贝构造。这样的话调用拷贝构造的时候编译器会提示错误。
    • 如果提供了拷贝构造的定义,就算是私有,也可以调用成功。因为调用的函数是类的成员函数,尽管是静态。

在c++11后,当变量在初始化的时候,如果多线程并发同时进入声明语句,并发线程将会阻塞等待初始化结束。

  • 具体原因可以参考这里。https://stackoverflow.com/questions/17712001/how-is-meyers-implementation-of-a-singleton-actually-a-singleton

类内的函数内的变量是局部变量,不算做类的成员,也不占用类的大小。静态也是这样。而且局部静态变量的生存周期是从声明(但是必须程序执行到该对象的声明处)起至程序结束。在这里就是从函数调用开始到程序结束。所以正是利用了这个特性既保证了线程安全,又是懒汉式,又是全局生存周期。

如果针对这种单例模式,我们返回对象会发生什么?也就是局部静态变量的拷贝

静态局部对象会正常拷贝。

假设我们把单例模式的所有函数都设置为public。

1
2
3
4
5
static Student getInstance()				
{   
    static Student myInstance;  //除第一次外,其余调用编译器会忽略
    return myInstance;	//正常拷贝。调用拷贝构造    		
}

我们知道,局部静态变量只允许也只会被初始化一次。如果发现已经被初始化了则会跳过。所以static Student myInstance;只有在第一次经过的时候会初始化。剩下的调用中会忽略这一行。但是return myInstance; 会正常拷贝。

1
Student stu_1 = Student::getInstance();

如果我们用值去接。则这个过程会拷贝构造两次。有编译器优化就是一次。

第一次是作为临时对象拷贝出函数。第二次是临时对象拷贝至外部对象。然后临时对象销毁。

其他实现方式:

https://www.cnblogs.com/liyuan989/p/4264889.html

类的私有静态变量可以通过作用域访问运算符直接访问

main函数结束也会为栈对象调用析构,但是不会为堆对象调用。

单例模式可以把构造函数设置为protected以允许子类继承

现在的 C++ Standard 已经强制要求局部静态对象在第一次被使用时才被构造出来。

局部静态变量只允许也只会被初始化一次。如果发现已经被初始化了则会跳过。

块作用域声明且带有 staticthread_local (C++11 起) 说明符的变量拥有静态或线程 (C++11 起)存储期,但在控制首次经过它的声明时才会被初始化(除非它被零初始化常量初始化,这可以在首次进入块前进行)。在其后所有的调用中,声明都会被跳过。

如果初始化抛出异常,那么不认为变量被初始化,且控制下次经过该声明时将再次尝试初始化。

如果初始化递归地进入正在初始化的变量的块,那么行为未定义。

如果多个线程试图同时初始化同一静态局部变量,那么初始化严格发生一次(类似的行为也可对任意函数以 std::call_once 来达成)。 注意:此功能特性的通常实现均使用双检查锁定模式的变体,这使得对已初始化的局部静态变量检查的运行时开销减少为单次非原子的布尔比较。

注意,块作用域(block scope)不是类作用域(class scope)。只有块作用域声明且带有static的才叫局部静态变量。静态类成员变量不是局部静态变量。

静态成员变量的生存周期是从程序开始到程序结束。和全局变量,全局静态变量一致。而静态局部变量是在控制首次经过它的声明时才会被初始化,然后到程序结束为止。

本文由作者按照 CC BY 4.0 进行授权