[Android]《Android 源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》讀書筆記 3

簡(jiǎn)介

這周繼續(xù)寫《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》讀書筆記。本書的第二章介紹了單例模式的各種實(shí)現(xiàn)方式,以及在 Android 源碼中的應(yīng)用。

單例模式介紹

確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。它的作用是避免產(chǎn)生多個(gè)對(duì)象消耗過(guò)多的資源,或者某種類型的對(duì)象只應(yīng)該有且只有一個(gè)。比如創(chuàng)建一個(gè)對(duì)象需要消耗的資源過(guò)多,如要訪問(wèn) IO 和數(shù)據(jù)庫(kù)等資源。

單例模式使用要點(diǎn)

單例模式 UML 類圖如下:

單例模式 UML 類圖

實(shí)現(xiàn)單例模式主要有如下幾個(gè)關(guān)鍵點(diǎn):

1.構(gòu)造函數(shù)不對(duì)外開(kāi)放,一般為 Private;

2.通過(guò)一個(gè)靜態(tài)方法或者枚舉返回單例對(duì)象;

3.確保單例類的對(duì)象只有一個(gè),尤其是在多線程環(huán)境下(難點(diǎn));

4.確保單例類對(duì)象在反序列化時(shí)不會(huì)重新構(gòu)建對(duì)象。

單例模式用法

餓漢模式

public class Singleton {  
     private static Singleton instance = new Singleton();  
     private Singleton (){
     }
     public static Singleton getInstance() {  
     return instance;  
     }  
 }  

餓漢模式在裝載類時(shí)就創(chuàng)建對(duì)象實(shí)例,是典型的空間換時(shí)間。

懶漢模式

public class Singleton {
    private static Singleton instance;
    private Singleton(){};
    
    public static synchronized Singleton getInstance(){
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

懶漢模式在每次獲取實(shí)例時(shí)都會(huì)進(jìn)行判斷,是典型的時(shí)間換空間。 getInstance() 方法中添加了 synchronized關(guān)鍵字,也就是上面所說(shuō)的在多線程情況下保證單例對(duì)象唯一性的手段。但是即使 instance 已經(jīng)被初始化,每次調(diào)用 getInstance() 方法都會(huì)進(jìn)行同步,浪費(fèi)不必要的資源,這也就是懶漢模式的最大問(wèn)題。因此這種模式一般不建議使用。

雙重檢查鎖定(Double Check Lock)實(shí)現(xiàn)單例

public class DCLSingleton {
    // JDK1.5后的版本才可使用volatile關(guān)鍵字,保證sInstance對(duì)象每次都從主內(nèi)存中讀取
    private volatile static DCLSingleton sInstance = null;

    private DCLSingleton() {
    }
    
    public static DCLSingleton getInstance(){
        if(sInstance==null){
            synchronized(DCLSingleton.class){
                if(sInstance==null){
                    sInstance=new DCLSingleton();
                }
            }
        }
        return sInstance;
    }
}

這個(gè)寫法的特別之處在于對(duì) instance 進(jìn)行了兩次判空:第一層主要是為了避免不必要的同步,第二層則是為了在 null 的情況下創(chuàng)建實(shí)例。
我們會(huì)發(fā)現(xiàn)上面代碼有一個(gè)volatile關(guān)鍵字,因?yàn)樵谶@里會(huì)有DCL失效問(wèn)題。

DCL 失效問(wèn)題:假設(shè)線程 A 執(zhí)行到sInstance=new DCLSingleton()語(yǔ)句,這看上去像是一句代碼,實(shí)際上它并不是一個(gè)原子操作,這句代碼最終會(huì)被編譯為多條匯編指令,它大致做了三件事:

1.給 sInstance 的實(shí)例分配內(nèi)存;

2.調(diào)用 DCLSingleton 的構(gòu)造函數(shù),初始化成員字段;

3.將 sInstance 對(duì)象指向分配的內(nèi)存空間(此時(shí) sInstance 就不是 null了)。

但是由于 Java 編譯器允許處理器亂序執(zhí)行。因此執(zhí)行順序可能是 1-2-3 也可能是 1-3-2,如果是后者,并且在 3 執(zhí)行完畢、2 未執(zhí)行之前被切換到 B 線程上,這時(shí)的 sInstance 因?yàn)橐呀?jīng)在線程 A 內(nèi)執(zhí)行過(guò)了第三點(diǎn),sInstance 已經(jīng)是非空了,所以線程 B 直接取走 sInstance,再使用就會(huì)出錯(cuò),這就是 DCL 失效問(wèn)題。

