爲何單例模式須要double check

 最近被多線程問題(multi-thread issue)弄昏了頭。之前雖然也知道系統裏要考慮多線程問題,也無數次見到double-check的代碼,可是因爲本身碰到這方面的問題基本上就是從其餘地方拷貝一份現成的代碼,改吧改吧,也一直沒有遇到多線程帶來的bug,因此就沒有留心。知道年前,一份兩三個月前寫的代碼出現了因爲多線程帶來的bug,最近寫的代碼在code review中又被師兄批評沒有考慮多線程問題,這才專門去補了補這方面的知識,如今就小結一下multi-thread問題以及在其中常常會用到的double-check。編程

    在一個多線程程序中,若是共享資源同時被多個線程使用,就有可能會形成多線程問題,這主要取決於針對該資源的某項操做是不是線程安全的。例如,.Net中的dictionary就是一個徹底線程不安全的數據結構,對於dictionary的插入、刪除都有可能帶來多線程問題,這主要是因爲dictionary的內部實現結構會頻繁的因爲插入、刪除操做而改變長度,這時,若是出現多線程問題,程序最可能拋出數組越界的Exception。特別的,對於WebService來說,每個請求都會生成一個thread/instance,所以就要特別注意多線程問題了。c#

    通常地,多線程問題經常發生於對於共享資源的同時使用。例如,對於類的成員變量的使用,對於全局靜態變量的使用,而對於函數內部的局部變量而言,通常式不會存在多線程問題的,由於每一個線程在調用一個特定的函數時,都會生成一份函數內部成員變量的副本,線程和線程之間是互不相干的。設計模式

    解決多線程問題,最多見的方式就是加鎖,使得某一資源在同一時刻只能被一個線程所用,而其餘線程則必須在被加鎖的代碼外等待,直到鎖被解除,例如以下c#代碼所示:數組

lock(_lock) 安全

{數據結構

    //do something to the shared resources.多線程

}編程語言

    下面說說double-check。函數

    多線程問題也經常和一種lazy-initialize的設計模式聯繫在一塊兒。在這裏就會慢慢引出double-check。lazy-initialize講的是,對於一些特別複雜的對象,讓程序在第一次調用它的時候再對它進行初始化,並且保證僅僅初始化一次。性能

    首先想到的設計是這樣的:

Class A

{

    private ComplexClass _result = null;

    public ComplexClass GetResult()

    {

         if(_result == null)

         {

              _result = new ComplexClass();

         }

         return _result;

    }

}

    可是這樣有一個問題。ComplexClass的構造過程較長的話,當第一個線程還在進行ComplexClass構造的時候,_result多是null,也可能指向了一個還沒有初始化完成的對象。這樣,要麼兩個線程初始化了兩次ComplexClass,要麼第二個線程會返回一個指向不完整對象的引用。因此,在這裏須要用到一個鎖,以下所示:

    ComplexClass GetResult()

    {

        lock(_lock)

        {

             if(_result == null)

             {

                  _result = new ComplexClass();

             }

         }         

         return _result;

    }

    這樣,雖然多線程的問題解決了,可是每一次須要使用result時都會請求鎖,而請求鎖對程序的性能是有很大影響的,所以咱們在lock的外面再加一層check:

    ComplexClass GetResult()

    {

        if(_result == null)

        {

            lock(_lock)

            {

                 if(_result == null)

                 {

                      _result = new ComplexClass();

                 }

             }  

         }       

         return _result;

    }

    這樣,對於全部初始化完成後的請求,就都不用請求鎖,而是直接返回_result。

    可是仍是存在一點問題。對於一些編程語言來講,_result = new ComplexClass();這句代碼會使得_result指向一個部分初始化的對象。也就是說,當線程A在初始化ComplexClass時,線程B有可能會判斷_result已經不是null了,而這時其實初始化還沒有完成,這時線程B就直接返回了一個部分初始化的對象,會形成程序的崩潰。那麼,這個問題怎麼解決呢?通常的解決方法是在程序內部再加一個局部變量(標識變量)作一層緩衝:

    ComplexClass GetResult()

    {

        ComplexClass result;

        if(_result == null)

        {

            lock(_lock)

            {

                 if(_result == null)

                 {

                      result = new ComplexClass();

                      _result = result;

                 }

             }  

         }       

         return _result;

    }

這樣,上面的問題就完全解決了~

相關文章
相關標籤/搜索