天天看點

java 多線程下race condition問題

這個問題的讨論來自内部的一個關于“多線程環境下使用Hashmap的安全問題”的讨論,HashMap多線程的問題之前已經提過一次,見之前的blog.本篇文章主要讨論多線程下race condition的問題。以下内容部分引用自内部郵件:

錯誤代碼:

private static Map cachedMap = new HashMap();
private static Boolean firstInvoke = true;
程式是設想在第一次開始對該map變量進行初始化
線程:
Public Object getMyValue(){
     If(firstInvoke){
        While(i<){
            …………
            cachedMap.put("new","newValue");
            i++;
        }
        firstInvoke = false;
    }
}

線程:
線上程對cachedMap對象put的時候,線程從這個cachedMap中取值
cachedMap.get("new");
           

錯誤分析

單步Debug是沒問題,但代碼在多線程情況下工作會出現線程安全。 Hashmap不是讀寫線程安全的,隻有全部隻讀才是線程安全的,Hashmap在被并發讀寫使用的時候會出現線程安全問題,一般了解的線程安全問題導緻的是資料錯誤。 而Hashmap多線程同時讀寫操作時,可能使程式挂起。

以下引用自http://sdh5724.javaeye.com/blog/619130

分析: 我們知道Hashmap在被并發讀寫使用的時候, 會抛出ConcurrentModificationException這個異常, 但是JDK文檔明确指出, 這個異常抛出是屬于 fail-fast 的一個設計方法, 目的是為了開發者能及早的意識到線程安全問題發生。 但是, 這個fail-fast不是一定會發生, 而是可能會發生的行為。 是以, 在一個不确定狀态下的下,jvm線程發生持續100%cpu行為是比較容易了解了(for (Entry

“正确用法”

注意更改HashMap中的内容時是否存在同時并發線程讀的情況,如果有, 需要對讀寫的入口做同步. 如果知道要在多線程情況下讀寫Map, 建議使用線程安全的ConcurrentHashMap實作代替HashMap。ConcurrentHashMap 可以在不損失線程安全的同時提供很好的并發性。

代碼如下:

private static Map cacheMap = new ConcurrentHashMap();
private static Boolean firstInvoke = true;
程式是設想在第一次開始對該map變量進行初始化
線程:
Public Object  getMyValue(){
     If(firstInvoke){
        While(i<){
            …………
            cachedMap.put("new","newValue");
            i++;
        }
        firstInvoke = false;
    }
}
線程:
線上程對cachedMap對象put的時候,線程從這個cachedMap中取值
cachedMap.get("new");
           

上述解決方案的race condition問題:

這個HashMap不當使用的問題很經典。很多時候我們用“單線程”思維習慣去寫代碼,不知不覺就忘記了運作時的多線程場景。

其實,我覺得下面的例子中還是有隐含的race condition問題的,那就是在這個if(firstInvoke) then load data and firstInvoke=false這個邏輯中。

即:If(firstInvoke){… //ß 這裡可能會導緻多條線程同時進入,導緻多次load data

通常我們用一個boolean變量來實作lazy操作, 那麼在多線程環境下,要記得使用synchronize關鍵詞 或者 采用volatile類型變量+CAS操作,確定變量被每條線程都能正确的讀取和寫入。

1. 保險的做法:(在最新JVM中,這種方式是最安全,最可讀,成本效益最高的,如果JVM支援鎖逃逸即Biased Locking,性能也會非常好)

Synchronized(lock){
    If(firstInvoke){
        Then load data…
        firstInvoke = false
    }
}
           

2. 或者,用volatile變量+DCL

Private volatile boolean firstInvoke = true;

If(firstInvoke){

    Synchronized(lock){

        If(firstInvoke){

            Then load data …

            firstInvoke = false;

        }

    }

}
           

3. SMP友好,但是偷懶的做法,用AtomicBoolean,裡面用到了CompareAndSet操作。(volatile隻保證變量可見性,Spinning CAS保證操作原子性)

Private AtomicBoolean firstInvoke = new AtomicBoolean(true);

If(firstInvoke.getAndSet(false)){ // cas spinning inside the AtomicBoolean::getAndSet() method

    Then load  data…

}
           

4. 最後,最複雜,但是同時滿足SMP友好,及性能最佳的:

private AtomicBoolean firstInvoke = new AtomicBoolean(true);

for(;;){

    Boolean current = firstInvoke.get();

    If(!current){     // the most likely condition branch, see http://pt.alibaba-inc.com/wp/dev_related/optimization_363/likely-unlikely.html

        Break;

    }

    If(firstInvoke.compareAndSet(current,false){

        Then load data…

        Break;

    }

}
           

在××××代碼中,為了確定SMP狀态下性能最優,我們在某一些關鍵地方也用到了上面的CAS+spinning的技巧。

我們也許并不會時時刻刻用到“回字的四種寫法”,但是搞清楚JVM記憶體可見性和操作原子性的基本概念還是必須的,這也是確定寫出線程安全代碼的前提條件)。

參考資料:

http://sdh5724.javaeye.com/blog/619130

http://www.tech-faq.com/race-condition.html

《 The Art of Multiprocessor Programming》 http://book.douban.com/subject/3024605/

相關資料:

Java輕量級鎖原理詳解(Lightweight Locking)

Java偏向鎖實作原理(Biased Locking)

深入了解DCL(雙檢鎖)的安全性

轉載自:http://www.cnblogs.com/redcreen/archive/2011/03/29/1999032.html

繼續閱讀