設(shè)計模式(三)—— 讓我們一起來扒光“單例模式”

前言

時隔一月之久,設(shè)計模式第三篇終于來了。(好吧,主要還是我自己拖沓了,下次注意)

通過前兩篇文章,相信大家已經(jīng)對設(shè)計模式有了大概的了解。那么今天我們就正式進(jìn)入模式的學(xué)習(xí)。首先肯定要先拿軟柿子捏,單例模式——最簡單的設(shè)計模式

身為程序員,你可能沒有系統(tǒng)的學(xué)習(xí)過設(shè)計模式,但是你一定知道單例模式,因為它相對簡單,而且最常被大家所用到。既然大家都用到過,也都知道為什么我還要單獨列出一篇文章來寫呢?
?
因為絕大部分開發(fā)者平時對單例模式的認(rèn)識,可能僅僅停留在“會用”的階段。為什么會有這個模式?為什么要用這個模式?在哪里用單例模式最合適?亂用了會有什么負(fù)面影響?
?
這些可能大多數(shù)人都一知半解。今天就讓我們大家一起來扒光單例模式的外衣,有深度的認(rèn)識一下單例模式。

扒光

通過這篇文章你能學(xué)到什么

(建議你可以帶著問題去學(xué)習(xí))

  1. 單例模式的?定義
  2. 單例模式在Android源碼中的應(yīng)用
  3. 單例模式的九種寫法以及優(yōu)劣對比
  4. 單例模式的使用場景
  5. 單例模式存在的缺點??????

接下來我們就一起進(jìn)入今天的學(xué)習(xí)了

單例模式的定義

在學(xué)單例模式之前,我想大家都會自己問自己:“單例模式存在的意義是什么?我們?yōu)槭裁匆脝卫J???/strong>

眾所周知,在古代封建社會,一個國家都只有一個國王或者叫皇帝。我們在這個國家的任何一個地方,只要提起國王,大家都知道他是誰。因為國王是唯一的。其實這個就是單例模式的核心思想:保證對象的唯一性。

?單例模式(Singleton Pattern):確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例,這個類稱為單例類,它提供全局訪問的方法。 單例模式是一種對象創(chuàng)建型模式。

??從其定義我們可以看出來單例模式存在三個要點:

1、實例唯一性
2、自行創(chuàng)建
3、全局訪問

????如何設(shè)計一個優(yōu)秀的單例模式?其實也是圍繞著這三點來的。
?
說了這么多了,還不知道單例模式到底啥樣呢?接下來我們一起來著手設(shè)計這個“國王”的單例類。我們先看一下單例模式的類圖:

?
單例模式類圖

單例模式的類圖看起來很簡單,一個私有的當(dāng)前類型的成員變量,一個私有的構(gòu)造方法,一個 getInstance 方法,創(chuàng)建對象不再通過new 而通過 getInstance 讓該類自行創(chuàng)建。相信我們大多數(shù)人使用的單例模式都是這種,因為太簡單了。但是單例模式的寫法可不止這一種。接下來我們一起來看一下單例模式的九種寫法。

單例模式的九種寫法

一、餓漢式(靜態(tài)常量)
 /**
 * 餓漢式(靜態(tài)常量)
 */
class King {
    private static final King kingInstance = new King();

    static King getInstance() {
        return kingInstance;
    }

    private King() {
    }
} 
  • 優(yōu)點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
  • 缺點:在類裝載的時候就完成實例化,沒有達(dá)到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內(nèi)存的浪費。
二、餓漢式(靜態(tài)代碼塊)
/**
 * 餓漢式(靜態(tài)代碼塊)
 */
class King {
    private static King kingInstance;

    static {
        kingInstance = new King();
    }

    private King() {
    }

    public static King getKingInstance() {
        return kingInstance;
    }
} 
  • 優(yōu)點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
  • 缺點:在類裝載的時候就完成實例化,沒有達(dá)到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內(nèi)存的浪費。
