問題根源
在多線程運行場景中,每個線程可能運行與 不同的CPU中,每個線程的讀寫都是操作自己CPU所在的高速緩存,但操作的數據存儲在堆中,作爲共享變量,每個線程操作的實際上是自己本地線程的副本,在併發 編程中,可能就會存在緩存不一致的問題
早期的CPU是通過總線加Lock#鎖的方式解決緩存不一致問題,對總線加Lock#鎖只保證只能有一個CPU使用這個變量的內存,這樣會導致其他CPU無法訪問內存,效率低,現在主要是通過緩存一致性協議來解決緩存一致性問題
Volatile特徵
- Volatile保證了不同線程對該線程操作的內存可見性
- 禁止編譯器進行指令性重排
- volatile變量是一種輕量同步機制,訪問volatile不會執行加鎖操作
- volatile無法同事保證內存可見性和 原子性,因此更建議使用加鎖(同步)機制
Volatile解決內存可見性問題
之前我們分析過 ,多線程的讀寫並不會操作主內存(共享內存),而是操作線程的本地副本,這也是導致線程間數據不可見的本質,Volatile主要是從這個角度解決問題
- 修改volatile修飾的變量,CPU會強制將修改後的值刷新到主內存中
- 修改volatile修飾的變量,會導致其他線程 的本地內存的對應該變量值失效,該變量值的獲取需要再重新從共享內存中讀取
如何防止編譯器指令性重排
編譯器語義重排的後果
編譯器遵守as-if-serial 語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作可能被編譯器和處理器重排序。
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
如圖所示,操作1和操作2做了重排序。程序執行時,線程A首先寫標記變量 flag,隨後線程 B 讀這個變量。由於條件判斷爲真,線程 B 將讀取變量a。此時,變量 a 還根本沒有被線程 A 寫入,在這裏多線程程序的語義被重排序破壞了!
volatile關鍵字保證有序性
* volatile關鍵字關鍵字實際上給CPU添加了一個內存屏障,它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成
volatile使用要點
-
對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
-
該變量沒有包含在具有其他變量的不變式中。