单例模式的实现步骤
实现步骤
1.首先你得有个类
2.将构造、拷贝构造、赋值运算符私有化(如果用不到可以将拷贝构造和赋值运算符只声明不定义)
3.提供唯一的类实例对象。
4.对外提供获取实例唯一接口。
实现样例
browser.h
class Browser
{
public:
static Browser *getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
static Browser *m_browser;
};
browser.cpp
Browser *Browser::m_browser = new Browser;
Browser *Browser::getInstance()
{
return m_browser;
}
"唯一实例"的几种实现方式
上述例子中通过全局静态指针,在类外初始化唯一实例。除此之外还有其他几种方式:
全局静态指针,在类内初始化
browser.h
class Browser
{
public:
static Browser *getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
static Browser *m_browser;
};
browser.cpp
Browser *Browser::m_browser = nullptr;
Browser *Browser::getInstance()
{
if(m_browser == nullptr)
m_browser = new Browser();
return m_browser;
}
全局静态变量
browser.h
class Browser
{
public:
static Browser *getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
static Browser m_browser;
};
browser.cpp
Browser *Browser::getInstance()
{
return &m_browser;
}
局部静态指针
browser.h
class Browser
{
public:
static Browser *getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
};
browser.cpp
Browser *Browser::getInstance()
{
Browser *browser = new Browser();
return browser;
}
局部静态变量
browser.h
class Browser
{
public:
static Browser *getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
};
browser.cpp
Browser *Browser::getInstance()
{
Browser browser;
return &browser;
}
局部静态引用
browser.h
class Browser
{
public:
static Browser &getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
};
browser.cpp
Browser &Browser::getInstance()
{
Brower browser;
return browser;
}
上述几种方式中有采用指针的,有采用变量的也有采用引用的。有关线程安全、效率的问题后面会提到。这里先谈一个需求:如果想要在不结束程序的情况下,释放单例对象。(暴捶产品~)
这种情况下需要主义的几点:
- 首先避免在单例的析构中释放对象。因为会无线循环下去。。。
- 其次,对于局部静态变量和全局静态变量,是采用取地址方式返回指针,所以不可以在外部直接释放这个指针(因为delete的不是new分配的),
- 而静态引用,无法释放。
- 对于局部静态指针,可能想从外部获取到指针,然后释放。但是问题来了,内存确实可以释放,但是指针对象无法处理(因为外部指针对象和内部指针对象虽然指向同一个内存,但是不是一个指针,所以即使在外部释放这个内存,并将指针指向nullptr,也不会改变单例中的那个指针),所以在下次调用getInstance时,browser非空,被返回,导致指针指向以释放的内存。
一系列排除后只剩下全局指针了。同样还是不能够在析构中释放,我们需要提供一个释放的接口,由调用者主动释放。
void Browser::destroyInstance()
{
if (m_browser) {
delete m_browser;
m_browser = nullptr;
}
}
懒汉or饿汉
顾名思义,懒汉式表示很懒,用的时候初始化,不用不初始化;饿汉式表示很饿,很急,所以上来就初始化。上述几种唯一实例中除了全局静态指针,类外初始化和全局静态变量为饿汉式,其他均为懒汉式。
区别
初始化时间:对于比较费时的单例对象,饿汉式会导致程序启动慢,但是后续运行不会卡顿,而懒汉式则启动快,后续初始化单例时会费时。
内存:饿汉式不论是否使用都会初始化,占用内存,而懒汉式不用不初始化。
线程安全问题:饿汉式因为初始化都在main函数之前,此时线程只有一个,不存在线程竞速问题。而懒汉式则存在线程竞速的问题。
non-local static的初始化顺序问题:由于C++对于non-local static变量的初始化顺序并不保证,所以当实例A调用实例B,而实例B初始化在A之后,则会出现问题。
主动释放:采用全局静态指针,两者都可以主动释放。
懒汉式的线程安全
前面提到懒汉式是非线程安全的。具体原因是
Browser *Browser::getInstance()
{
if(m_browser == nullptr)
m_browser = new Browser();
return m_browser;
}
对于上面全局静态指针的懒汉式,在线程A运行到
m_browser = new Browser();
时(未执行),跳转到线程B并执行了
if(m_browser == nullptr)
这样就会导致多份m_browser的情况了。
检查锁
QMutex mutex;
Browser *Browser::getInstance()
{
mutex.lock();
if(m_browser == nullptr){
m_browser = new Browser();
}
mutex.unlock();
return m_browser;
}
采用单锁的方式,确保同一时间只能有一个线程能够进入判断。但是每次getInstance都需要加锁,非常费时。
双检查锁
QMutex mutex;
Browser *Browser::getInstance()
{
if(m_browser == nullptr){
mutex.lock();
if(m_browser == nullptr){
m_browser = new Browser();
}
mutex.unlock();
}
return m_browser;
}
通过上述两层检查可以解决单锁的效率问题。但是。。。还有问题
双检查锁的问题——非原子操作
直到
m_browser = new Browser();
之前,一切都是美好的,但是因为这一句并非原子操作,导致双检查锁并不安全。这条语句可以分为
- new分配内存
- 调用Browser()构造初始化内存
- 将m_browser指向初始化后的内存
由于第二句依赖于第一句,所以两者顺序并不会改变,但是第二句和第三句可能由于编译器优化,导致顺序颠倒,这样线程A在此处将m_browser指向一片内存(未初始化),而线程B来到getInstance中,判断m_browser非空,很开心返回这个指针到外部使用,然后崩了。。。
volatile关键字
为了避免编译器优化操作顺序,C++11提供了volatile关键字,通过volatile修饰的关键字结合双检查锁可以实现线程安全的懒汉式单例。
但是其使用实在不够友好:
browser.h
class Browser
{
Q_OBJECT
public:
static volatile Browser *getInstance();
static void destroyInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
static Browser *volatile m_browser;
static QMutex m_mutex;
};
browser.cpp
Browser *volatile Browser::m_browser = nullptr;
QMutex Browser::m_mutex;
volatile Browser *Browser::getInstance()
{
if (m_browser == nullptr) {
QMutexLocker lock(&m_mutex);
if (m_browser == nullptr) {
m_browser = new Browser();
}
}
return m_browser;
}
内存栅栏
内存栅栏确保命令执行的相对顺序,却不决定执行时序,其之后的内存访问操作一定在其之前的内存访问操作之后执行。所以我峨嵋你
Browser *volatile Browser::m_browser = nullptr;
QMutex Browser::m_mutex;
volatile Browser *Browser::getInstance()
{
if (m_browser == nullptr) {
QMutexLocker lock(&m_mutex);
if (m_browser == nullptr) {
auto tmp = new Browser();
//内存栅栏操作,确保下面赋值时tmp肯定初始化好了
m_browser = tmp;
}
}
return m_browser;
}
具体的可以通过atomic_thread_fence和std::atomic实现。
call_once
这是C++11引入的比较简单的方式,需要用到一个全局标志位和call_once函数。
std::once_flag oc;
Browser *Browser::getInstance()
{
std::call_once(oc, [&]() {m_browser = new Browser; });
}
其原理为:全局标志oc如果为false,则进入call_once进行初始化,否则不允许进入。
其他
由于C++11确保局部静态变量的初始化是线程安全的,所以我们还可以采用Scoffmeyer式,也就是前面所说的局部静态引用方式。
局部静态引用
browser.h
class Browser
{
public:
static Browser &getInstance();
~Browser();
private:
Browser();
Browser(const Browser &);
Browser &operator=(const Browser &);
};
browser.cpp
Browser &Browser::getInstance()
{
Brower browser;
return browser;
}
总结
1.如果没有对内存的主动释放要求,可以采用Scoffmeyer式或call_once,比较简单
2.如果有要求则采用全局静态指针的懒汉式,并采用call_once初始化。