三、懶漢式(線程不安全)
/**
 * 懶漢式(線程不安全)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            kingInstance = new King();
        }
        return kingInstance;
    }
}
  • 優(yōu)點:懶加載,只有使用的時候才會加載。
  • 缺點:但是只能在單線程下使用。如果在多線程下,一個線程進(jìn)入了if (singleton == null)判斷語句塊,還未來得及往下執(zhí)行,另一個線程也通過了這個判斷語句,這時便會產(chǎn)生多個實例。所以在多線程環(huán)境下不可使用這種方式。
四、懶漢式(線程安全)
/**
 * 懶漢式(線程安全,同步方法)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static synchronized King getKingInstance() {
        if (kingInstance == null) {
            kingInstance = new King();
        }
        return kingInstance;
    }
}
  • 優(yōu)點:懶加載,只有使用的時候才會加載,獲取單例方法加了同步鎖,保障線程安全。
  • 缺點:效率太低了,每個線程在想獲得類的實例時候,執(zhí)行g(shù)etInstance()方法都要進(jìn)行同步。
五、懶漢式(線程安全,同步代碼塊)
/**
 * 懶漢式(線程安全,同步代碼塊)
 */
public class King {
    private static King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            synchronized (King.class) {
                kingInstance = new King();
            }
        }
        return kingInstance;
    }
}
  • 優(yōu)點:改進(jìn)了第四種效率低的問題。
  • 缺點:不能完全保證單例,假如一個線程進(jìn)入了if (singleton == null)判斷語句塊,還未來得及往下執(zhí)行,另一個線程也通過了這個判斷語句,這時便會產(chǎn)生多個實例。
六、雙重檢查(DCL)
/**
 * 雙重檢查(DCL)
 */
public class King {

    private static volatile King kingInstance;

    private King() {
    }

