volatile 關鍵字(修飾變量)
1. 含義
是一種比 sychronized
關鍵字更輕量級的同步機制,訪問 volitile
變量時,不會執行加鎖操作。
2. 作用
volatile 是一個類型修飾符(type specifier)。volatile 的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。
- 保證可見性
- 禁止指令重排序優化
指令重排序優化:普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序和程序代碼中的執行順序一致
3. 如何保證可見性
新值能立即同步到主內存,以及每次使用前立即從主內存刷新。
-
volatile 修飾的變量,是直接拿的主內存的值,就是說這個值永遠是最新的,對其他線程是可見的。
-
而訪問非
volatile
變量時,每個線程都會從系統內存(主內存)拷貝變量到工作內存中,然后修改工作內存中的變量值,操控的變量可能不同。
4. 如何禁止指令重排序優化
volatile 通過設置 Java 內存屏障禁止重排序優化。
java 內存屏障
內存屏障也稱為內存柵欄或柵欄指令,是一種屏障指令,它使CPU或編譯器對屏障指令之前和之后發出的內存操作執行一個排序約束。 這通常意味着在屏障之前發布的操作被保證在屏障之后發布的操作之前執行。
java 的內存屏障通常所謂的四種即
LoadLoad
,StoreStore
,LoadStore
,StoreLoad
實際上也是上述兩種的組合,完成一系列的屏障和數據同步功能。(
Load
指令(也就是從內存讀取),Store指令
(也就是寫入內存)。)
LoadLoad 屏障:對於這樣的語句
Load1; LoadLoad; Load2
,在 Load2 及后續讀取操作要讀取的數據被訪問前,保證Load1 要讀取的數據被讀取完畢。StoreStore 屏障:對於這樣的語句
Store1; StoreStore; Store2
,在 Store2 及后續寫入操作執行前,保證 Store1 的寫入操作對其它處理器可見。LoadStore 屏障:對於這樣的語句
Load1; LoadStore; Store2
,在 Store2 及后續寫入操作被刷出前,保證 Load1 要讀取的數據被讀取完畢。StoreLoad 屏障:對於這樣的語句
Store1; StoreLoad; Load2
,在 Load2 及后續所有讀取操作執行前,保證 Store1 的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能
volatile 做了什么
在一個變量被 volatile 修飾后,JVM 會為我們做兩件事:
-
在每個 volatile 寫操作前插入 StoreStore 屏障,在寫操作后插入 StoreLoad 屏障。(
StoreStore-寫-StoreLoad
) -
在每個 volatile 讀操作前插入 LoadLoad 屏障,在讀操作后插入LoadStore屏障。(
LoadLoad-讀-LoadStore
)
5. volatile 是不安全的
雖然 volatile 可見性保證了對 volatile 變量所有的寫操作都能立刻反應到其他線程之中(即 volatile 變量在各個線程中都是一致的),但是 Java 里面的運算並非原子操作。只有是原子操作的 volatile 變量才是線程安全的,比如我們很常見的 變量++ 自增操作,在這個過程中,自增包括取數,加一,保存三個過程的操作,所以自增並不是原子性操作,使用 volatile 修飾的變量自增操作仍然是不安全的。
舉個例子:
public class MyVolitile {
private static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
結果每次都不一樣,不一定等於 200。
6. volatile 不適用場景
由於 volatile 變量只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用 synchronized 或 java.util.concurrent 中的原子類)來保證原子性。
- 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值
- 變量不需要與其他的狀態變量共同參與不變約束