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