    public static King getKingInstance() {
        if (kingInstance == null) {
            synchronized (King.class) {
                if (kingInstance == null){
                    kingInstance = new King();
                }
            }
        }
        return kingInstance;
    }
}
  • 優(yōu)點:線程安全;延遲加載;效率較高。
  • 缺點:JDK < 1.5 的時候不可用
  • 不可用原因:由于volatile關(guān)鍵字會屏蔽Java虛擬機(jī)所做的一些代碼優(yōu)化,可能會導(dǎo)致系統(tǒng)運行效率降低,而JDK 1.5 以及之后的版本都修復(fù)了這個問題。(面試裝逼用,謹(jǐn)記?。。。?/strong>
就是為了裝逼
七、靜態(tài)內(nèi)部類
/**
 * 靜態(tài)內(nèi)部類
 */
public class King {

    private King() {
    }

    private static class KingInstance{
        private static final King KINGINSTANCE = new King();
    }

    public static King getInstance(){
        return KingInstance.KINGINSTANCE;
    }
}
  • 優(yōu)點:避免了線程不安全,延遲加載,效率高。
  • 缺點:暫無,最推薦使用。
  • 特點:這種方式跟餓漢式方式采用的機(jī)制類似,但又有不同。
  • 兩者都是采用了類裝載的機(jī)制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是只要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時并不會立即實例化,而是在需要實例化時,調(diào)用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。 類的靜態(tài)屬性只會在第一次加載類的時候初始化,所以在這里,JVM幫助我們保證了線程的安全性,在類進(jìn)行初始化時,別的線程是無法進(jìn)入的。
八、枚舉
/**
 * 枚舉
 */
public enum  King {
    KINGINSTANCE;
}
  • 優(yōu)點:不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對象。
  • 缺點:JDK 1.5之后才能使用。
九、容器類管理
/**
 * 使用容器實現(xiàn)單例模式(可以用于管理單例,有興趣的可以嘗試一下)
 * */
class InstanceManager {
    private static Map<String, Object> objectMap = new HashMap<>();
    private InstanceManager(){}
    public static void registerService(String key,Object instance){
        if (!objectMap.containsKey(key)){
            objectMap.put(key,instance);
        }
    }
    public static Object getService(String key){
        return objectMap.get(key);
    }
}
/**
* 使用方式
* Dog類就不貼出來了 
* 自己隨便寫個就行
* 可以運行一下看看 打印的地址是否一致
*/
class Test {
    public static void main(String[] args) {

        InstanceManager .registerService("dog", new Dog());

        Dog dog = (Dog) InstanceManager .getService("dog");
        Dog dog2 = (Dog) InstanceManager .getService("dog");
        Dog dog3 = (Dog) InstanceManager .getService("dog");
        Dog dog4 = (Dog) InstanceManager .getService("dog");

        System.out.println(dog);
        System.out.println(dog2);
        System.out.println(dog3);
        System.out.println(dog4);
    }
}
  • 優(yōu)點:在程序的初始,將多種單例類型注入到一個統(tǒng)一的管理類中,在使用時根據(jù)key獲取對象對應(yīng)類型的對象。這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進(jìn)行獲取操作, 降低了用戶的使用成本,也對用戶隱藏了具體實現(xiàn),降低了耦合度。
  • 缺點:不常用,有些麻煩

九種寫法的優(yōu)劣對比

名稱 優(yōu)點 缺點 是否推薦
餓漢式(靜態(tài)常量) 寫法簡單,類裝載時完成實例化。 避免了線程同步 急切實例化, 容易內(nèi)存泄漏 可用
餓漢式(靜態(tài)代碼塊) 同上 同上 可用
懶漢式(線程不安全) 懶加載 只能在單線程下使用 多線程不可用
懶漢式(線程安全,同步方法) 懶加載,方法同步鎖 效率低 不推薦用
懶漢式(線程安全,同步代碼塊) 同上,同時改變效率低問題 不能完全保證單例 不推薦用
雙重檢查(DCL)① 線程安全;延遲加載;效率較高 JDK < 1.5 的時候不可用 JDK >1.5 推薦用
靜態(tài)內(nèi)部類② 線程安全,延遲加載,效率高。 暫無發(fā)現(xiàn) 墻裂推薦
枚舉 寫法簡單,防止反序列化 JDK 1.5之后才能使用 JDK >1.5 推薦用
容器實現(xiàn) 可用管理多個單例對象 不常用,多創(chuàng)建了一個Map 可用

①:不可用原因:由于volatile關(guān)鍵字會屏蔽Java虛擬機(jī)所做的一些代碼優(yōu)化,可能會導(dǎo)致系統(tǒng)運行效率降低,而JDK 1.5 以及之后的版本都修復(fù)了這個問題。(面試裝逼用,謹(jǐn)記?。。。?/p>

②:這種方式跟餓漢式方式采用的機(jī)制類似,但又有不同。兩者都是采用了類裝載的機(jī)制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是只要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用, 而靜態(tài)內(nèi)部類方式在Singleton類被裝載時并不會立即實例化,而是在需要實例化時,調(diào)用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。 類的靜態(tài)屬性只會在第一次加載類的時候初始化,所以在這里,JVM幫助我們保證了線程的安全性,在類進(jìn)行初始化時,別的線程是無法進(jìn)入的。

單例模式在Android源碼中的應(yīng)用

在我們每天接觸的Android源碼中其實也有很多地方用到了單例模式:

1、EventBus中獲取實例:

private static volatile EventBus defaultInstance;
public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

可以看到,EventBus采用的是雙重檢查(DCL)的方式實現(xiàn)的單例模式。

2、InputMethodManager獲取實例

static InputMethodManager sInstance;
public static InputMethodManager getInstance() {
    synchronized (InputMethodManager.class) {
        if (sInstance == null) {
            IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
            IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
            sInstance = new InputMethodManager(service, Looper.getMainLooper());
        }
        return sInstance;
    }
}

我們看到,其實這里是懶漢式(同步代碼塊)方式的改寫,去掉了外部判斷為空,放到了里面。然后通過ServiceManger.getService()方法,通過容器的方式獲取了單例。

在Android很多系統(tǒng)服務(wù)都是通過容器獲取的單例。

單例模式在日常開發(fā)中的應(yīng)用場景

日常開發(fā)中我們也有些場景是需要用到單例模式的,例如:

1、圖片加載
2、網(wǎng)絡(luò)請求
3、工具類封裝

案例有很多,相信大家也都有用到,我就不列舉了。這里我們?nèi)绾魏侠淼脑陧椖恐惺褂脝卫J健?/p>

合理的辨析一個設(shè)計是否應(yīng)該為單例模式前,大家先問問自己幾個問題,也是檢驗標(biāo)準(zhǔn):

Quote from 《 Use your singletons wisely 》

Will every application use this class exactly the same way? (keyword: exactly)
Will every application ever need only one instance of this class? (keyword: ever & one)
Should the clients of this class be unaware of the application they are part of?

每一個應(yīng)用(組件/模塊)是否以完全一致的方式來使用這個類?
每一個應(yīng)用(組件/模塊)是否真的只需要這個類的一個實例呢?
對于這個類的客戶端類來說,對他們自己是應(yīng)用中的一部分這件事是否應(yīng)該保持毫無察覺的狀態(tài)呢?

以上3條就是檢驗一個類是否應(yīng)該被設(shè)計為單例模式的判斷準(zhǔn)則,

如果我們對于以上這3條均給出了“是的”的答案,那么這個類就是可以被設(shè)計為單例模式了。反之還是不要用的好。

單例模式的優(yōu)點

單例模式的優(yōu)點其實已經(jīng)在定義中提現(xiàn)了:可以減少系統(tǒng)內(nèi)存開支,減少系統(tǒng)性能開銷,避免對資源的多重占用、同時操作。

單例模式的缺點

任何事物都不是完美的,單例模式也是如此,它也存在以下幾個缺點:

1、違反了單一責(zé)任鏈原則,測試?yán)щy

