1. 程式人生 > 程式設計 >記一次JVM指令重排引起的執行緒問題

記一次JVM指令重排引起的執行緒問題

問題

多個執行緒同時讀一個HashMap,有一條執行緒會定時獲取最新的資料,構建新的HashMap並將舊的引用指向新的HashMap,這是一種類似CopyOnWrite(寫時複製)的寫法,主要程式碼如下,在不要求資料實時更新(容忍資料不是最新),並且各個執行緒之間容忍看不到對方的最新資料的情況下,這麼這種寫法安全嗎?

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後才是安全的

延伸

很多資料都會介紹volatile是易失性的,各個執行緒可以實時看到volatile變數的值,這種解釋雖然沒錯但是不完整,容易誤導開發人員(包括本文遇到的問題),同時這種解釋沒有深入到JVM的happen-before,建議大家少看這種解釋

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

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才是原因

在本文的問題中,如果沒有volatile,不滿足happen-before的原則,JVM會對指令進行重排,cache = newcache可能先於newcache.put("key","value"),如果此時其他執行緒讀取了HashMap,就會找不到資料,換句話說這種寫法是執行緒不安全的.

教訓

對於執行緒問題,水平不夠或者理解不夠深入,還是乖乖用JDK提供的實現類,這些類都是經過千錘百煉出來的,遠遠沒有看上去簡單

參考資料