double check 解決單例模式的多線程並發問題


 

  最近被多線程問題(multi-thread issue)弄昏了頭。以前雖然也知道系統里要考慮多線程問題,也無數次見到double-check的代碼,但是由於自己碰到這方面的問題基本上就是從其他地方拷貝一份現成的代碼,改吧改吧,也一直沒有遇到多線程帶來的bug,所以就沒有留心。知道年前,一份兩三個月前寫的代碼出現了由於多線程帶來的bug,最近寫的代碼在code review中又被師兄批評沒有考慮多線程問題,這才專門去補了補這方面的知識,現在就小結一下multi-thread問題以及在其中經常會用到的double-check。
    在一個多線程程序中,如果共享資源同時被多個線程使用,就有可能會造成多線程問題,這主要取決於針對該資源的某項操作是否是線程安全的。例如,.Net中的dictionary就是一個完全線程不安全的數據結構,對於dictionary的插入、刪除都有可能帶來多線程問題,這主要是由於dictionary的內部實現結構會頻繁的由於插入、刪除操作而改變長度,這時,如果出現多線程問題,程序最可能拋出數組越界的Exception。特別的,對於WebService來講,每一個請求都會生成一個thread/instance,因此就要特別注意多線程問題了。
    一般地,多線程問題常常發生於對於共享資源的同時使用。例如,對於類的成員變量的使用,對於全局靜態變量的使用,而對於函數內部的局部變量而言,一般式不會存在多線程問題的,因為每個線程在調用一個特定的函數時,都會生成一份函數內部成員變量的副本,線程和線程之間是互不相干的。
    解決多線程問題,最常見的方式就是加鎖,使得某一資源在同一時刻只能被一個線程所用,而其他線程則必須在被加鎖的代碼外等待,直到鎖被解除,例如如下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;
    }
這樣,上面的問題就徹底解決了~
 
文章來源 http://blog.sina.com.cn/s/blog_597a437101011o66.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM