單例模式
介紹
為了節(jié)約系統(tǒng)資源,有時(shí)需要確保系統(tǒng)中某個(gè)類只有唯一一個(gè)實(shí)例,當(dāng)這個(gè)唯一實(shí)例創(chuàng)建成功之后,我們無法再創(chuàng)建一個(gè)同類型的其他對象,所有的操作都只能基于這個(gè)唯一實(shí)例。為了確保對象的唯一性,我們可以通過單例模式來實(shí)現(xiàn),這就是單例模式的動機(jī)所在。
定義
確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例,這個(gè)類稱為單例類,它提供全局訪問的方法。單例模式是一種對象創(chuàng)建型模式。
單例模式有三個(gè)要點(diǎn):一是某個(gè)類只能有一個(gè)實(shí)例;二是它必須自行創(chuàng)建這個(gè)實(shí)例;三是它必須自行向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。
UML類圖

餓漢式單例與懶漢式單例
餓漢式單例類

由于在定義靜態(tài)變量的時(shí)候?qū)嵗瘑卫?,因此在類加載的時(shí)候就已經(jīng)創(chuàng)建了單例對象
當(dāng)類被加載時(shí),靜態(tài)變量instance會被初始化,此時(shí)類的私有構(gòu)造函數(shù)會被調(diào)用,單例類的唯一實(shí)例將被創(chuàng)建。
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
懶漢式單例類與線程鎖定

