定义
单例模式:保证一个类仅有一个实例,并提供一个访问他的全局访问点。
单例模式分为:
①懒汉模式:在第一次调用时实例化
②饿汉模式:在创建类的时候实例化
优点:在内存中只有一个实例,减少内存开销、全局统一的访问点可以严格控制对象的访问
缺点:没有接口,扩展困难
应用场景:数据库连接池的设计
实现步骤:
①构造函数私有化
②提供一个全局的静态方法(全局访问点)
③在类中定义一个静态指针,指向本类唯一的实例对象
懒汉模式单例(不保证线程安全)
c++代码
#include <iostream>
using namespace std;
class SingleLenton
{
public:
static SingleLenton* getInstance()
{
//对外接口中判断指向本类唯一实例的指针是否为空
if (!instance)
{
//为空说明该类还没有实例化对象
cout << "实例化对象, 即将调用私有的构造函数。\n";
instance = new SingleLenton();
}
//不为空说明该类已存在实例化对象,返回其指针即可
return instance;
}
private:
//构造函数私有化
SingleLenton(); //声明未定义-未实现,也可内联实现SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
//提供一个静态指针,指向本类唯一实例。
//问题:为什么要静态的
static SingleLenton* instance;
};
//私有构造函数实现
SingleLenton::SingleLenton()
{
cout << "只会被调用一次的私有构造函数\n";
}
//私有静态成员初始化
SingleLenton* SingleLenton::instance = nullptr;
int main()
{
//懒汉单例
//调用静态方法:①可通过实例对象调用 ②也可用 类名::函数名 调用
SingleLenton* pOne = SingleLenton::getInstance(); //第一次调用私有构造
cout << "测试:私有构造在哪被调用\n";
SingleLenton* pTwo = SingleLenton::getInstance(); //没有调用私有构造
if (pOne == pTwo)
{
//判断两指针是否指向同一对象
cout << "指向同一实例对象,单例实现成功!\n";
}
getchar();
return 0;
}
问题1:为什么instance 和 getInstance() 要声明为static 的?
补充:用static修饰的成员称为“静态成员”。这种数据成员的生存期是大于类的实例对象的。和普通数据成员对比:静态数据成员是每个类的
有一份(即属于类),普通数据成员是每个对象有一份,因此静态成员变量也叫做类变量,普通成员变量叫做实例变量。用static修饰的函数
叫做静态成员函数,静态成员函数属于类,不属于某一个实例对象(生存期大于实例对象)。
根据上面单例的实现过程,可通俗理解为:你不能用该类在其他地方创建对象,而是通过该类自身提供的方法访问类中的那个自定义对象。
那么问题的关键来了,程序调用类中方法只有两种方式,①创建类的一个对象,用该对象去调用类中方法;②使用类名直接调用类中方法,格式“类名::方法名()”;
构造函数私有化后第一种情况就不能用(通过类自身提供的方法得到示例对象,但又需要实例对象调用方法。矛盾了),只能使用第二种方法。
而使用类名直接调用类中方法,类中方法必须是静态的,而静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的。
这就是单例模式唯一实例必须设置为静态的原因。
问题2:为什么懒汉模式的单例不保证线程安全?
假设存在这样一种并发情况:
有两个线程A 和 B,创建时均已SingleLenton::getInstance() 函数作为线程函数,假设第一个线程A 首先进入SingleLenton::getInstance() 函数,此时对线程A 来说static SingleLenton* instance = nullptr,即还没有实例化对象,于是进入if 语句块,不幸的是这时线程A 的cpu调度时间到了,还没有生成实例对象时(即还没执行new 语句)。
第二个线程B 开始运行,进入线程函数SingleLenton::getInstance(),发现此时instance也是空的(线程A 还没有执行new 语句),幸运的是线程B 的cpu调度时间未到,于是继续执行,进入if 语句块生成一个实例对象。之后线程B 的cpu时间调用到了。
再次回到线程A 上次结束的语句(进入了if 语句块,有可能是cout语句),于是线程A 有生成了一个实例对象。
至此,类SingleLenton存在两个实例对象,这违背了单例的定义,因此说懒汉模式的单例不保证线程安全。懒汉模式的单例不保证线程安全 可以理解成 【任何可能违背设计者意图的方式,都是“不安全”的。】
测试结果
实现线程安全的懒汉模式单例
如何实现
doublecheck(二次检查)+ 锁。
c++代码
#include <iostream>
#include <mutex>
using namespace std;
mutex mt;
class SingleLenton
{
public:
static SingleLenton* getInstance()
{
if (!instance) //第一次检查
{
//为空再加锁,通过lock_guard 类管理互斥量
lock_guard<mutex> lg(mt);
if (!instance) //二次检查
{
cout << "实例化对象, 即将调用私有的构造函数。\n";
instance = new SingleLenton();
}
}
return instance;
}
private:
SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
//删除拷贝构造
SingleLenton(const SingleLenton& s) = delete;
//删除=运算符重载
SingleLenton& operator= (const SingleLenton& s) = delete;
//静态成员变量一定要初始化
static SingleLenton* instance;
};
SingleLenton* SingleLenton::instance = nullptr;
int main()
{
//懒汉单例
//调用静态方法:①可通过实例对象调用 ②也可用 类名::函数名 调用
SingleLenton* pOne = SingleLenton::getInstance(); //第一次调用私有构造
cout << "测试:私有构造在哪被调用\n";
SingleLenton* pTwo = SingleLenton::getInstance(); //没有调用私有构造
if (pOne == pTwo)
{
//判断两指针是否指向同一对象
cout << "指向同一实例对象,单例实现成功!\n";
}
getchar();
return 0;
}
为什么安全?
假设还是上面那个情景,线程A 进入第一个if 语句块但未执行到加锁语句,此时线程A 的时间调度完了。
线程B 开始运行,也进入第一个if 语句块,因为线程A 未加锁,所以这里线程B 上锁并准备第二次检查,此时线程B 的时间调度完了。
回到线程A ,线程A (进入第一个if 语句块了)准备加锁,发现已上锁,于是等待解锁,因为线程B 中的锁一直未释放,所以线程A 这次的时间调度会一直等待。
第二次进入线程B ,已上锁且线程A 未生成实例对象,所以线程B 顺利通过第二次检查并成功生成了实例对象,生成完毕后解锁互斥量。
再次回到线程A 中,顺利上锁,但第二次检查时发现instance已不为空(说明实例对象已存在),于是线程A 会解锁互斥量并退出if 语句块,然后返回已生成(线程B 中生成)的实例对象指针。
测试结果
饿汉模式单例
保证线程安全:即任何情况下只有一个实例存在
和懒汉的区别主要在于实例对象创建的时机。饿汉在创建类时就创建了实例化对象
#include <iostream>
using namespace std;
class SingleLenton
{
public:
static SingleLenton* getInstance()
{
cout << "实例对象已存在,无需再次创建\n";
return instance;
}
private:
//构造函数私有化
SingleLenton() { cout << "只会被调用一次的私有构造函数\n"; }
//删除拷贝构造
SingleLenton(const SingleLenton& s) = delete;
//删除=运算符重载
SingleLenton& operator= (const SingleLenton& s) = delete;
//一个静态的指针
static SingleLenton* instance; //创建时就生成实例对象
};
//初始化静态指针,即生成实例对象
SingleLenton* SingleLenton::instance = new SingleLenton();
int main()
{
//懒汉单例
//调用静态方法:①可通过实例对象调用 ②也可用 类名::函数名 调用
SingleLenton* pOne = SingleLenton::getInstance(); //第一次调用私有构造
cout << "测试:私有构造在哪被调用\n";
SingleLenton* pTwo = SingleLenton::getInstance(); //没有调用私有构造
if (pOne == pTwo)
{
//判断两指针是否指向同一对象
cout << "指向同一实例对象,单例实现成功!\n";
}
getchar();
return 0;
}