記一次JVM指令重排引發的線程問題

問題

多個線程同時讀一個HashMap,有一條線程會定時獲取最新的數據,構建新的HashMap並將舊的引用指向新的HashMap,這是一種相似CopyOnWrite(寫時複製)的寫法,主要代碼以下,在不要求數據實時更新(容忍數據不是最新),而且各個線程之間容忍看不到對方的最新數據的狀況下,這麼這種寫法安全嗎?html

public class myClass {
    private HashMap<String,String> cache = null;
    public void init() {
        refreshCache();
    }
    // this method can be called occasionally to update the cache.
    public void refreshCache() {
        HashMap<String,String> newcache = new HashMap<String,String>();
        newcache.put("key", "value"); // code to fill up the new cache
        // and then finally
        cache = newcache; //assign the old cache to the new one in Atomic way
    }
}
複製代碼

解答

這種寫法並不安全,HashMap須要聲明爲volatile後纔是安全的java

延伸

不少資料都會介紹volatile是易失性的,各個線程能夠實時看到volatile變量的值,這種解釋雖然沒錯可是不完整,容易誤導開發人員(包括本文遇到的問題),同時這種解釋沒有深刻到JVM的happen-before,建議你們少看這種解釋安全

JVM的會對指令進行優化重排,雖然書寫順序是A先於B,但可能執行結果是B先於A,要避免這種問題就要用到happen-beforeapp

happen-before有如下八大原則:優化

  • 單線程happen-before原則:在同一個線程中,書寫在前面的操做happen-before後面的操做。
  • 鎖的happen-before原則:同一個鎖的unlock操做happen-before此鎖的lock操做。
  • volatile的happen-before原則:對一個volatile變量的寫操做happen-before對此變量的任意操做(固然也包括寫操做了)。
  • happen-before的傳遞性原則:若是A操做 happen-before B操做,B操做happen-before C操做,那麼A操做happen-before C操做。
  • 線程啓動的happen-before原則:同一個線程的start方法happen-before此線程的其它方法。
  • 線程中斷的happen-before原則:對線程interrupt方法的調用happen-before被中斷線程的檢測到中斷髮送的代碼。
  • 線程終結的happen-before原則:線程中的全部操做都happen-before線程的終止檢測。
  • 對象建立的happen-before原則:一個對象的初始化完成先於他的finalize方法調用。

被聲明爲volatile的變量知足happen-before原則,對此變量的寫操做老是happen-before於其餘操做,因此纔會出現其餘文章關於volatile對其餘線程可見的解釋,可見是表象,happen-before纔是緣由this

在本文的問題中,若是沒有volatile, 不知足happen-before的原則,JVM會對指令進行重排,cache = newcache可能先於newcache.put("key", "value"),若是此時其餘線程讀取了HashMap,就會找不到數據,換句話說這種寫法是線程不安全的.atom

教訓

對於線程問題,水平不夠或者理解不夠深刻,仍是乖乖用JDK提供的實現類,這些類都是通過千錘百煉出來的,遠遠沒有看上去簡單spa

參考資料

相關文章
相關標籤/搜索