天天看點

多線程之volatile關鍵字

每個線程都運作在java棧記憶體中,每個線程都有自己的工作記憶體。線程的計算一般是通過工作記憶體進行互動的。如圖所示:

多線程之volatile關鍵字
多線程之volatile關鍵字
多線程之volatile關鍵字

從上圖中我們可以看到,線程在初始化時從主記憶體中加載所需的變量值到工作記憶體中,然後線上程運作時,如果是讀取,則直接從工作記憶體中讀取,如果是寫入則先寫到工作記憶體中,之後再重新整理到主記憶體中,這個可以看做是jvm的一個簡單的記憶體模型,但是這樣的結構在多線程的情況下有可能會出問題。比如:a線程修改變量的值,也重新整理到主記憶體中了,但是此時b、c線程讀取的還是本線程的工作記憶體,也就是它們讀取的不是最新的值,此時就會出現不同線程持有的公共資源不同步的情況。

對于此類問題:我們可以使用synchronized關鍵字來同步代碼塊,也可以使用lock鎖來解決該問題,不過java可以使用volatile關鍵字更簡單的解決此類問題。比如在一個變量前加上volatile關鍵字。。可以確定每個線程對本地變量的通路和修改都是直接與主記憶體互動的,而不是與本線程的工作記憶體互動的。保證每個線程都能獲得最“新鮮”的變量值。如下圖所示:

多線程之volatile關鍵字

但是此時我們需要注意的是:volatile變量隻能保證線程取的是最新的值,但是并不能保證資料的同步性。兩個線程同時修改一個volatile,有可能會産生髒資料。請看代碼的例子:

這段代碼的邏輯是這樣的:

啟動1000個線程,修改共享資源count的值。

休眠15毫秒,讓活動線程數變為1(這裡有一個monitor ctrl break線程)。

判斷實際值和理想值是否一緻,如果不一緻則此時出現髒資料

我們先來看看自加的操作:count++這句話可以分為兩部分,先取出count的值,再執行加1的操作。是以在某兩個緊鄰的時間片段内可能會出現下面的事情:

1、第一個時間片段:

a線程獲得執行機會,因為有關鍵字volatile修飾,是以它從主記憶體中獲得count的最新值98,記下來的事情又分為兩種類型:

如果是單cpu,此時排程器暫停a線程,讓出執行幾乎給b線程,于是b線程也獲得了count的最新值98。

如果是多cpu,此時線程a繼續執行,而線程b也同時獲得count的最新值98。

2、第二個時間片段:

如果是單cpu,b線程執行完加1動作(原子操作),count的值為99:。由于是volatile類型的變量,是以直接寫入主記憶體,然後a線程繼續執行,計算的結果也是99:,重新寫入主記憶體中。

如果是多cpu,a線程執行完加1動作後修改主記憶體的變量count為99:,線程b執行完畢後也修改住記憶體中的變量為99:

當這兩個時間片執行完畢後,原本期望的結果為100,單運作後的值卻為99:,這表示出現了線程不安全的情況,這也證明了,volatile關鍵字并不能保證線程安全,它隻是能保證當線程需要該變量的值時能夠擷取到最新的值,不能保證多個線程修改的安全性。

參考自:編寫高品質代碼 改善java程式的151個建議。