Java Thread多線程volatile問題

問題

當多個線程併發同時進行set、get時,其它線程能否感知到flag的變化

public class ThreadSafeCache {

    boolean flag = true;//默認設置true

    public boolean isFlag() {
        return flag;
    }

    public synchronized ThreadSafeCache setFlag(boolean flag) {
        this.flag = flag;
        return this;
    }

    public static void main(String[] args) {
        ThreadSafeCache threadSafeCache = new ThreadSafeCache();
        //循環創建多個線程
        for (int i = 0;i < 10;i++){
            new Thread(() -> {
              int j = 0;
              while(threadSafeCache.isFlag()){
                  j++;
              }
              System.out.println(j);
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadSafeCache.setFlag(false);
    }
}

運行結果

未添加volatile運行結果

可以看到程序是卡死了,一直沒有退出

分析

這個類非常簡單,裏面有一個屬性,有兩個方法,set、get,並且在set方法上添加了synchronized。

多線程併發的同時進行set、get操作,A線程調用set、B線程調用get能感知到flag發生變化嗎?

說到這裏,問題就變成了synchronized能否保證上下文可見性!!!

關鍵詞synchronized的用法

  • 指定加鎖對象:對給定的對象進行加鎖,進入同步代碼前需要獲得給定對象的鎖。
  • 直接作用於實例方法:相當於對當前對象的實例加鎖,進入同步代碼前需要獲得當前對象實例的鎖
  • 直接作用於靜態方法:相當於對當前類進行加鎖,進入同步代碼前需要獲得當前類的鎖。

從代碼中,我們可以看到只對set方法加了同步鎖,多個線程調用set方法時,由於存在鎖,會一個一個的進行set,但對於get來說,並沒有加鎖,多個線程無需獲得該實例的鎖,就可以直接獲取到flag的值,那麼我們就需要考慮某一個線程set之後的flag對其它線程是否可見!!!

Java內存模型happens-before原則

JSR-133內存模型使用happens-before原則的概念來闡述操作之間的內存可見性。在JMM(JAVA Memory Model)中,如果一個執行的結果需要對另一個操作可見,那麼這兩個操作直接必須要存在happens-before關係。兩個操作可以是同一個線程內的也可以是不同線程中的。

happens-before(之前發生)原則

  • 程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
  • 監視器鎖規則:對一個監視器的解鎖,happens-before於隨後對這個監視器的加鎖。
  • volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile的讀。
  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
  • 線程啓動規則:Thread對象的start方法先行發生於此線程的每一個動作。
  • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
  • 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值的手段檢測到線程是否已經終止執行。
  • 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始。

注意:兩個操作之間存在happens-before關係,並不一定前一個操作必須要在後一個操作執行!!!

happens-before僅僅要求前一個操作的執行結果對後一個操作可見,且前一個操作的執行順序排在後一個操作之前(因爲java虛擬機重排不相關的指令)。

volatile

volatile可見性

前面的happens-before原則中提到了volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。因此,volatile保證了多線程下的可見性!!!

volatile禁止內存重排序

下面是JMM針對編譯器制定的volatile重排序規則:

是否能重排序 第二個操作
第一個操作 普通讀/寫 volatile讀 volatile寫
普通讀/寫 NO
volatile讀 NO NO NO
volatile寫 NO NO

通過上面的分析我們添加關鍵字volatile來試試
添加volatile運行結果

結論

多線程併發的同時進行set、get操作,A線程調用set方法,B線程並不一定能對這個改變可見,上面的代碼中,如果get也添加synchronized也是可見的,還是happens-before的監視器規則:對一個監視器的解鎖,happens-before於隨後對這個監視器的加鎖。只是volatile對比synchronized更輕量級,所以本例使用volatile,但是對於符合非原子操作i++這裏還是不行的,還得用synchronized。

不過使用volatile也會限制一些調優

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章