微信公眾號:「小林coding」
用簡潔的方式,分享編程小知識。
什么是線程安全?
在擁有共享數據的多條線程並行執行的程序中,線程安全的代碼會通過同步機制保證各個線程都可以正常且正確的執行,不會出現數據污染等意外情況。
如何保證線程安全?
- 給共享的資源加把鎖,保證每個資源變量每時每刻至多被一個線程占用。
- 讓線程也擁有資源,不用去共享進程中的資源。如: 使用threadlocal可以為每個線程的維護一個私有的本地變量。
什么是單例模式?
單例模式指在整個系統生命周期里,保證一個類只能產生一個實例,確保該類的唯一性。
單例模式分類
單例模式可以分為懶漢式和餓漢式,兩者之間的區別在於創建實例的時間不同:
- 懶漢式:指系統運行中,實例並不存在,只有當需要使用該實例時,才會去創建並使用實例。(這種方式要考慮線程安全)
- 餓漢式:指系統一運行,就初始化創建實例,當需要時,直接調用即可。(本身就線程安全,沒有多線程的問題)
單例類特點
- 構造函數和析構函數為private類型,目的禁止外部構造和析構
- 拷貝構造和賦值構造函數為private類型,目的是禁止外部拷貝和賦值,確保實例的唯一性
- 類里有個獲取實例的靜態函數,可以全局訪問
01 普通懶漢式單例 ( 線程不安全 )
/////////////////// 普通懶漢式實現 -- 線程不安全 //////////////////
#include <iostream> // std::cout
#include <mutex> // std::mutex
#include <pthread.h> // pthread_create
class SingleInstance
{
public:
// 獲取單例對象
static SingleInstance *GetInstance();
// 釋放單例,進程退出時調用
static void deleteInstance();
// 打印單例地址
void Print();
private:
// 將其構造和析構成為私有的, 禁止外部構造和析構
SingleInstance();
~SingleInstance();
// 將其拷貝構造和賦值構造成為私有函數, 禁止外部拷貝和賦值
SingleInstance(const SingleInstance &signal);
const SingleInstance &operator=(const SingleInstance &signal);
private:
// 唯一單例對象指針
static SingleInstance *m_SingleInstance;
};
//初始化靜態成員變量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
SingleInstance* SingleInstance::GetInstance()
{
if (m_SingleInstance == NULL)
{
m_SingleInstance = new (std::nothrow) SingleInstance; // 沒有加鎖是線程不安全的,當線程並發時會創建多個實例
}
return m_SingleInstance;
}
void SingleInstance::deleteInstance()
{
if (m_SingleInstance)
{
delete m_SingleInstance;
m_SingleInstance = NULL;
}
}
void SingleInstance::Print()
{
std::cout << "我的實例內存地址是:" << this << std::endl;
}
SingleInstance::SingleInstance()
{
std::cout << "構造函數" << std::endl;
}
SingleInstance::~SingleInstance()
{
std::cout << "析構函數" << std::endl;
}
/////////////////// 普通懶漢式實現 -- 線程不安全 //////////////////
// 線程函數
void *PrintHello(void *threadid)
{
// 主線程與子線程分離,兩者相互不干涉,子線程結束同時子線程的資源自動回收
pthread_detach(pthread_self());
// 對傳入的參數進行強制類型轉換,由無類型指針變為整形數指針,然后再讀取
int tid = *((int *)threadid);
std::cout << "Hi, 我是線程 ID:[" << tid << "]" << std::endl;
// 打印實例地址
SingleInstance::GetInstance()->Print();
pthread_exit(NULL);
}
#define NUM_THREADS 5 // 線程個數
int main(void)
{
pthread_t threads[NUM_THREADS] = {0};
int indexes[NUM_THREADS] = {0}; // 用數組來保存i的值
int ret = 0;
int i = 0;
std::cout << "main() : 開始 ... " << std::endl;
for (i = 0; i < NUM_THREADS; i++)
{
std::cout << "main() : 創建線程:[" << i << "]" << std::endl;
indexes[i] = i; //先保存i的值
// 傳入的時候必須強制轉換為void* 類型,即無類型指針
ret = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i]));
if (ret)
{
std::cout << "Error:無法創建線程," << ret << std::endl;
exit(-1);
}
}
// 手動釋放單實例的資源
SingleInstance::deleteInstance();
std::cout << "main() : 結束! " << std::endl;
return 0;
}
普通懶漢式單例運行結果:
從運行結果可知,單例構造函數創建了兩個,內存地址分別為0x7f3c980008c0
和0x7f3c900008c0
,所以普通懶漢式單例只適合單進程不適合多線程,因為是線程不安全的。
02 加鎖的懶漢式單例 ( 線程安全 )
/////////////////// 加鎖的懶漢式實現 //////////////////
class SingleInstance
{
public:
// 獲取單實例對象
static SingleInstance *&GetInstance();
//釋放單實例,進程退出時調用
static void deleteInstance();
// 打印實例地址
void Print();
private:
// 將其構造和析構成為私有的, 禁止外部構造和析構
SingleInstance();
~SingleInstance();
// 將其拷貝構造和賦值構造成為私有函數, 禁止外部拷貝和賦值
SingleInstance(const SingleInstance &signal);
const SingleInstance &operator=(const SingleInstance &signal);
private:
// 唯一單實例對象指針
static SingleInstance *m_SingleInstance;
static std::mutex m_Mutex;
};
//初始化靜態成員變量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;
SingleInstance *&SingleInstance::GetInstance()
{
// 這里使用了兩個 if判斷語句的技術稱為雙檢鎖;好處是,只有判斷指針為空的時候才加鎖,
// 避免每次調用 GetInstance的方法都加鎖,鎖的開銷畢竟還是有點大的。
if (m_SingleInstance == NULL)
{
std::unique_lock<std::mutex> lock(m_Mutex); // 加鎖
if (m_SingleInstance == NULL)
{
m_SingleInstance = new (std::nothrow) SingleInstance;
}
}
return m_SingleInstance;
}
void SingleInstance::deleteInstance()
{
std::unique_lock<std::mutex> lock(m_Mutex); // 加鎖
if (m_SingleInstance)
{
delete m_SingleInstance;
m_SingleInstance = NULL;
}
}
void SingleInstance::Print()
{
std::cout << "我的實例內存地址是:" << this << std::endl;
}
SingleInstance::SingleInstance()
{
std::cout << "構造函數" << std::endl;
}
SingleInstance::~SingleInstance()
{
std::cout << "析構函數" << std::endl;
}
/////////////////// 加鎖的懶漢式實現 //////////////////
加鎖的懶漢式單例的運行結果:
從運行結果可知,只創建了一個實例,內存地址是0x7f28b00008c0
,所以加了互斥鎖的普通懶漢式是線程安全的
03 內部靜態變量的懶漢單例(C++11 線程安全)
/////////////////// 內部靜態變量的懶漢實現 //////////////////
class Single
{
public:
// 獲取單實例對象
static Single &GetInstance();
// 打印實例地址
void Print();
private:
// 禁止外部構造
Single();
// 禁止外部析構
~Single();
// 禁止外部復制構造
Single(const Single &signal);
// 禁止外部賦值操作
const Single &operator=(const Single &signal);
};
Single &Single::GetInstance()
{
// 局部靜態特性的方式實現單實例
static Single signal;
return signal;
}
void Single::Print()
{
std::cout << "我的實例內存地址是:" << this << std::endl;
}
Single::Single()
{
std::cout << "構造函數" << std::endl;
}
Single::~Single()
{
std::cout << "析構函數" << std::endl;
}
/////////////////// 內部靜態變量的懶漢實現 //////////////////
內部靜態變量的懶漢單例的運行結果:
-std=c++0x
編譯是使用了C++11的特性,在C++11內部靜態變量的方式里是線程安全的,只創建了一次實例,內存地址是0x6016e8
,這個方式非常推薦,實現的代碼最少!
[root@lincoding singleInstall]#g++ SingleInstance.cpp -o SingleInstance -lpthread -std=c++0x
04 餓漢式單例 (本身就線程安全)
////////////////////////// 餓漢實現 /////////////////////
class Singleton
{
public:
// 獲取單實例
static Singleton* GetInstance();
// 釋放單實例,進程退出時調用
static void deleteInstance();
// 打印實例地址
void Print();
private:
// 將其構造和析構成為私有的, 禁止外部構造和析構
Singleton();
~Singleton();
// 將其拷貝構造和賦值構造成為私有函數, 禁止外部拷貝和賦值
Singleton(const Singleton &signal);
const Singleton &operator=(const Singleton &signal);
private:
// 唯一單實例對象指針
static Singleton *g_pSingleton;
};
// 代碼一運行就初始化創建實例 ,本身就線程安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton;
Singleton* Singleton::GetInstance()
{
return g_pSingleton;
}
void Singleton::deleteInstance()
{
if (g_pSingleton)
{
delete g_pSingleton;
g_pSingleton = NULL;
}
}
void Singleton::Print()
{
std::cout << "我的實例內存地址是:" << this << std::endl;
}
Singleton::Singleton()
{
std::cout << "構造函數" << std::endl;
}
Singleton::~Singleton()
{
std::cout << "析構函數" << std::endl;
}
////////////////////////// 餓漢實現 /////////////////////
餓漢式單例的運行結果:
從運行結果可知,餓漢式在程序一開始就構造函數初始化了,所以本身就線程安全的
特點與選擇
- 懶漢式是以時間換空間,適應於訪問量較小時;推薦使用內部靜態變量的懶漢單例,代碼量少
- 餓漢式是以空間換時間,適應於訪問量較大時,或者線程比較多的的情況