個人介紹
Java愛好者,個人網站: kailuncen.me/about/
前言
這幾天學習併發程式設計,race-conditions-and-critical-sections,翻譯一下,寫點自己的筆記並加上點個人的理解。
網頁中裡中提到兩個名詞Race Condition 和 Critical Section,接下來對他們進行解釋和例子演示。
Race Condition
在多執行緒場景下,當多個執行緒訪問同一塊資源,且執行結果與執行緒訪問的先後順序相關,即表明這裡面存在著Race Condition,中文翻譯即競爭條件。
看下面?的程式碼,多個執行緒都會呼叫add方法對同一個count值進行加法。
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}複製程式碼
然而,add方法中的加法需要好幾個步驟才能完成。
1. 從記憶體中讀取count的值到暫存器。
2. 加value。
3. 寫回記憶體。複製程式碼
如果有兩個執行緒都對add方法進行了操作,比如執行緒A加3,執行緒B加2,我們的預期結果是5。由於執行緒的訪問順序以及切換的時間是不可預期的,在特定的訪問順序下,可能出現一些出乎意料的結果,比如下文中的執行順序。
A: Reads this.count into a register (0)
B: Reads this.count into a register (0)
B: Adds value 2 to register
B: Writes register value (2) back to memory. this.count now equals 2
A: Adds value 3 to register
A: Writes register value (3) back to memory. this.count now equals 3複製程式碼
由於加法不是原子性的,在加法執行過程中的每一步都可能存在著執行緒切換。
比如執行緒A和B都先後讀到0,然後執行緒B佔用了時間片完成了加2的操作,寫回了記憶體,此時記憶體中count的值等於2。
然後執行緒A重新得到排程,此時執行緒A內部的count值還是0,執行緒A對主記憶體內count的變化是不可見的,然後執行緒完成加3操作,寫回記憶體,此時count值等於3。
上述程式碼中的add方法內部就存在著競爭條件,會根據執行緒執行順序的不確定性影響最後的執行結果。
Critical Section
我們把會導致Race Condition的區域稱為Critical Section,中文翻譯臨界區。臨界區即每個執行緒中訪問臨界資源的那段程式碼。
在上文的程式碼中,this.count就是臨界資源
this.count = this.count + value複製程式碼
就是臨界區,為了保證執行結果的正確性,避免臨界區內產生競爭條件,我們需要確保臨界區內的執行是原子的,每次僅允許一個執行緒進去,進入後不允許其他執行緒進入。
我們可以採用執行緒同步做到以上的要求,執行緒同步可以使用synchronized同步程式碼,或者locks,或者是原子變數比如AtomicInteger等。
可以把整個臨界區使用synchronized同步,但把臨界區拆分成多個小的臨界區能夠降低對共享資源的爭奪,增加整個臨界區的吞吐量,下面舉個例子。
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
public void add(int val1, int val2){
synchronized(this){
this.sum1 += val1;
this.sum2 += val2;
}
}
}複製程式碼
在上述程式碼中,簡單的做法就是鎖住整個物件,只有一個執行緒能夠執行兩個不同變數的加法操作。然而,由於這兩個變數是互相獨立的,可以拆分到兩個不同的synchronized塊中。
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1);
private Integer sum2Lock = new Integer(2);
public void add(int val1, int val2){
synchronized(this.sum1Lock){
this.sum1 += val1;
}
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}複製程式碼
改動後,兩個執行緒可以同時在add方法中操作,一個執行緒在第一個synchronized塊,另一個執行緒在第二個synchronized塊,兩個synchronized塊同步的是不同的物件,所以兩個執行緒可以獨立執行,整體執行緒等待的時間會變少,吞吐量能夠得到提升。