從可見性與有序性問題的原因著手
導致可見性問題的原因是快取,導致有序性問題的原因是編譯優化,那麼解決二者的最直接方法就是禁用快取和編譯優化。但是這樣程式的效能將會受到很大程度降低。
這裡較為合理的方案是按需禁用快取和編譯優化。Java記憶體模型規範了JVM如何提供按需禁用快取和編譯優化的方法。具體包括:volatile、synchronized和final關鍵字和Happens-Before規則。
volatile關鍵字
“當變數宣告為volatile型別後,編譯器與執行時都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重排序。---《Java併發程式設計實戰》”
當將一個變數宣告為volatile型別,則表明對於這個變數的讀寫不使用CPU快取,必須從記憶體中讀取或寫入。
從對volatile的描述可以看出,被volatile修飾的變數不寫入快取,且不參與重排序,這就解決了可見性與有序性的問題,但這是否保證了執行緒安全呢?答案是否定的,volatile無法保證原子性--引起併發過程中Bug的另一個源頭。關於原子性的保證,將在後面進行討論。
Happens-Before規則
"In computer science, the happened-before relation (denoted: → ) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow)." ---wikipedia
"In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read." ---wikipedia
Happens-Before規則所描述的是:先後發生的兩個操作,其結果必須也體現操作的先後順序,即:前一個操作的結果對後續操作是可見的。Happens-Before規則具體有以下六項:
1.程式的順序性規則
如以下程式碼,按照程式的執行順序,"x = 12;" Happens-Before 於 "v = true;",對於x的賦值一定發生在對v賦值之前,即對x的操作對後續操作可見。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 12;
v = true;
}
public void reader() {
if(v == true){
//x=?
}
}
}
複製程式碼
2.volatile變數規則
規則2代表針對volatile變數的寫操作,Happens-Before 於後續對這個變數的讀操作。
3.傳遞性
規則3表示:若 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。 正如上面的示例程式碼,假設執行緒A先呼叫writer()方法,執行緒B之後呼叫reader()方法,則:
- 1.根據規則1,"x = 12" Happens-Before "v = true";
- 2.根據規則2,寫變數 "v = true" Happens-Before 於讀變數 "v = true";
- 3.再根據規則3,"x = 12" Happens-Before 讀變數 "v = true"。
這就意味著,執行緒A對x的寫操作對於執行緒B對x的讀操作是可見的,則執行緒B讀取的x為12。
4.管程中鎖的規則
規則4是指一個鎖的解鎖操作 Happens-Before 於後續對這個鎖的加鎖。
管程是一種通用的同步原語,在Java中指的就是synchronized,synchronized是Java裡對管程的實現。
結合規則3,獲得鎖的執行緒在解鎖之前的操作 Happens-Before 解鎖操作,加上規則4的內容,則上述操作 Happens-Before 後續的加鎖操作,也就是該操作對後面獲得鎖的執行緒來說是可見的。
5.執行緒start()規則
規則5指主執行緒A啟動子執行緒B後,子執行緒B能夠看到A在啟動子執行緒B之前的操作。
也就是執行緒A呼叫執行緒B的start()方法,那麼該start()操作 Happens-Before 於執行緒B中的任意操作。
Thread B = new Thread() {
//主執行緒呼叫B.strat()之前所有對共享變數的修改
//此處皆可見,var為20
}
//對共享變數賦值
var = 20;
//主執行緒啟動子執行緒
B.start();
複製程式碼
6.執行緒join()規則
規則6描述執行緒間的等待關係,當主執行緒通過join()方法呼叫子執行緒,等待子執行緒完成。子執行緒中對共享變數的操作對主執行緒全部可見。即:子執行緒中的任意操作 Happens-Before 與join()方法的返回。
總結
1.volatile關鍵字通過禁用快取和指令重排序的方式保證了程式執行中的可見性與有序性; 2.Happens-Before 本質上是一種描述可見性的規則,意味著若事件A Happens-Before 事件B,則A中的所有操作對B而言都是可見的,無論事件AB是否發生在同一個執行緒上。如事件A發生線上程1,事件B發生線上程2上,Happens-Before 規則依舊保證執行緒2上可以看見A事件的發生。