volatile關鍵字比較少用,原因無外乎兩點,一是在Java1.5之前該關鍵字在不同的操作系統上有不同的表現,所帶來的問題就是移植性較差;而且比較難設計,而且誤用較多,這也導致它的"名譽" 受損。
我們知道,每個線程都運行在棧內存中,每個線程都有自己的工作內存(Working Memory,比如寄存器Register、高速緩衝存儲器Cache等),線程的計算一般是通過工作內存進行交互的,其示意圖如下圖所示:
從示意圖上我們可以看到,線程在初始化時從主內存中加載所需的變量值到工作內存中,然後在線程運行時,如果是讀取,則直接從工作內存中讀取,若是寫入則先寫到工作內存中,之後刷新到主內存中,這是JVM的一個簡答的內存模型,但是這樣的結構在多線程的情況下有可能會出現問題,比如:A線程修改變量的值,也刷新到了主內存,但B、C線程在此時間內讀取的還是本線程的工作內存,也就是說它們讀取的不是最"新鮮"的值,此時就出現了不同線程持有的公共資源不同步的情況。
對於此類問題有很多解決辦法,比如使用synchronized同步代碼塊,或者使用Lock鎖來解決該問題,不過,Java可以使用volatile更簡單地解決此類問題,比如在一個變量前加上volatile關鍵字,可以確保每個線程對本地變量的訪問和修改都是直接與內存交互的,而不是與本線程的工作內存交互的,保證每個線程都能獲得最"新鮮"的變量值,其示意圖如下:
明白了volatile變量的原理,那我們思考一下:volatile變量是否能夠保證數據的同步性呢?兩個線程同時修改一個volatile是否會產生髒數據呢?我們看看下面代碼:
上面的代碼定義了一個多線程類,run方法的主要邏輯是共享資源count的自加運算,而且我們還為count變量加上了volatile關鍵字,確保是從內存中讀取和寫入的,如果有多個線程運行,也就是多個線程執行count變量的自加操作,count變量會產生髒數據嗎?想想看,我們已經為count加上了volatile關鍵字呀!模擬多線程的代碼如下:
想讓volatite變量"出點醜",還是需要花點功夫的。此段程序的運行邏輯如下:
啟動100個線程,修改共享資源count的值
暫停15秒,觀察活動線程數是否為1(即只剩下主線程再運行),若不為1,則再等待15秒。
-
判斷共享資源是否是不安全的,即實際值與理想值是否相同,若不相同,則發現目標,此時count的值為髒數據。
如果沒有找到,繼續循環,直到達到最大循環為止。
運行結果如下:
循環到:40 遍,出現線程不安全的情況
此時,count= 999
這只是一種可能的結果,每次執行都有可能產生不同的結果。這也說明我們的count變量沒有實現數據同步,在多個線程修改的情況下,count的實際值與理論值產生了偏差,直接說明了volatile關鍵字並不能保證線程的安全。
在解釋原因之前,我們先說一下自加操作。count++表示的是先取出count的值然後再加1,也就是count=count+1,所以,在某個緊鄰時間片段內會發生如下神奇的事情:
(1)、第一個時間片段
A線程獲得執行機會,因為有關鍵字volatile修飾,所以它從主內存中獲得count的最新值為998,接下來的事情又分為兩種類型:
如果是單CPU,此時調度器暫停A線程執行,讓出執行機會給B線程,於是B線程也獲得了count的最新值998.
如果是多CPU,此時線程A繼續執行,而線程B也同時獲得了count的最新值998.
(2)、第二個片段
如果是單CPU,B線程執行完+1操作(這是一個原子處理),count的值為999,由於是volatile類型的變量,所以直接寫入主內存,然後A線程繼續執行,計算的結果也是999,重新寫入主內存中。
如果是多CPU,A線程執行完加1動作後修改主內存的變量count為999,線程B執行完畢後也修改主內存中的變量為999
這兩個時間片段執行完畢後,原本期望的結果為1000,單運行後的值為999,這表示出現了線程不安全的情況。這也是我們要說明的:volatile關鍵字並不能保證線程安全,它只能保證當前線程需要該變量的值時能夠獲得最新的值,而不能保證線程修改的安全性。
順便說一下,在上面的代碼中,UnsafeThread類的消耗CPU計算是必須的,其目的是加重線程的負荷,以便出現單個線程搶佔整個CPU資源的情景,否則很難模擬出volatile線程不安全的情況,大家可以自行模擬測試。
閱讀更多 龐小輝 的文章