雙 IF 魔符

來源:互聯網
上載者:User

轉自IBM developerWorks 中國

如果您發現您的代碼 99.99% 的時間在單 CPU 上運行,但是當您按比例增加到兩個或更多個 CPU 時,它很快就會崩潰,那麼這一珍品正適合您。

這一問題不僅與 Java 代碼在一個對稱式多處理器(symmetric multiple processor,SMP)平台上啟動並執行複雜程度有關,還與管道技術對“受保護的(protected)”代碼的影響有關。儘管您的代碼應該高效而且一致始終都很重要,但是由於 SMP 這樣的平台將誇大儲存模型中的一致性問題,因此,當您的應用程式在 SMP 平台上運行時尤其是如此。

雖然雙 if 子句是解決多線程應用程式所帶來的問題的一般方法,但它使您遇到“弱一致性”模型(CPU 管道技術、預測執行(speculative execution)等等)所帶來的問題(尤其是關於 SMP 系統)。因此,這個珍品簡而言之就是:如果您的應用程式將在 SMP 系統上運行,那麼請不要在您的代碼中使用雙 if 邏輯。現在,讓我們仔細看一下這個問題的原因和機理。

雙 if 邏輯
首先,請考慮一下把指標指向資源的下列代碼,其中 flag 表示有無該資源:

IF flag == 0                                    
{                                               
    // no resource                            
    set flag                                  
    acquire resource                          
    set pointer to resource                   
}                                               
ELSE                                            
    set pointer to resource                   

如果這段代碼是可重新進入的(即,可以由多個線程運行它),那麼它是非常危險的。請考慮一下如果一個線程在 if 子句的中途,此時另外一個線程發現 flag 已經被設定了,於是它就試圖去設定一個指標指向第一個線程還未分配的資源,結果會怎樣呢?幸運的是,這個問題很容易糾正,如下所示:

IF flag == 0                                    
{                                               
    // no resource                            
    acquire resource           ----- A  
    set pointer to resource                   
    set flag                                  
}                                               
ELSE                                            
    set pointer to resource          

但是,現在我們又發現另外一個問題:一個線程阻塞在 A 點,另一個線程卻因發現 flag 被設為 0 而繼續執行。在這種情況下,這兩個線程都執行同一段代碼分配資源 — 這根本不是我們所希望的!然而,解決方案又是眾所周知:我們只要在執行擷取資源的代碼時,封鎖所有其它線程,如下所示:

ENTER LOCK                                   
IF flag == 0                                 
{                                            
    // no resource                         
    acquire resource                       
    set pointer to resource                
    set flag                               
}                                            
ELSE                                         
    set pointer to resource                
LEAVE LOCK                                

(請注意我使用了 LOCK 這一術語來保持樣本簡單。當然,在 Java 代碼中是用 synchronize 子句內的同步代碼來表示它)。

現在我們的代碼是安全執行緒的,但還不是高效的。擷取鎖(或 Java 術語中的管程(monitor))需要很多時間,而且鎖中的代碼比我們需要的要多。為了糾正這一點,我們把代碼改寫成下面這樣:

IF flag == 0                                 
{                                            
    // no resource        ----- A    
    ENTER LOCK                             
    acquire resource                       
    set pointer to resource                
    set flag                               
    LEAVE LOCK                             
}                                            
ELSE                                         
    set pointer to resource       

這幾乎奏效了,但是我們又引入了前面的問題,一個線程在 A 點被中止,而另一個線程插入進來,因此造成了 CPU 的混亂。為了糾正這一點,我們使用著名的雙 if 子句:

IF flag == 0 

// no resource        ----- A     
ENTER LOCK                                
    IF flag == 0                              
        acquire resource              
        set pointer to resource       
        set flag                      
    ELSE                               
        set pointer to resource             
LEAVE LOCK                          
}                                         
ELSE                                      
    set pointer to resource

通過添加雙 if 子句,我們已經盡了最大努力使代碼安全執行緒而且高效。許多進階編程技術方面的書中都推薦使用雙 if 邏輯來解決線程爭用。但是雙 if 邏輯在多個線程可以同時執行的 SMP 機器上並不安全。

您會說,可是某一時刻在“鎖住的”那部分代碼中只會有一個線程,那麼會出什麼問題呢?很多!這就是把這一技巧叫做“雙 if 魔符”的原因所在。

雙 if 魔符請考慮一下管道技術對上面這段代碼的影響:

由於 CPU 看不到任何依賴,所以可以以任何次序執行 if 子句裡的代碼。這會影響到下面用 * 標出的指令。

如果 flag 被設為 0,那麼與先計算“flag”再計算 flag 非 0 情況下的指標相比,計算指標(可能會是垃圾)並拋掉它可能是更高效的做法。所以,CPU 可以採用管道技術處理下面用 ** 標出的指令。
以下又是這些代碼,為說明上述觀點而加上了標記:

**IF flag == 0                                 
{                                            
    // no resource                         
    ENTER LOCK                             
    IF flag == 0                           
        *acquire resource                 
        *set pointer to resource          
        *set flag                         
    ELSE                                   
        set pointer to resource          
    LEAVE LOCK                             
}                                           
ELSE                                         
    **set pointer to resource          

儘管這似乎有點奇怪,但上面的代碼可能會象下面這樣執行:

**set pointer to resource                            
**IF flag == 0                                       
{                                                  
    // no resource                               
    ENTER LOCK                                   
    set pointer to resource                      
    IF flag == 0                                 
        *set flag                               
        *acquire resource        ----- A  
        *set pointer to resource                
    ELSE 
        
    LEAVE LOCK                                   
}                                                  
ELSE 

正如您所見到的,我們很敏感的代碼仍在鎖中,但是現在在分配資源之前設定 flag。因此,請想象一下線程 A 正在 A 點附近執行,此刻另外一個線程到來了。在 SMP 機器上,第二個線程可以在另一個 CPU 上與第一個線程同時執行;這個線程看到 flag 沒有被設定成 0(因為它在鎖的外部),它可以繼續把指標指向未分配資源 — 上當了!

此外,即使代碼按我們所寫的執行,我們仍然沒有免除災難,原因在於 CPU 可以採用管道技術處理以上用 ** 標出的代碼:

線程 A 進入鎖住的那部分代碼。

線程 B 對第一個 if 語句求值並計算指標的值(在最後的 else 子句中)。

線程 B 得到“指標”(由於線程 A 還沒有指定它,所以是垃圾)的值。

線上程 A 完成並解鎖的同時,線程 B 將計算第一個 if 的值。

解鎖導致“flag”的值被清除(意味著線程 B 在它的快取中具有的任何值都是無效的)。

線程 B 計算好第一個 if 子句的值,當然,現在它發現值為 true。

現線上程 B 使用前面那個是垃圾的指標值。

這就是 Java 雙 if 魔符。雖然其它的語言(比如 C/C++)讓您通過使用語言本機功能強制執行的次序來解決這個問題,Java 語言卻不行。為了安全的執行上述樣本,您必須把所有代碼封裝在一個 synchronize 子句中,或者尋找另外一種寫法。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.