单例模式
单例模式介绍
单例模式指的是, 无论怎么获取, 永远只能得到该类类型的唯一实例对象.
一般遇到写进程池类、日志类、内存池的话就会用到 单例模式。
设计单例必须满足下面三个条件:
-
全局只有一个实例,用 static 特性实现,构造函数设为私有
-
通过公有接口获得实例.
-
线程安全.
// 饿汉单例模式
class CSingleton
{
public:
static CSingleton *getInstance()
{
return &single;
}
private:
static CSingleton single;
CSingleton()
{
cout << "CSingleton()" << endl;
}
~CSingleton()
{
cout << "~CSingleton()" << endl;
}
CSingleton(const CSingleton &); // 防止外部拷贝构造.
};
CSingleton CSingleton::single;
// 1.懒汉单例模式
class CSingleton
{
public:
static CSingleton *getInstance()
{
if (single == nullptr)
{
single = new CSingleton(); // 没有 delete.
}
return single;
}
private:
static CSingleton *single;
CSingleton() { cout << " CSingleton()" << endl; }
~CSingleton() { cout << " ~CSingleton()" << endl; }
CSingleton(const CSingleton &);
};
CSingleton *CSingleton::single = nullptr;
// 2. 资源没释放,存在内存泄漏问题.. 加个嵌套类.
class CSingleton
{
public:
static CSingleton *getInstance()
{
if (single == nullptr)
{
single = new CSingleton();
}
return single;
}
private:
static CSingleton *single;
CSingleton() { cout << " CSingleton()" << endl; }
~CSingleton() { cout << " ~CSingleton()" << endl; }
CSingleton(const CSingleton &);
// 定义一个嵌套类, 在该类的析构函数中, 自动释放外层类的资源.
class CRelease
{
public:
~CRelease()
{
delete single;
}
};
// 通过该静态对象在程序结束时自动析构的特点,来释放外层类的对象资源.
static CRelease release;
};
CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
上述代码存在线程安全问题,故需要设计线程安全的单例模式。
线程安全的单例模式
1、饿汉单例模式的线程安全特性:
饿汉单例模式中, 单例对象定义成了一个 static 静态对象,他是在程序启动时, main 函数运行之前就初始化好的, 因此不存在线程安全问题,可以在多线程运行。
2、懒汉单例模式的线程安全特性
// 懒汉单例模式获取单例对象方法如下:
static CSingleton* getInstance()
{
if(single == nullptr)
{
single = new CSingleton();
}
return single;
}
显然, getInstance 是个 不可重入函数, 在多线程中会出现竞态条件问题, single = new CSingleton() 会做三件事:
1.开辟内存 2.调用构造函数 3.给 single 指针赋值.
在多线程中, 可能出现如下可能:
- 线程 A 先调用 getInstance 函数,由于 single 为 nullptr, 进入 if 语句.
- new 操作先开辟内存, 此时 A 现成的 CPU 时间片到了, 切换到 B 线程.
- B 线程由于 single 为 nullptr 也进入 if 语句了, 开始 new 操作.
线程安全的懒汉单例模式
// 使用 pthread库 中提供的线程互斥操作方法 mutex 互斥锁.
class CSingleton
{
public:
static CSingleton *getInstance1()
{
// 获取互斥锁 (效率太低, 每次获取实例, 第一次加锁有必要, 后面都没必要)
pthread_mutex_lock(&mutex);
if (nullptr == single)
{
single = new CSingleton();
}
// 释放互斥锁
pthread_mutex_unlock(&mutex);
return single;
}
static CSingleton *getInstance2()
{
if(single == nullptr)
{
// 获取互斥锁.
pthread_mutex_lock(&mutex);
// 锁 + 双重判断
if(single == nullptr)
{
single = new CSingleton();
}
// 释放互斥锁.
pthread_mutex_unlock(&mutex);
}
return single;
}
private:
static CSingleton *single;
CSingleton() { cout << "CSingleton()" << endl; }
~CSingleton()
{
pthread_mutex_destroy(&mutex); // 释放互斥锁
cout << "~CSingleton()" << endl;
}
CSingleton(const CSingleton &);
class CRelease
{
public:
~CRelease() { delete single; }
};
static CRelease release;
// 定义线程间的互斥锁
static pthread_mutex_t mutex;
};
CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
// 互斥锁的初始化
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;
封装互斥锁的线程安全懒汉单例模式
class CMutex
{
public:
CMutex() { pthread_mutex_init(&mutex, NULL); }; // 初始化互斥锁.
~CMutex() { pthread_mutex_destroy(&mutex); }; // 销毁互斥锁.
void lock() { pthread_mutex_lock(&mutex); }; // 加锁,获取互斥锁.
void unlock() { pthread_mutex_unlock(&mutex); }; // 解锁,释放互斥锁.
private:
pthread_mutex_t mutex; // 定义互斥锁对象.
};
class CSingleton
{
public:
static CSingleton *getInstance1()
{
if (single == nullptr)
{
mutex.lock();
if (single == nullptr)
{
single = new CSingleton();
}
mutex.unlock();
}
return single;
}
// 以下代码是否线程安全?
static CSingleton *getInstance2()
{
static CSingleton single;
return &single;
}
// 对于 static 静态局部变量的初始化, 编译器会自动对它的初始化进行加锁和解锁控制, 使静态局部变量的初始化成为线程安全的操作,
// 不用担心多线程都会初始化静态局部变量!
private:
CSingleton() { cout << "CSingleton()" << endl; }
~CSingleton() { cout << "~CSingleton()" << endl; }
CSingleton(const CSingleton &);
class CRelease
{
public:
~CRelease()
{
delete single;
}
};
static CRelease release;
static CSingleton *single;
// 线程间的静态互斥锁.
static CMutex mutex;
};
CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
// 定义互斥锁静态对象.
CMutex CSingleton::mutex;
Double-Checked Locking thesis (DCLP)
// from the header file
class Singleton
{
public:
static Singleton* instance();
private:
static Singleton* pInstance;
};
// from the implementation file
Singleton* Singleton::instance()
{
if(pInstance == 0)
{
pInstance = new Singleton();
}
return pInstance;
} // unsafe
// 采用锁可实现安全获取单例对象.
Singleton* Singleton::instance()
{
Lock lock; //acquire lock.
if(pInstance == 0)
{
pInstance = new Singleton();
}
// release lock
return pInstance;
}
// 这种方法能做到线程安全, 缺点就是资源耗费严重, 每次获取实例都需要获取锁,实际上只需要在第一次获取对象时需要加锁.
// The Double-Checked Locking Pattern (DCLP) 实现.
Singleton* Singleton::instance()
{
if(pInstance == 0) // 1st
{
Lock lock;
if(pInstance == 0) // 2st
{
pInstance = new Singleton();
}
// release lock
}
return pInstance;
}
// This statement causes three things to happen:
1. Allocate memory to hold a Singleton object. (分配内存以保存 Singleton 对象).
2. Construct a Singleton object in the allocated memory. (在分配的内存中构造一个 Singleton 对象).
3. Make pInstance point to the allocated memory. (使 pInstance 指向分配的内存).
// 有时编译器交换步骤 2和 3. 如下:
Singleton* Singleton::instance()
{
if(pInstance == 0)
{
Lock lock;
if(pInstance == 0)
{
pInstance = // Step 3.
void* rawMemory = operator new (sizeof(Singleton)); // Step 1.
new (rawMemory) Singleton; // Step 2.
}
}
return pInstance;
}
// volatile 关键字
class Singleton
{
public:
static Singleton* instance();
private:
static Singleton* volatile pInstance; // volatile added.
int x;
Singleton()
:x(5)
{}
};
// from the implementation file
Singleton* volatile Singleton::pInstance = 0;
Singleton* Singleton::instance()
{
if(pInstance == 0)
{
Lock lock;
if(pInstance == 0)
{
Singleton* volatile temp = new Singleton(); //volatile added
pInstance = temp;
}
}
return pInstance;
}
// 内联函数展开之后...
// After inlining the constructor, the code looks like this:
if(pInstance == 0)
{
Lock lock;
if(pInstance == 0)
{
Singleton* volatile temp =
static_cast<Singleton*>(operator new (sizeof(Singleton)));
temp->x = 5; // inlined Single constructor
pInstance = temp;
}
}
// problem:
// 编译器可以对 temp->x 的赋值重新排序,以重新排序对 pInstance 的赋值。如果这样做,pInstance 将在它指向的数据初始化之前分配,
// 这再次导致其他线程读取未初始化的 x 的可能性.
// 可能解决的方法:
class Singleton
{
public:
static volatile Singleton* volatile instance();
private:
static volatile Singleton* volatile pInstance;
};
volatile Singleton* volatile Singleton::pInstance = 0;
volatile Singleton* volatile Singleton::instance1()
{
if(pInstance == 0)
{
Lock lock;
if(pInstance == 0)
{
volatile Singleton* volatile temp =
new volatile Singleton();
pInstance = temp;
}
}
return pInstance;
}
volatile Singleton* volatile Singleton::instance2()
{
if(pInstance == 0)
{
Lock lock;
if(pInstance == 0)
{
Singleton* volatile temp =
static_cast<Singleton*> (operator new (sizeof(Singleton)));
static_cast<volatile int&>(temp->x) = 5;
pInstance = temp;
}
}
return pInstance;
}
// 缓存一致性问题的一般解决方案是使用内存屏障
Singleton* Singleton::instance()
{
Singleton* tmp = pInstance;
//.... insert memory barrier.
if(tmp == 0)
{
Lock lock;
tmp = pInstance;
if(tmp == 0)
{
tmp = new Singleton();
//... insert memory barrier.
pInstance = tmp;
}
}
return tmp;
}
面试考题
单例对象为什么是 static ?
-
唯一性:静态变量属于类,而不是类的某个实例。这意味着静态变量在所有实例间共享。通过将单例实例声明为
static
,我们确保该类只有一个实例,即使有多个对象引用,所有这些引用都指向同一个static
实例。这个机制可以避免创建多个实例,从而保持单例的唯一性。 -
全局访问:静态变量和方法可以通过类名直接访问,不需要创建类的对象。这使得单例模式中的实例可以方便地在整个应用中被访问。这一点是单例模式的一个重要特性:通过类名直接访问单例对象,而无需实例化新对象。
-
生命周期控制:静态变量的生命周期随着类的加载而开始,随着类的卸载而结束。这意味着单例对象的生命周期通常贯穿整个程序的运行时间,它的初始化发生在第一次访问类时,而销毁则发生在程序结束时。这保证了单例对象在程序运行期间始终存在,且可以全局访问。
如何初始化单例对象?
- 饿汉式单例(Eager Initialization)
饿汉式的单例通常通过静态变量在程序开始时直接初始化,这个过程是线程安全的,因为 C++ 的静态初始化由编译器保证是安全的。
优点:
1. 实现简单:静态局部变量的初始化是 C++11 以后线程安全的,所以不需要额外的锁或同步机制。
2. 类加载时即完成实例化:单例对象在类第一次被使用时初始化,避免了多线程竞争问题。
缺点:
1. 资源浪费:即使程序可能不需要这个实例,也会在类加载时创建对象。如果单例对象较大,可能会占用不必要的资源。
- 懒汉式单例(Lazy Singleton)
懒汉式是只有在第一次调用时才初始化单例对象的方式。通常使用双重检查锁定来确保线程安全。在 C++ 中,为了保证双重检查锁定的线程安全性,std::mutex
和 std::lock_guard
可以用于线程同步。
优点:
- 延迟加载:只有在真正需要时才创建对象,节省了内存和资源。
- 节省资源:比饿汉式更加灵活,可以避免不必要的对象创建。
缺点:
- 实现复杂:需要使用双重检查锁定和
std::mutex
来确保线程安全,增加了代码复杂度。 - 线程同步问题:如果不正确处理线程安全,会导致在多线程环境下创建多个实例。
如何销毁单例对象?
1. 手动销毁
可以通过提供一个销毁单例对象的方法手动控制对象的生命周期。比如,定义一个 destroyInstance
函数来手动删除单例对象:
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr)
{
instance = new Singleton();
}
return instance;
}
static void destroyInstance() {
delete instance;
instance = nullptr;
}
private:
Singleton() {}
~Singleton() {}
static Singleton* instance;
};
Singleton* Singleton::instance = nullptr;
优点:
- 可以显式地控制对象的销毁时间,避免对象的生命周期贯穿整个程序。
缺点:
- 需要程序员主动调用
destroyInstance
,如果忘记调用,可能会造成内存泄漏。 - 如果在多线程环境下,需要注意同步问题
2. 智能指针
通过使用 std::unique_ptr
管理单例对象的生命周期,std::unique_ptr
会在程序结束时自动销毁单例对象,从而避免手动管理内存。
#include <memory>
class Singleton {
public:
static Singleton& getInstance() {
static std::unique_ptr<Singleton> instance(new Singleton());
return *instance;
}
private:
Singleton() {}
~Singleton() {}
};
优点:
- 自动管理内存:使用
std::unique_ptr
确保对象在不再需要时自动销毁,避免内存泄漏。 - 简单性:智能指针会自动管理对象的生命周期,不需要显式调用
delete
。
缺点:
- 无法手动控制销毁时间:智能指针会在作用域结束时销毁对象,但无法精确控制销毁时间。如果你希望对象在程序某一特定时刻销毁,则不适用。
- 性能开销:相较于静态局部变量,智能指针会带来一些额外的管理开销。
3. 使用静态局部变量
通过静态局部变量的方式实现单例对象,C++11 以后,静态局部变量的初始化是线程安全的,而且对象会在程序结束时自动销毁。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量,线程安全
return instance;
}
private:
Singleton() {}
~Singleton() {}
};
优点:
- 实现简洁:使用静态局部变量可以简化单例的实现,静态局部变量只会在第一次访问时被初始化。
- 线程安全:从 C++11 开始,静态局部变量的初始化是线程安全的,因此无需手动同步。
- 自动销毁:程序结束时,静态局部变量会自动销毁,无需显式删除对象。
缺点:
无法手动控制对象的销毁:静态局部变量的生命周期与程序一致,它会在程序结束时销毁,而不能在程序运行中显式控制对象的销毁时机。