個人博客請訪問 http://www.x0100.top
1. 保證可見性
volatile保證了不同線程對volatile修飾變量進行操作時的可見性。
對一個volatile變量的讀,(任意線程)總是能看到對這個volatile變量最後的寫入。
-
一個線程修改volatile變量的值時,該變量的新值會立即刷新到主內存中,這個新值對其他線程來說是立即可見的。
-
一個線程讀取volatile變量的值時,該變量在本地內存中緩存無效,需要到主內存中讀取。
舉例:
中斷線程時常採用這種標記辦法。
boolean stop = false;// 是否中斷線程1標誌
//Tread1
new Thread() {
public void run() {
while(!stop) {
doSomething();
}
};
}.start();
//Tread2
new Thread() {
public void run() {
stop = true;
};
}.start();
目的: Tread2設置stop=true時,Tread1讀取到stop=true,Tread1中斷執行。
問題: 雖然大多數時候可以達到中斷線程1的目的,但是有可能發生Tread2設置stop=true後,Thread1未被中斷的情況,而且這種情況引發的都是比較嚴重的線上問題,排查難度很大。
問題分析: Tread2設置stop=true時,並未將stop=true刷到主內存,導致Tread1到主內存中讀取到的仍然是stop=false,Tread1就會繼續執行。也就是有內存可見性問題。
解決: stop變量用volatile修飾。
Tread2設置stop=true時,立即將volatile修飾的變量stop=true刷到主內存;
Tread1讀取stop的值時,會到主內存中讀取最新的stop值。
2. 保證有序性
volatile關鍵字能禁止指令重排序,保證了程序會嚴格按照代碼的先後順序執行,即保證了有序性。
volatile的禁止重排序規則:
1)當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
2)當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
3)當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
舉例:
boolean inited = false;// 初始化完成標誌
//線程1:初始化完成,設置inited=true
new Thread() {
public void run() {
context = loadContext(); //語句1
inited = true; //語句2
};
}.start();
//線程2:每隔1s檢查是否完成初始化,初始化完成之後執行doSomething方法
new Thread() {
public void run() {
while(!inited){
Thread.sleep(1000);
}
doSomething(context);
};
}.start();
目的: 線程1初始化配置,初始化完成,設置inited=true。線程2每隔1s檢查是否完成初始化,初始化完成之後執行doSomething方法。
問題: 線程1中,語句1和語句2之間不存在數據依賴關係,JMM允許這種重排序。如果在程序執行過程中發生重排序,先執行語句2後執行語句1,會發生什麼情況?
當線程1先執行語句2時,配置並未加載,而inited=true設置初始化完成了。線程2執行時,讀取到inited=true,直接執行doSomething方法,而此時配置未加載,程序執行就會有問題。
解決: volatile修飾inited變量。
volatile修飾inited,“當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。”,保證線程1中語句1與語句2不能重排序。
3. 不保證原子性
volatile是不能保證原子性的。
原子性是指一個操作是不可中斷的,要全部執行完成,要不就都不執行。
舉例:
public class VolatileTest {
public volatile int a = 0;
public void increase() {
a++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
test.increase();
};
}.start();
}
while (Thread.activeCount() > 1) {
// 保證前面的線程都執行完
Thread.yield();
}
System.out.println(test.a);
}
}
目的: 10個線程將inc加到10000。
結果: 每次運行,得到的結果都小於10000。
原因分析:
首先來看a++操作,其實包括三個操作:
①讀取a=0;
②計算0+1=1;
③將1賦值給a;
保證a++的原子性,就是保證這三個操作在一個線程沒有執行完之前,不能被其他線程執行。
一個可能的執行時序圖如下:
關鍵一步:線程2在讀取a的值時,線程1還沒有完成a=1的賦值操作,導致線程2讀取到當前a=0,所以線程2的計算結果也是a=1。
問題在於沒有保證a++操作的原子性。如果保證a++的原子性,線程1在執行完三個操作之前,線程2不能執行a++,那麼就可以保證在線程2執行a++時,讀取到a=1,從而得到正確的結果。
解決:
-
synchronized保證原子性,用synchronized修飾increase()方法。
-
CAS來實現原子性操作,AtomicInteger修飾變量a。
4. volatile實現原理
volatile保證有序性原理
前文介紹過,JMM通過插入內存屏障指令來禁止特定類型的重排序。
java編譯器在生成字節碼時,在volatile變量操作前後的指令序列中插入內存屏障來禁止特定類型的重排序。
volatile內存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的後面插入一個StoreLoad屏障。
在每個volatile讀操作的後面插入一個LoadLoad屏障。
在每個volatile讀操作的後面插入一個LoadStore屏障。
內存屏障
Store:數據對其他處理器可見(即:刷新到內存中)
Load:讓緩存中的數據失效,重新從主內存加載數據
volatile保證可見性原理
volatile內存屏障插入策略中有一條,“在每個volatile寫操作的後面插入一個StoreLoad屏障”。
StoreLoad屏障會生成一個Lock前綴的指令,Lock前綴的指令在多核處理器下會引發了兩件事:
1. 將當前處理器緩存行的數據寫回到系統內存。
2. 這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。
volatile內存可見的寫-讀過程:
-
volatile修飾的變量進行寫操作。
-
由於編譯期間JMM插入一個StoreLoad內存屏障,JVM就會向處理器發送一條Lock前綴的指令。
-
Lock前綴的指令將該變量所在緩存行的數據寫回到主內存中,並使其他處理器中緩存了該變量內存地址的數據失效。
-
當其他線程讀取volatile修飾的變量時,本地內存中的緩存失效,就會到到主內存中讀取最新的數據。
總結
併發編程中,常用volatile修飾變量以保證變量的修改對其他線程可見。
volatile可以保證可見性和有序性,不能保證原子性。
volatile是通過插入內存屏障禁止重排序來保證可見性和有序性的。