原始的單例模式
單例模式要做如下事情:
- 不能通過構造函數(shù)構造,否則就能夠實例化多個。構造函數(shù)需要私有聲明
- 保證只能產(chǎn)生一個實例
下面是一個簡單的實現(xiàn):
class Singleton
{
private:
static Singleton *local_instance;
Singleton(){};
public:
static Singleton *getInstance()
{
if (local_instance == nullptr)
{
local_instance = new Singleton();
}
return local_instance;
}
};
Singleton * Singleton::local_instance = nullptr;
int main()
{
Singleton * s = Singleton::getInstance();
return 0;
}
使用局部靜態(tài)對象來解決存在的兩個問題
剛剛的代碼中有兩個問題,一個是多線程的情況下可能會出現(xiàn)new兩次的情況。另外一個是程序退出后沒有運行析構函數(shù)。
下面采用了靜態(tài)對象來解決。
class Singleton
{
private:
static Singleton *local_instance;
Singleton(){
cout << "構造" << endl;
};
~Singleton(){
cout << "析構" << endl;
}
public:
static Singleton *getInstance()
{
static Singleton locla_s;
return &locla_s;
}
};
int main()
{
cout << "單例模式訪問第一次前" << endl;
Singleton * s = Singleton::getInstance();
cout << "單例模式訪問第一次后" << endl;
cout << "單例模式訪問第二次前" << endl;
Singleton * s2 = Singleton::getInstance();
cout << "單例模式訪問第二次后" << endl;
return 0;
}