JDK 1.5 之后的版本具體化了 volatile 關(guān)鍵字,用它可以保證 sInstance 對(duì)象每次都從主內(nèi)存中讀取,雖然會(huì)影響性能,這種方式第一次加載時(shí)會(huì)稍慢,在高并發(fā)環(huán)境會(huì)有缺陷,但是一般能夠滿足需求。

靜態(tài)內(nèi)部類單例模式

public class Singleton implements  Serializable{
    private Singleton(){}
    
    public static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    /**
     * 靜態(tài)內(nèi)部類
     */
    private static class SingletonHolder{
        private static final Singleton sInstance=new Singleton();
    }
    
    /**
     * 為了杜絕對(duì)象在反序列化時(shí)重新生成對(duì)象,則重寫Serializable的私有方法
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException{
        return SingletonHolder.sInstance;
    }
    
}

這種是推薦使用的單例模式實(shí)現(xiàn)方式。當(dāng)?shù)谝淮渭虞dSingleton類時(shí)并不會(huì)初始化INSTANCE,只有在第一次調(diào)用getInstance方法時(shí)才會(huì)導(dǎo)致INSTANCE被初始化。這種方式不僅能夠保證線程安全,也能保證單例對(duì)象的唯一性,同時(shí)也延長(zhǎng)了單例的實(shí)例化。
上面的代碼重寫了 readResolve() 方法,這是因?yàn)橥ㄟ^(guò)序列化可以將一個(gè)單例的實(shí)例對(duì)象寫到磁盤,然后讀回來(lái),從而獲得一個(gè)實(shí)例。即使構(gòu)造函數(shù)是私有的,反序列化時(shí)依然可以通過(guò)特殊的途徑去創(chuàng)建類的一個(gè)新的實(shí)例,相當(dāng)于調(diào)用該類的構(gòu)造函數(shù)。反序列化操作提供了一個(gè)特別的鉤子函數(shù),類中具有一個(gè)私有的、被實(shí)例化的方法 readResolve(),這個(gè)方法可以讓開(kāi)發(fā)人員控制對(duì)象的反序列化。重寫該方法返回 SingletonHolder.sInstance ,而不是默認(rèn)的生成新的實(shí)例,從而保持單例。

枚舉單例

public enum SingletonEnum {
    INSTANCE;
    public void doSomething(){
        System.out.println("do sth.");
    }
}

這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問(wèn)題,而且還能防止反序列化重新創(chuàng)建新的對(duì)象。

容器實(shí)現(xiàn)單例

public class SingletonManager {
    private static Map<String,Object> objMap=new HashMap<String,Object>();
    
    private SingletonManager(){};
    
    public static void registerService(String key,Object instance){
        if(!objMap.containsKey(key)){
            objMap.put(key, instance);
        }
    }
    
    public static Object getService(String key){
        return objMap.get(key);
    }
}

將多種單例類型注入到一個(gè)統(tǒng)一的管理類中,在使用時(shí)根據(jù)key獲取對(duì)象對(duì)應(yīng)類型的對(duì)象。這種方式使得我們可以管理多種類型的單例,并且在使用時(shí)可以通過(guò)統(tǒng)一的接口進(jìn)行獲取操作,降低了用戶的使用成本,也對(duì)用戶隱藏了具體實(shí)現(xiàn),降低了耦合度。

單例模式運(yùn)用場(chǎng)景

  1. Windows 的 Task Manager (任務(wù)管理器)就是很典型的單例模式(這個(gè)很熟悉吧),想想看,是不是呢,你能打開(kāi)兩個(gè) windows task manager 嗎? 不信你自己試試看哦~

  2. windows的Recycle Bin(回收站)也是典型的單例應(yīng)用。在整個(gè)系統(tǒng)運(yùn)行過(guò)程中,回收站一直維護(hù)著僅有的一個(gè)實(shí)例。

  3. 網(wǎng)站的計(jì)數(shù)器,一般也是采用單例模式實(shí)現(xiàn),否則難以同步。

  4. 應(yīng)用程序的日志應(yīng)用,一般都何用單例模式實(shí)現(xiàn),這一般是由于共享的日志文件一直處于打開(kāi)狀態(tài),因?yàn)橹荒苡幸粋€(gè)實(shí)例去操作,否則內(nèi)容不好追加。

  5. Web 應(yīng)用的配置對(duì)象的讀取,一般也應(yīng)用單例模式,這個(gè)是由于配置文件是共享的資源。

  6. 數(shù)據(jù)庫(kù)連接池的設(shè)計(jì)一般也是采用單例模式,因?yàn)閿?shù)據(jù)庫(kù)連接是一種數(shù)據(jù)庫(kù)資源。數(shù)據(jù)庫(kù)軟件系統(tǒng)中使用數(shù)據(jù)庫(kù)連接池,主要是節(jié)省打開(kāi)或者關(guān)閉數(shù)據(jù)庫(kù)連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因?yàn)楹斡脝卫J絹?lái)維護(hù),就可以大大降低這種損耗。

  7. 多線程的線程池的設(shè)計(jì)一般也是采用單例模式,這是由于線程池要方便對(duì)池中的線程進(jìn)行控制。

  8. 操作系統(tǒng)的文件系統(tǒng),也是大的單例模式實(shí)現(xiàn)的具體例子,一個(gè)操作系統(tǒng)只能有一個(gè)文件系統(tǒng)。

  9. HttpApplication 也是單位例的典型應(yīng)用。熟悉 ASP.Net(IIS) 的整個(gè)請(qǐng)求生命周期的人應(yīng)該知道 HttpApplication 也是單例模式,所有的 HttpModule 都共享一個(gè) HttpApplication 實(shí)例.

總結(jié)以上,不難看出:

單例模式應(yīng)用的場(chǎng)景一般發(fā)現(xiàn)在以下條件下:

(1)資源共享的情況下,避免由于資源操作時(shí)導(dǎo)致的性能或損耗等。如上述中的日志文件,應(yīng)用配置。

(2)控制資源的情況下,方便資源之間的互相通信。如線程池等。

Android源碼中的單例模式

在 Android 系統(tǒng)中,我們經(jīng)常會(huì)通過(guò) Context 獲取系統(tǒng)級(jí)別的服務(wù),如 WindowsManagerService、ActivityManagerService 等,更常用的是一個(gè) LayoutInflater 的類,這些服務(wù)會(huì)在合適的時(shí)候以單例的形式注冊(cè)在系統(tǒng)中,在我們需要的時(shí)候就通過(guò) Context 的 getSystemService(String name) 獲取。

總結(jié)

優(yōu)點(diǎn):

1.由于單例模式在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存開(kāi)支,特別是一個(gè)對(duì)象需要頻繁的創(chuàng)建、銷毀時(shí),而且創(chuàng)建或銷毀時(shí)性能又無(wú)法優(yōu)化,單例模式的優(yōu)勢(shì)就非常明顯。

2.單例模式可以避免對(duì)資源的多重占用,例如一個(gè)文件操作,由于只有一個(gè)實(shí)例存在內(nèi)存中,避免對(duì)同一資源文件的同時(shí)操作。

3.單例模式可以在系統(tǒng)設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn),例如,可以設(shè)計(jì)一個(gè)單例類,負(fù)責(zé)所有數(shù)據(jù)表的映射處理。

缺點(diǎn):

1.單例模式一般沒(méi)有接口,擴(kuò)展很困難,若要擴(kuò)展,只能修改代碼來(lái)實(shí)現(xiàn)。

2.單例對(duì)象如果持有 Context,那么很容易引發(fā)內(nèi)存泄露。此時(shí)需要注意傳遞給單例對(duì)象的 Context 最好是 Application Context。

參考資料

設(shè)計(jì)模式之——單例模式(Singleton)的常見(jiàn)應(yīng)用場(chǎng)景

《Android 源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn) 》

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

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,366評(píng)論 25 708
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,715評(píng)論 19 139
  • 介紹 單例對(duì)象的類必須保證只有一個(gè)實(shí)例存在。某個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。 場(chǎng)景 避...
    appcompat_v7閱讀 266評(píng)論 0 1
  • 我想幫幫她。 她說(shuō)催回家的電話不會(huì)打,短信居然也發(fā)不出。寫不出來(lái)。寫了刪,刪了寫,終又字字撤回。為什么怎么寫都感覺(jué)...
    斑錦閱讀 576評(píng)論 16 12
  • 春天的那次郊游盡興而歸。 果樹居士還揀到兩塊黑乎乎的爛樹疙瘩。樹疙瘩和一些樹枝草桿小石頭什么的堆在一塊田的地頭,應(yīng)...
    鉛筆芒種閱讀 2,008評(píng)論 0 1

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