單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因為單例類既充當(dāng)了工廠角色,提供了工廠方法,同時又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。

2、擴(kuò)展困難

由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。修改功能必須修改源碼。

3、共享資源有可能不一致。

現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術(shù),因此,如果實例化的共享對象長時間不被利用,系統(tǒng)會認(rèn)為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導(dǎo)致共享的單例對象狀態(tài)的丟失。

總結(jié)

今天我們通過文章學(xué)習(xí)了第一個設(shè)計模式,了解了他的設(shè)計理念,學(xué)會了他的九種寫法,也認(rèn)識了他的優(yōu)缺點。相信大家已經(jīng)對單例模式有了一個全新的認(rèn)識。(反正我寫完文章才認(rèn)識到自己原來根本不了解單例模式)

最后還是要給大家說一句話:模式是死的,代碼是活的。不要硬套模式。代碼會告訴你怎么做,你聽就是了。(也是借鑒前輩們的經(jīng)驗)

設(shè)計模式目錄

設(shè)計模式(一)—— 認(rèn)識設(shè)計模式
設(shè)計模式(二)—— 技術(shù)直男正確“面向?qū)ο蟆钡牧笤瓌t
設(shè)計模式(三)—— 單例模式
設(shè)計模式(四)—— 原型模式
設(shè)計模式(五)—— 簡單工廠模式

參考資料

《設(shè)計模式——可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
《Head First設(shè)計模式》
《大話設(shè)計模式》
《設(shè)計模式之禪》
《Android 源碼設(shè)計模式解析與實戰(zhàn)》

單例模式,這一篇差不多了 -- Singleton is an angel but an evil!
單例模式的八種寫法比較
潛談單例模式

劉偉

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

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