「技术笔记」Linux高并发编程之单例模式

[TOC]

单例模式作为最常用的设计模式之一,用来保证一个类仅有一个实例化对象,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

1、单例模式的实现思路

​ 单例模式单例模式的实现思路一般有如下两步:

(1)私有化类的构造函数,以防止外界创建单例类的对象;

(2)使用类的私有静态指针变量指向类的唯一实例,并用一个公有静态方法获取该实例。

​ 根据类的唯一实例初始化的时机,可以将单例模式的实现分为:懒汉模式饿汉模式

懒汉模式:非常懒,不用的时候不去初始化,只在第一次被调用时才进行初始化;

饿汉模式:迫不及待,即使还没有程序调用它,其已提前初始化等待被调用

2、单线程的单例模式

我们实现一个经典的懒汉模式的单例模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
if(_instance==nullptr)_instance=new Singleton();
return _instance;
};
};
// 全局变量类外声明
Singleton* Singleton::_instance=NULL;

我们实现一个经典的饿汉模式的单例模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){return _instance;};
};
// 全局变量类外声明,直接实例化
Singleton* Singleton::_instance=new Singleton;

可以看到,所谓的懒汉模式饿汉模式的区别非常容易理解。

3、多线程的单例模式

在多线程中,需要考虑共享资源的安全性,使用互斥锁

(1)经典的线程安全懒汉模式

单例模式有两种实现方法,分别是懒汉模式和饿汉模式。顾名思义,。

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

class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){
pthread_mutex_init(&lock, NULL);
cout<<"构造函数执行成功"<<endl;
};
private:
//设置一个私有的静态类指针,用来保存全局唯一的实例对象
static Singleton* _instance;//静态成员变量
static pthread_mutex_t lock;//静态锁,是由于静态函数只能访问静态成员
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
if (_instance==NULL){
pthread_mutex_lock(&lock);
if (_instance==NULL){
_instance = new Singleton();
}
pthread_mutex_unlock(&lock);
}
return _instance;
};
};
// 全局变量类外声明,直接实例化
Singleton* Singleton::_instance=NULL;
pthread_mutex_t Singleton::lock;

为什么要用双检测,只检测一次不行吗?

​ 如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

(2)局部静态变量之线程安全懒汉模式

​ 前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。它不用加锁是因为在C++11之后,编译器会保证局部静态变量的线程安全性,所以不需要程序员额外为局部静态变量设置线程安全性

1
2
3
4
5
6
7
8
9
10
11
class Singleton{
protected:
//构造函数被保护,不能被外界访问
Singleton(){cout<<"构造函数执行成功"<<endl;};
public:
//设置一个公有的静态成员函数,用来作为外界唯一的创建接口
static Singleton* Instance(){
static Singleton _instance;//静态成员变量
return &_instance;
};
};

类的访问权限

​ c++的访问权限(也叫访问级别):指类外和子类对类内成员的访问权限。c++成员的默认访问权限是private。

访问权限 类外(实例化对象) 类内成员 子类成员 友元函数 友元类
public 可访问 可访问 可访问 可访问 可访问
protected 不可访问 可访问 可访问 可访问 可访问
private 不可访问 可访问 不可访问 可访问 可访问

​ 在c++中,鼓励将所有数据声明为private,想要访问数据则通过单独定义public的成员函数来访问,public为公有的成员函数。

static关键字

​ static是c++的一个限定符,用来控制某个变量的存储方式和可见性。static可以修饰变量(全局变量、局部变量、类成员变量等),类成员函数、普通函数。所有的静态变量都存储在c++的全局数据区,在声明处被初始化,如果没有指明初始值就自动初始化为0,一旦初始化就直至程序结束才释放内存。使用场景如下:

静态全局变量:只能在本文件中访问,不能在其他文件中访问,即便是extern外部声明也不可以(外部可见性缩小)。
静态局部变量在首次执行时初始化,直至程序运行结束后才释放(生命周期延长)。
类的静态成员变量必须在类外声明,且类的多个对象共享同一个静态成员变量。
类的静态成员函数只能调用静态成员变量(函数内没有this指针)。

类的静态成员函数

1)静态成员函数只能调用类的静态成员变量或者静态成员函数。

2)静态成员函数在类外定义时,不能加static修饰,否则出错。

3)在类外可以通过对象名或者类名来调用类的静态成员函数。

4)非静态成员函数可以任意地访问静态成员函数和静态数据成员。

内联函数inline

​ (问题:inline关键字的作用是什么) inline是c++的一个限定符,用来显示声明一个函数为内联函数,类中定义的函数都默认为内联函数。

内联函数可以减少函数调用的开销,提高程序执行的效率。编译器处理内联函数时,不会单独进行函数调用,而是在编译期间直接将整个函数体的代码插入调用语句处,就像整个函数体在调用处被重写了一遍一样,这个过程发生在编译期间。显然使用内联函数会使最终可执行程序的体积增加,因为内联函数是以空间换取时间。

​ (什么时候需要使用inline关键字) 内联函数适用于小而简单、执行很快的函数,如果一个函数非常庞大或者需要消耗大量时间,那么将其声明为内联函数虽然节省了函数调用的时间,但是却让程序体积增加了更多,这样程序执行速度很可能反而会下降。现代c++编译器提供了内联函数的保护机制,一般程序员声明的内联函数只是给编译器的建议,具体编译器是否真的按照内联函数的方式处理可能由内部算法决定。