只一篇就夠了·設(shè)計(jì)模式(5) - 單例模式

單例模式(Singleton Pattern)確保一個(gè)類只有一個(gè)實(shí)例,并且提供一個(gè)全局的訪問。

單例模式隨處可見,比如線程池、緩存對(duì)話框、日志對(duì)象等,這些時(shí)候如果制造出多個(gè)實(shí)例,程序運(yùn)行就會(huì)出現(xiàn)預(yù)期之外的情況。
這里可能有疑問,我用全局靜態(tài)變量也能做到一個(gè)類只有一個(gè)實(shí)例,為什么要引入這樣一個(gè)設(shè)計(jì)模式呢?原因其實(shí)很簡(jiǎn)單,全局靜態(tài)變量會(huì)造成資源浪費(fèi):假設(shè)這個(gè)類非常消耗資源,程序在運(yùn)行過程中,不是每一次都用到這個(gè)類,那就是極大的浪費(fèi)。

類圖

類圖不是目的,僅僅幫助理解

[圖片上傳失敗...(image-1fb5c0-1527174223215)]

單例模式的類圖很簡(jiǎn)單,只有一個(gè)類,有一個(gè)代表自己實(shí)例的instance變量,還有一個(gè)提供全局訪問的靜態(tài)方法getInstance()。

以下的代碼和思路是針對(duì)Java語言

單例類型

單例模式分為懶漢式和餓漢式,區(qū)別在于實(shí)例化單例對(duì)象的時(shí)機(jī)。

懶漢式

在懶漢式單例模式實(shí)現(xiàn)中,不管單例是否用到,都會(huì)實(shí)例化一個(gè)單例對(duì)象。典型的寫法如下:

/**
 * 單例
 * Created by Carlton on 2016/11/21.
 */
class Singleton private constructor()
{
    companion object
    {
        private val instance = Singleton()
        fun instance() = instance
    }
}

因?yàn)?code>Kotlin和Java在靜態(tài)語法上的不一致,后面的代碼都用Java來實(shí)現(xiàn)方便理解

/**
 * 單例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {
        
    }

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

餓漢式非常簡(jiǎn)單,也不會(huì)出現(xiàn)資源占用之外的其他問題,就不多說。

飽漢式、常規(guī)方法

飽漢式也就是常規(guī)實(shí)現(xiàn)方式比較復(fù)雜,原因是我們用到類實(shí)例的時(shí)候才會(huì)去實(shí)例化,這中間會(huì)出現(xiàn)各種各樣的情況。

介紹了兩種實(shí)現(xiàn)方式,接下來我們實(shí)現(xiàn)一個(gè)常規(guī)的單例模式:

/**
 * 單例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {

    }

    private static Singleton instance = null;
    public static Singleton instance()
    {
        if(instance == null)
        {
            // 1
            instance = new Singleton();
        }
        return instance;
    }
}

在客戶端獲取單例的時(shí)候,檢查對(duì)象是否是null如果是,則實(shí)例化一個(gè),如果不是則直接返回已有的對(duì)象,如果在單線程的情況下,確實(shí)如此,現(xiàn)在如果,兩個(gè)或者兩個(gè)以上的線程就有問題了:

  • 如果兩個(gè)線程都到1這個(gè)位置
  • 那么現(xiàn)在的情況就是if (instance == null)判斷的時(shí)候兩個(gè)線程都通過了
  • 這個(gè)時(shí)候instance會(huì)實(shí)例化兩次,這兩個(gè)線程拿到的不是同一個(gè)實(shí)例

怎么解決這個(gè)問題呢?不慌解決,先看看一下雙重驗(yàn)證和volatile

雙重檢查和volatile

如果加上線程鎖,好像問題就解決了,先看看加線程鎖怎么寫:

/**
 * 單例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
    private Singleton()
    {

    }

    private static Singleton instance = null;
    public static Singleton instance()
    {
        if(instance == null)
        {
            // 1
            synchronized (Singleton.class)
            {
                // 2
                if(instance == null)
                { // 3
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

現(xiàn)在看看多線程的情況程序會(huì)出現(xiàn)什么問題:

  • 如果有兩個(gè)線程都到了1
  • 因?yàn)橥芥i的原因,只有一個(gè)線程可以先進(jìn)入到2
  • 當(dāng)?shù)谝粋€(gè)線程進(jìn)入3實(shí)例化一個(gè)instance后,第二個(gè)線程進(jìn)入判斷的時(shí)候,就不會(huì)進(jìn)入3
    這就是雙重驗(yàn)證,在C/C++中,這樣做是沒有問題的,但是:雙重檢查對(duì)Java語言編譯器不成立!原因在于,Java編譯器中,Singleton類的初始化與instance變量賦值的順序不可預(yù)料,如果一個(gè)線程在沒有同步化的條件下讀取instance引用,并調(diào)用這個(gè)對(duì)象的方法的話,可能會(huì)發(fā)現(xiàn)對(duì)象的初始化過程還沒有完成,從而造成崩潰。

可能有人會(huì)覺得volatile可以解決問題,修改變量申明:

private static volatile Singleton instance = null;

先看看volatile是什么?

volatile變量具有synchronized的可見性特性,但是不具備原子特性。這就是說線程能夠自動(dòng)發(fā)現(xiàn)volatile變量的最新值。volatile變量可用于提供線程安全,但是只能應(yīng)用于非常有限的一組用例:多個(gè)變量之間或者某個(gè)變量的當(dāng)前值與修改后值之間沒有約束。

通過這個(gè)描述知道volatile是一個(gè)輕量級(jí)的線程同步,之前出現(xiàn)的問題在于線程沒有同步化的條件下讀取instance,現(xiàn)在加上volatile問題就解決了。但是:JDK1.5之前,這樣使用雙重檢查還是有問題。

Java中如何正確的實(shí)現(xiàn)單例模式

說了這么多,如何才能正確實(shí)現(xiàn)單例模式呢?

  • 使用餓漢式
  • JDK1.5以后使用帶volatile修飾的雙重檢查
  • 同步鎖加到方法上:
/**
 * 單例模式
 * Created by Carlton on 2016/11/21.
 */
public class Singleton
{
        private static volatile Singleton instance = null;
        private Singleton()
        {
        }
        public static synchronized Singleton instance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
        return instance;
        }
}

多說幾句,如果把同步鎖加到方法上面,代表這個(gè)方法同一時(shí)間只有一個(gè)線程能夠進(jìn)入方法,這個(gè)時(shí)候后面的線程進(jìn)入就會(huì)正常的直接返回instance實(shí)例。

總結(jié)

單例模式在思路上是很簡(jiǎn)單的模式,也就不提供例子,單例模式還有很多單例模式的變種,但是核心沒變:一個(gè)類只有一個(gè)實(shí)例;這個(gè)實(shí)例由自己來實(shí)例化;單例模式?jīng)]有提供公共的構(gòu)造函數(shù),所以其他類不能對(duì)其實(shí)例化。需要注意的是,這個(gè)模式的復(fù)雜點(diǎn)在于實(shí)現(xiàn)方式,如何才能保證在各種情況下只有一個(gè)類實(shí)例才是關(guān)鍵點(diǎn)。

??查看更多??

不登高山,不知天之高也;不臨深溪,不知地之厚也
感謝指點(diǎ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)容

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