【Java併發】volatile 和 final
volatile 和 final
重排序
重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。
資料依賴性
如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這個兩個操作就存在資料依賴性。資料依賴性分下列三種類型:
名稱 | 程式碼例項 | 說明 |
---|---|---|
寫後讀 | a = 1; b = a; | 寫一個變數後,再讀這個變數 |
寫後寫 | a = 1; a = 2; | 寫一個變數後,再寫這個變數 |
讀後寫 | a = b; b = 1; | 讀一個變數後,再寫這個變數 |
上面3中情況,只要重排序兩個操作的執行順序,程式的執行結果就會被改變。所以編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序。
as-if-serial語義
as-if-serial語義的意思是:不管怎麼重排序,(單執行緒)程式的執行結果不能被改變。
一個生動的例子:
double pi = 3.14 //A
double r = 1.0 // B
double area = pi * r * r; //C
A 和 C存在資料依賴關係,同時B 和 C之間也存在資料依賴關係。因此C不會被重排序到A 和 B的前面。
A 和 B之間沒有資料依賴關係,編譯器和處理器可以重排序A 和 B之間的執行順序。
重排序對多執行緒的影響
一個生動的例子
public 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
......
}
}
}
假設有兩個執行緒A和B,A首先執行writer() 方法,隨後B執行緒接著執行reader() 方法。執行緒B在執行操作4時,能否看到執行緒A在操作1對共享變數a的寫入呢?答案是:不一定能看到。
由於操作1和操作2沒有資料依賴關係,編譯器和處理器可以對這兩個操作重排序;操作3和操作4沒有資料依賴關係,編譯器和處理器也可以對這兩個操作重排序。
如果操作1和操作2重排序時,如下圖:
如果操作3和操作4重排序時,如下圖:
happens - before 規則
如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係執行的結果一致,那麼這種重排序並不非法。
- 程式順序規則:一個執行緒中的每個操作,happens-before與該執行緒中的任意後續操作
- 鎖規則:對一個鎖的解鎖,happens-before與隨後對這個鎖的加鎖
- volatile規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀
- start()規則:如果執行緒A執行操作ThreadB.start(),那麼A執行緒的ThreadB.start()操作happens-before與執行緒B中的任意操作
- join規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before與執行緒A從ThreaB.join()操作成功返回。
- 傳遞性:如果A happens-before B,B happens-befo C,那麼A happens-before C。
volatile記憶體語義
一個生動的例子
class VolatileExample{
int a = 0;
volatile flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
......
}
}
}
假設執行緒A先執行writer()方法,執行緒B執行reader()方法。根據happens-before規則,這個過程建立的happens-before關係分類3類:
- 根據程式次序規則,1 happens-before 2;3 happens-before 4。
- 根據volatile規則,2 happens-before 3。
- 根據傳遞性規則,1 happens-before 4。
程式時序圖如下圖所示:
volatile保證:
- volatile寫之前的操作不會被重排序到volatile之後。
- volatile讀之後的操作不會被重排序到volatile之前。
- 第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
final記憶體語義
一個生動的例子
class FinalExample {
int i; // 普通變數
final int j; // final 變數
static FinalExample obj;
public FinalExample() {
i = 1;
j = 2;
}
public void writer() {
obj = new FinalExample();
}
public void reader() {
FinalExample object = obj;
int a = object.i;
int b = object.j;
}
}
假設執行緒A執行writer()方法,隨後另一個執行緒B執行reader()方法。
寫final域的重排序規則的程式時序圖如下:
讀final域的重排序規則的程式時序圖如下:
final保證:
- 在物件引用為任意執行緒可見之前,物件的final域已經被正確初始化過了。
- 在讀一個物件的final域之前,一定會先讀包含這個final域的物件的引用,而引用物件的final域肯定初始化過了。
final引用不能從建構函式溢位
一個生動的例子
class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1;
obj = this;
}
public void writer() {
new FinalReferenceEscapeExample();
}
public void reader() {
if (obj != null) {
int temp = obj.i;
}
}
}
假設執行緒A執行writer()方法,隨後另一個執行緒B執行reader()方法。
執行程式時序圖可能如下:
在建構函式返回前,被構造物件的引用不能為其他執行緒所見,因為此時的final域可能還沒有被初始化。在建構函式返回後,任意執行緒都將保證能看到final正確初始化之後的值。
參考
- Java併發程式設計的藝術[書籍]