該代碼可能在c++11之前的版本導致多次構造函數(shù)的調(diào)用,所以只能在較新的編譯器上使用。
如果是c++11之前的版本,靜態(tài)對象線程會不安全
下面這個版本使用了mutex以及靜態(tài)成員來析構單例。該方案的劣處在于鎖導致速度慢,效率低。但是至少是正確的,也能在c++11之前的版本使用,代碼的示例如下:
class Singleton
{
private:
static Singleton *local_instance;
static pthread_mutex_t mutex;
Singleton(){
cout << "構造" << endl;
};
~Singleton(){
cout << "析構" << endl;
}
class rememberFree{
public:
rememberFree(){
cout << "成員構造" << endl;
}
~rememberFree(){
if(Singleton::local_instance != nullptr){
delete Singleton::local_instance;
}
}
};
static rememberFree remember;
public:
static Singleton *getInstance()
{
pthread_mutex_lock(&mutex);
if (local_instance == nullptr)
{
local_instance = new Singleton();
}
pthread_mutex_unlock(&mutex);
return local_instance;
}
};
Singleton * Singleton::local_instance = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
Singleton::rememberFree Singleton::remember;
使用雙鎖檢查導致未初始化的內(nèi)存訪問
使用如下的代碼來實現(xiàn)已經(jīng)初始化的對象的直接返回。可以使上述代碼性能會大大加快。但是相同的代碼在Java下面有很明顯的問題,由于CPU亂序執(zhí)行,可能導致訪問到未經(jīng)初始化的對象的引用。
C++是否有同樣的問題呢?看下文: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
結論是一樣的,c++也存在相同的問題,可能導致未定義行為導致段錯誤。雙鎖檢查代碼的例子如下:
static Singleton *getInstance()
{
if(local_instance == nullptr){
pthread_mutex_lock(&mutex);
if (local_instance == nullptr)
{
local_instance = new Singleton();
}
pthread_mutex_unlock(&mutex);
}
return local_instance;
}
假如線程A進入鎖內(nèi)并分配對象的空間,但是由于指令可能亂序,實際上導致local_instance被先指向一塊未被分配的內(nèi)存,然后再在這塊內(nèi)存上進程初始化。但是在指向后,未初始化前,另一線程B可能通過getInstance獲取到這個指針。
嘗試使用局部變量并不能保證指令執(zhí)行順序
嘗試使用臨時變量強制指定指令運行順序時,仍然會被編譯器認為是無用的變量,然后被優(yōu)化掉。下述代碼是一個想法很好但是無法實現(xiàn)目的代碼:
if(local_instance == nullptr){
static mutex mtx;
lock_guard<mutex> lock(mtx);
if (local_instance == nullptr)
{
auto tmp = new Singleton()
local_instance = tmp; }
}
return local_instance;
不優(yōu)雅的使用volatile來解決指令亂序在雙檢查鎖中出現(xiàn)的問題
嘗試使用volatile聲明內(nèi)部的指針,代碼如下:
class Singleton
{
private:
static Singleton * volatile local_instance;
Singleton(){
cout << "構造" << endl;
};
~Singleton(){
cout << "析構" << endl;
}
class rememberFree{
public:
rememberFree(){
cout << "成員構造" << endl;
}
~rememberFree(){
if(Singleton::local_instance != nullptr){
delete Singleton::local_instance;
}
}
};
static rememberFree remember;
public:
static Singleton *getInstance()
{
if(local_instance == nullptr){
static mutex mtx;
lock_guard<mutex> lock(mtx);
if (local_instance == nullptr)
{
auto tmp = new Singleton();
local_instance = tmp;
}
}
return local_instance;
}
};
Singleton * volatile Singleton::local_instance = nullptr;
Singleton::rememberFree Singleton::remember;
int main()
{
cout << "單例模式訪問第一次前" << endl;
Singleton * s = Singleton::getInstance();
cout << "單例模式訪問第一次后" << endl;
cout << "單例模式訪問第二次前" << endl;
Singleton * s2 = Singleton::getInstance();
cout << "單例模式訪問第二次后" << endl;
return 0;
}
在這份代碼中,雖然temp是volatile,但是*temp不是,其成員也不是。所以仍然可能被優(yōu)化。嘗試將其*temp也聲明為volatile,你會發(fā)現(xiàn)的的代碼充滿了volatile。但是至少是正確的:
class Singleton
{
private:
static volatile Singleton * volatile local_instance;
Singleton(){
cout << "構造" << endl;
};
~Singleton(){
cout << "析構" << endl;
}
class rememberFree{
public:
rememberFree(){
cout << "成員構造" << endl;
}
~rememberFree(){
if(Singleton::local_instance != nullptr){
delete Singleton::local_instance;
}
}
};
static rememberFree remember;
public:
static volatile Singleton *getInstance()
{
if(local_instance == nullptr){
static mutex mtx;
lock_guard<mutex> lock(mtx);
if (local_instance == nullptr)
{
auto tmp = new Singleton();
local_instance = tmp;
}
}
return local_instance;
}
};
volatile Singleton * volatile Singleton::local_instance = nullptr;
Singleton::rememberFree Singleton::remember;
int main()
{
cout << "單例模式訪問第一次前" << endl;
volatile Singleton * s = Singleton::getInstance();
cout << "單例模式訪問第一次后" << endl;
cout << "單例模式訪問第二次前" << endl;
volatile Singleton * s2 = Singleton::getInstance();
cout << "單例模式訪問第二次后" << endl;
return 0;
}
大殺器——內(nèi)存柵欄
在新的標準中,atomic類實現(xiàn)了內(nèi)存柵欄,使得多個核心訪問內(nèi)存時可控。這利用了c++11的內(nèi)存訪問順序可控。下面是代碼實現(xiàn):
class Singleton
{
private:
// static volatile Singleton * volatile local_instance;
static atomic<Singleton*> instance;
Singleton(){
cout << "構造" << endl;
};
~Singleton(){
cout << "析構" << endl;
}
class rememberFree{
public:
rememberFree(){
cout << "成員構造" << endl;
}
~rememberFree(){
Singleton* local_instance = instance.load(std::memory_order_relaxed);
if(local_instance != nullptr){
delete local_instance;
}
}
};
static rememberFree remember;
public:
static Singleton *getInstance()
{
Singleton* tmp = instance.load(std::memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if(tmp == nullptr){
static mutex mtx;
lock_guard<mutex> lock(mtx);
tmp = instance.load(memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton();
atomic_thread_fence(memory_order_release);
instance.store(tmp, memory_order_relaxed);
}
}
return tmp;
}
};
atomic<Singleton*> Singleton::instance;
Singleton::rememberFree Singleton::remember;
int main()
{
cout << "單例模式訪問第一次前" << endl;
Singleton * s = Singleton::getInstance();
cout << "單例模式訪問第一次后" << endl;
cout << "單例模式訪問第二次前" << endl;
Singleton * s2 = Singleton::getInstance();
cout << "單例模式訪問第二次后" << endl;
return 0;
}
上述代碼可能難以閱讀,instance的兩次加載可以被亂序執(zhí)行。但是在此期間內(nèi)的改動被其他CPU核心觀察不到。在muduo一書上,內(nèi)存柵欄也被評價為大殺器。
使用原子操作的內(nèi)存順序
這里有六個內(nèi)存序列選項可應用于對原子類型的操作:memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, 以及memory_order_seq_cst。除非你為特定的操作指定一個序列選項,要不內(nèi)存序列選項對于所有原子類型默認都是memory_order_seq_cst。雖然有六個選項,但是它們僅代表三種內(nèi)存模型:排序一致序列(sequentially consistent),獲取-釋放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel),和自由序列(memory_order_relaxed)。
這里可以采用的模型有:默認的memory_order_seq_cst即順序一致與memory_order_acquire、memory_order_release即獲取釋放序列。后者性能可能更好。
待完善
使用pthread_once 或者call_once
前者來自pthread庫。后者來自std::atomic。
待完善