1 核心思想
单例模式确保一个类只有一个实例,并提供一个全局访问点。
单例模式产生的对象具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
具体运用场景如:
设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
2 单例模式的实现
2.1 饿汉式
饿汉式就是静态成员直接初始化,这样就保证了静态变量只被初始化一次,可以确保多个线程使用的是同一个单例对象。
缺点是在不使用单例的时候也需要进行初始化,内存占用较大。
class Singleton
{
private:
Singleton() { std::cout << "构造对象" << std::endl; };
~Singleton() { std::cout << "析构对象" << std::endl; };
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* m_pSig;
public:
static Singleton* GetInstance()
{
return m_pSig;
}
};
Singleton* Singleton::m_pSig = new Singleton;
int main()
{
Singleton* pSing1 = Singleton::GetInstance();
Singleton* pSing2 = Singleton::GetInstance();
std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
return 0;
}
2.2 有缺陷的懒汉式
懒汉式表示用到的时候才去实例化,也就是说只有调用GetInstance函数的时候才会产生对象,好处是如果没有使用就不会占用内存,缺点是必须仔细进行访问控制以应对多线程访问。
//有多线程访问隐患的懒汉式单例模式
class Singleton
{
private:
Singleton() { std::cout << "构造对象" << std::endl; };
~Singleton() { std::cout << "析构对象" << std::endl; };
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* m_pSig;
public:
static Singleton* GetInstance()
{
if(m_pSig == nullptr)
m_pSig = new Singleton;
return m_pSig;
}
static void Delete()
{
delete m_pSig;
m_pSig = nullptr;
}
};
Singleton* Singleton::m_pSig = nullptr;
int main()
{
Singleton* pSing1 = Singleton::GetInstance();
Singleton* pSing2 = Singleton::GetInstance();
std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
return 0;
}
在GetInstance函数中若m_pSig为空,则new一个新的对象出来,若不为空则直接返回该对象。在单线程访问的情况下,可以保证取得正确的结果。如下图所示:
但在多线程访问的情况下,有可能会出现创建了两个对象。比如第一个线程在GetInstance函数中已经执行了m_pig是否为空的if判断,此时需要进行new操作,但在操作之前,第二个线程开始执行GetInstance函数,因为此时m_pig尚为空,第二个线程进行了new的操作并进行了返回创建了第一个对象。等到第一个线程开始执行的时候,因为它会继续暂停之前的操作,所以直接new了一个新的对象并返回,也就是又创建了一个新的对象。这样就造成了两个线程分别创建了不同的对象,单例的设计也就无效了。如下面的代码所示:
static Singleton *pSing1,*pSing2;
void ThreadFunc1()
{
pSing1 = Singleton::GetInstance();
}
void ThreadFunc2()
{
pSing2 = Singleton::GetInstance();
}
int main()
{
int index = 0;
while (1)
{
//Singleton* pSing1 = Singleton::GetInstance();
//Singleton* pSing2 = Singleton::GetInstance();
std::thread* pThread1 = new std::thread(ThreadFunc1);
std::thread* pThread2 = new std::thread(ThreadFunc2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (pSing1 && pSing2)
{
if (pSing1 != pSing2)
break;
else
{
Singleton::Delete();
index++;
continue;
}
}
}
std::cout << index << std::endl;
std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
return 0;
}
2.3 双重检查解决懒汉模式的缺陷
//使用双重锁解决懒汉式单例模式的缺陷
class Singleton
{
private:
Singleton()
{
std::cout << "构造对象" << std::endl;
};
~Singleton()
{
std::cout << "析构对象" << std::endl;
};
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* m_pSig;
static std::mutex m_lock;
public:
static Singleton* GetInstance()
{
if (m_pSig == nullptr)
{
std::lock_guard<std::mutex> lk(m_lock);
if(m_pSig == nullptr)
m_pSig = new Singleton;
}
return m_pSig;
}
static void Delete()
{
delete m_pSig;
m_pSig = nullptr;
}
};
Singleton* Singleton::m_pSig = nullptr;
std::mutex Singleton::m_lock;
//下面是main函数
static Singleton *pSing1,*pSing2;
void ThreadFunc1()
{
pSing1 = Singleton::GetInstance();
}
void ThreadFunc2()
{
pSing2 = Singleton::GetInstance();
}
int main()
{
int index = 0;
while (1)
{
//Singleton* pSing1 = Singleton::GetInstance();
//Singleton* pSing2 = Singleton::GetInstance();
std::thread* pThread1 = new std::thread(ThreadFunc1);
std::thread* pThread2 = new std::thread(ThreadFunc2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (pSing1 && pSing2)
{
if (pSing1 != pSing2)
break;
else
{
Singleton::Delete();
index++;
continue;
}
}
}
std::cout << index << std::endl;
std::cout << "pSing1:" << "\t" << pSing1 << "\tpSing2:" << "\t" << pSing2 << std::endl;
return 0;
}
代码运行测试可以发现永远只生成一个对象实例。
核心的改变就在于GetInstance函数中使用了双重检查,即
if (m_pSig == nullptr)
{
std::lock_guard<std::mutex> lk(m_lock);
if(m_pSig == nullptr)
m_pSig = new Singleton;
}
第一次进行m_pSig是否为空的判断是为了避免每次调用GetInstance函数都加锁,这样会造成开销太大,加上m_pSig是否为空的判断就会只有第一次调用GetInstance函数的时候才加锁。加锁之后再次进行m_pSig是否为空的判断,若为空则实例化对象。
双重检查机制在一般情况下是安全的,但要求编译器是按照顺序执行代码的,但实际上这里可能有内存安全的bug:
m_pSig = new Singleton;
为了执行这句代码,机器需要做三样事儿:
1.singleton对象分配空间。
2.在分配的空间中构造对象
3.使m_pSig指向分配的空间
遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.
将上面三个步骤标记到代码中就是这样:
Singleton* Singleton::GetInstance()
{
if (m_pSig == nullptr) {
std::lock_guard<std::mutex> lk(m_lock);
if (m_pSig == nullptr)
{
m_pSig = // Step 3
operator new(sizeof(Singleton)); // Step 1
new (m_pSig) Singleton; // Step 2
}
}
return m_pSig;
}
线程A进入了GetInstance函数,并且执行了step1和step3,然后挂起。这时的状态是:m_pSig不为空,而m_pSig指向的内存区没有对象!线程B进入了GetInstance函数,发现m_pSig不为null,就直接return m_pSig了。
2.4 推荐单例模式实现方式:使用局部静态变量
//使用局部静态变量
class Singleton
{
private:
Singleton()
{
std::cout << "构造对象" << std::endl;
};
~Singleton()
{
std::cout << "析构对象" << std::endl;
};
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& GetInstance()
{
static Singleton value;
return value;
}
};
之所以可以这样做是因为:局部静态变量不仅只会初始化一次,而且还是线程安全的。
注意GetInstance函数现在返回的是引用。
这种单例被称为Meyers’ Singleton。这种方法很简洁,也很完美,但是注意:
- gcc 4.0之后的编译器支持这种写法。
- C++11及以后的版本(如C++14)的多线程下,正确。
- C++11之前不能这么写。
2.5 使用c++11的call_once
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
class Singleton
{
private:
Singleton()
{
std::cout << "构造对象" << std::endl;
};
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr<Singleton> m_pSig;
public:
static Singleton& GetInstance()
{
std::call_once(flag, []() {m_pSig.reset(new Singleton()); });
return *m_pSig;
}
};
std::unique_ptr<Singleton> Singleton::m_pSig;
void do_onceflag()
{
Singleton& s = Singleton::GetInstance();
std::cout << &s << std::endl;
}
int main()
{
std::thread t1(do_onceflag);
std::thread t2(do_onceflag);
t1.join();
t2.join();
return 0;
}
3 总结
单例模式思想简单,但写出完全无风险的代码还是需要认真分析的,在满足前提条件时推荐使用2.4的方式进行实现。
参考:
https://zhuanlan.zhihu.com/p/62014096
https://www.cnblogs.com/sunchaothu/p/10389842.html