Java併發中正確使用volatile
阿新 • • 發佈:2018-12-23
前幾天併發程式設計群裡有同學對volatile的用法提出了疑問,剛好我記得Twitter有關實時搜尋的這個PPT對這個問題解釋的很清晰並有一個實際的應用場景,於是週末把這個問題摘錄了一些和併發相關的內容如下:
併發 – 定義
悲觀鎖 – Pressimistic locking
- 一個線性在執行一個操作時持有對一個資源的獨佔鎖。(互斥)
- 一般用在衝突比較可能發生的場景下
樂觀鎖 – Optimistic locking
- 嘗試採用原子操作,而不需要持有鎖;衝突可被檢測,如果發生衝突,具有相應的重試邏輯
- 通常用在衝突較少發生的場景下
非阻塞演算法 – Non-blocking algorithm
- 演算法確保對執行緒間競爭共享資源時候,不會因為互斥而使任一執行緒的執行無限延遲;
無鎖演算法 – Lock-free algorithm
- 如果系統整個流程的執行是無阻塞的(系統某一部分可能被短暫阻塞),這種非阻塞演算法就是無鎖的。
- 無鎖演算法比傳統的基於鎖的演算法對系統的開銷更小,且更容易在多核多CPU處理器上擴充套件;
- 在實時系統中可以避免鎖帶來的延遲;
- CAS (compare and swap)或LL/SC(load linked/store conditional),以及記憶體屏障相關的指令經常被用在演算法實現中。
無等待演算法 – Wait-free algorithm
- 如果每個執行緒的執行都是無阻塞的,這種非阻塞演算法就是無等待的(比無鎖演算法更好)
Java的併發
- Java的記憶體模型並不保證一個執行緒可以一直以程式執行的順序看到另一個執行緒對變數的修改,除非兩個執行緒都跨越了同一個記憶體屏障。(Safe publication)
Java記憶體模型
程式碼順序規則
- 一個執行緒內的每個動作 happens-before 同一個執行緒內在程式碼順序上在其後的所有動作
volatile變數規則
- 對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入
傳遞性
- 如果A happens-before B, B happens-before C,那 A happens-before C
Safe publication案例
class VolatileExample { int x = 0; volatile int b = 0; private void write() { x = 5; b = 1; } private void read() { int dummy = b; while (x != 5) { } } public static void main(String[] args) throws Exception { final VolatileExample example = new VolatileExample(); Thread thread1 = new Thread(new Runnable() { public void run() { example.write(); } }); Thread thread2 = new Thread(new Runnable() { public void run() { example.read(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
x並不需要定義為volatile
, 程式裡可以有需要類似x的變數,我們只需要一個volatile變數b來確保執行緒a能看到執行緒1對x的修改:
- 根據程式碼順序規則,執行緒1的
x=5;
happens-beforeb=1;
; 執行緒2的int dummy = b;
happens-beforewhile(x!=5);
- 根據volatile變數規則,執行緒2的
b=1;
happens-beforeint dummy=b;
- 根據傳遞性,
x=5;
happens-beforewhile(x!=5);
JSR-133
在JSR-133之前的舊Java記憶體模型中,雖然不允許volatile變數之間重排序,但舊的Java記憶體模型仍然會允許volatile變數與普通變數之間重排序。JSR-133則增強了volatile的記憶體語義:嚴格限制編譯器(在編譯器)和處理器(在執行期)對volatile變數與普通變數的重排序,確保volatile的寫-讀和監視器的釋放-獲取一樣,具有相同的記憶體語義。