【設(shè)計(jì)模式】單例模式

單例模式

介紹

為了節(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í)例。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄 本文的結(jié)構(gòu)如下: 什么是單例模式 為什么要用該模式 模式的結(jié)構(gòu) 代碼示例 優(yōu)點(diǎn)和缺點(diǎn) 適用環(huán)境 模式應(yīng)用 總...
    w1992wishes閱讀 484評論 1 2
  • 概念 java中單例模式是一種常見的設(shè)計(jì)模式,單例模式的寫法有好幾種,比較常見的有:懶漢式單例、餓漢式單例。單例模...
    怡紅快綠閱讀 545評論 0 0
  • 單例模式(Singleton Pattern)是眾多設(shè)計(jì)模式中較為簡單的一個(gè),同時(shí)它也是面試時(shí)經(jīng)常被提及的問題,如...
    廖少少閱讀 651評論 0 1
  • 單例模式(Singleton):在應(yīng)用這個(gè)模式時(shí),單例對象的類必須保證只有一個(gè)實(shí)例存在。許多時(shí)候整個(gè)系統(tǒng)只需要擁有...
    _SHYII閱讀 1,110評論 0 2
  • 說起懶惰我是最有發(fā)言權(quán)的人了,因?yàn)槲揖褪且粋€(gè)當(dāng)知不愧的“懶人”。怎么個(gè)懶法呢,賴床估計(jì)大家都有吧,我嚴(yán)重到那種程度...
    柚萫閱讀 432評論 0 2

友情鏈接更多精彩內(nèi)容