懶漢式單例在第一次調(diào)用getInstance()方法時(shí)實(shí)例化,在類加載時(shí)并不自行實(shí)例化,這種技術(shù)又稱為延遲加載(Lazy Load)技術(shù),即需要的時(shí)候再加載實(shí)例,為了避免多個(gè)線程同時(shí)調(diào)用getInstance()方法,可以使用鎖的形式,我們可以使用關(guān)鍵字synchronized
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
該懶漢式單例類在getInstance()方法前面增加了關(guān)鍵字synchronized進(jìn)行線程鎖,以處理多個(gè)線程同時(shí)訪問的問題。但是,上述代碼雖然解決了線程安全問題,但是每次調(diào)用getInstance()時(shí)都需要進(jìn)行線程鎖定判斷,在多線程高并發(fā)訪問環(huán)境中,將會導(dǎo)致系統(tǒng)性能大大降低。如何既解決線程安全問題又不影響系統(tǒng)性能呢?我們繼續(xù)對懶漢式單例進(jìn)行改進(jìn)。事實(shí)上,我們無須對整個(gè)getInstance()方法進(jìn)行鎖定,只需對其中的代碼“instance = new LazySingleton();”進(jìn)行鎖定即可。因此getInstance()方法可以進(jìn)行如下改進(jìn):
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
問題貌似得以解決,事實(shí)并非如此。如果使用以上代碼來實(shí)現(xiàn)單例,還是會存在單例對象不唯一。原因如下:
假如在某一瞬間線程A和線程B都在調(diào)用getInstance()方法,此時(shí)instance對象為null值,均能通過instance == null的判斷。由于實(shí)現(xiàn)了synchronized加鎖機(jī)制,線程A進(jìn)入synchronized鎖定的代碼中執(zhí)行實(shí)例創(chuàng)建代碼,線程B處于排隊(duì)等待狀態(tài),必須等待線程A執(zhí)行完畢后才可以進(jìn)入synchronized鎖定代碼。但當(dāng)A執(zhí)行完畢時(shí),線程B并不知道實(shí)例已經(jīng)創(chuàng)建,將繼續(xù)創(chuàng)建新的實(shí)例,導(dǎo)致產(chǎn)生多個(gè)單例對象,違背單例模式的設(shè)計(jì)思想,因此需要進(jìn)行進(jìn)一步改進(jìn),在synchronized中再進(jìn)行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實(shí)現(xiàn)的懶漢式單例類完整代碼如下所示:
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判斷
if (instance == null) {
//鎖定代碼塊
synchronized (LazySingleton.class) {
//第二重判斷
if (instance == null) {
instance = new LazySingleton(); //創(chuàng)建單例實(shí)例
}
}
}
return instance;
}
}
餓漢式單例類與懶漢式單例類比較
餓漢式單例類在類被加載時(shí)就將自己實(shí)例化,它的優(yōu)點(diǎn)在于無須考慮多線程訪問問題,可以確保實(shí)例的唯一性;從調(diào)用速度和反應(yīng)時(shí)間角度來講,由于單例對象一開始就得以創(chuàng)建,因此要優(yōu)于懶漢式單例。但是無論系統(tǒng)在運(yùn)行時(shí)是否需要使用該單例對象,由于在類加載時(shí)該對象就需要?jiǎng)?chuàng)建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統(tǒng)加載時(shí)由于需要?jiǎng)?chuàng)建餓漢式單例對象,加載時(shí)間可能會比較長。
懶漢式單例類在第一次使用時(shí)創(chuàng)建,無須一直占用系統(tǒng)資源,實(shí)現(xiàn)了延遲加載,但是必須處理好多個(gè)線程同時(shí)訪問的問題,特別是當(dāng)單例類作為資源控制器,在實(shí)例化時(shí)必然涉及資源初始化,而資源初始化很有可能耗費(fèi)大量時(shí)間,這意味著出現(xiàn)多線程同時(shí)首次引用此類的機(jī)率變得較大,需要通過雙重檢查鎖定等機(jī)制進(jìn)行控制,這將導(dǎo)致系統(tǒng)性能受到一定影響。
OC中實(shí)現(xiàn)單例模式
實(shí)現(xiàn)單例模式有三個(gè)條件:
- 類的構(gòu)造方法是私有的
- 類提供一個(gè)類方法用于產(chǎn)生對象
- 類中有一個(gè)私有的自己對象
針對于這三個(gè)條件,OC中都是可以做到的
- 類的構(gòu)造方法是私有的,我們只需要重寫allocWithZone方法,讓初始化操作只執(zhí)行一次
- 類提供一個(gè)類方法產(chǎn)生對象,這個(gè)可以直接定義一個(gè)類方法
- 類中有一個(gè)私有的自己對象,我們可以在.m文件中定義一個(gè)屬性即可
簡單版
static Singleton *shareInstance;
+ (instancetype)shareInstance {
if (shareInstance == nil) {
shareInstance = [[Singleton alloc] init];
}
return shareInstance;
}
這樣就創(chuàng)建一個(gè)簡單的單例模式,但實(shí)際上這是一個(gè)不“嚴(yán)格”版本,在實(shí)際中使用,如果我們使用alloc,copy等方法創(chuàng)建對象時(shí),依然會創(chuàng)建新的實(shí)例。而且如果多線程同時(shí)訪問時(shí)候也會創(chuàng)建多個(gè)實(shí)例,因此這樣做是非線程安全的。
懶漢模式
#import "Singleton.h"
@implementation Singleton
static id _instance;
/**
* 由于alloc方法內(nèi)部會調(diào)用allocWithZone: 所以我們只需要保證在該方法只創(chuàng)建一個(gè)對象即可
*/
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
if (_instance == nil) { // 防止頻繁加鎖
@synchronized(self) {
if (_instance == nil) { // 防止創(chuàng)建多次
_instance = [super allocWithZone:zone];
}
}
}
return _instance;
}
+ (instancetype)sharedSingleton{
if (_instance == nil) { // 防止頻繁加鎖
@synchronized(self) {
if (_instance == nil) { // 防止創(chuàng)建多次
_instance = [[self alloc] init];
}
}
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone {
// 因?yàn)閏opy方法必須通過實(shí)例對象調(diào)用, 所以可以直接返回_instance
// return [[self class] allocWithZone:zone];
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
// return [[self class] allocWithZone:zone];
return _instance;
}
餓漢模式(不常用)
#import "HMSingleton.h"
@implementation Singleton
static id _instance;
/**
* 當(dāng)類加載到OC運(yùn)行時(shí)環(huán)境中(內(nèi)存),就會調(diào)用一次(一個(gè)類只會加載1次)
*/
+ (void)load{
_instance = [[self alloc] init];
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
if (_instance == nil) { // 防止創(chuàng)建多次
_instance = [super allocWithZone:zone];
}
return _instance;
}
+ (instancetype)sharedSingleton{
return _instance;
}
- (id)copyWithZone:(NSZone *)zone {
// 因?yàn)閏opy方法必須通過實(shí)例對象調(diào)用, 所以可以直接返回_instance
// return [[self class] allocWithZone:zone];
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
// return [[self class] allocWithZone:zone];
return _instance;
}
@end
GCD實(shí)現(xiàn)單例模式
@implementation Singleton
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
+ (instancetype)sharedSingleton{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (id)copyWithZone:(NSZone *)zone {
// 因?yàn)閏opy方法必須通過實(shí)例對象調(diào)用, 所以可以直接返回_instance
// return [[self class] allocWithZone:zone];
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
// return [[self class] allocWithZone:zone];
return _instance;
}
@end
非ARC
在非ARC的環(huán)境下,需要再加上下面的方法:
- 重寫release方法為空
- 重寫retain方法返回自己
- 重寫retainCount返回1
- 重寫autorelease返回自己
- (oneway void)release { }
- (id)retain { return self; }
- (NSUInteger)retainCount { return 1;}
- (id)autorelease { return self;}
如何判斷是否是ARC
#if __has_feature(objc_arc)
//ARC環(huán)境
#else
//MRC環(huán)境
#endif
總結(jié)
優(yōu)點(diǎn)
- 單例模式提供了對唯一實(shí)例的受控訪問。因?yàn)閱卫惙庋b了它的唯一實(shí)例,所以它可以嚴(yán)格控制客戶怎樣以及何時(shí)訪問它。
- 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對象,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象單例模式無疑可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實(shí)例?;趩卫J轿覀兛梢赃M(jìn)行擴(kuò)展,使用與單例控制相似的方法來獲得指定個(gè)數(shù)的對象實(shí)例,既節(jié)省系統(tǒng)資源,又解決了單例單例對象共享過多有損性能的問題。
缺點(diǎn)
- 由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。
- 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時(shí)又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。
- 現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運(yùn)行環(huán)境都提供了自動垃圾回收的技術(shù),因此,如果實(shí)例化的共享對象長時(shí)間不被利用,系統(tǒng)會認(rèn)為它是垃圾,會自動銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致共享的單例對象狀態(tài)的丟失。
適用場景
- 系統(tǒng)只需要一個(gè)實(shí)例對象,如系統(tǒng)要求提供一個(gè)唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個(gè)對象。
- 客戶調(diào)用類的單個(gè)實(shí)例只允許使用一個(gè)公共訪問點(diǎn),除了該公共訪問點(diǎn),不能通過其他途徑訪問該實(shí)例。