【Java系列】Java併發之 Race Condition and Critical Section

凱倫說_美團點評發表於2017-09-08

個人介紹

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塊同步的是不同的物件,所以兩個執行緒可以獨立執行,整體執行緒等待的時間會變少,吞吐量能夠得到提升。

相關文章