Java併發編程(Java Concurrency)(8)- 競爭與臨界區(Race Conditions and Critical Sections)

原文鏈接:http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html

摘要:這是翻譯自一個大概30個小節的關於Java併發編程的入門級教程,原作者Jakob Jenkov,譯者Zhenning Lang,轉載請註明出處,thanks and have a good time here~~~(希望自己不要留坑)

“競爭”是可能發生在“臨界區”內的一種特使情況。臨界區的含義是:如果多個線程訪問一個代碼段,同時這些線程的執行順序將影響總得執行效果,那麼這個代碼端就叫做臨界區。

反言之,如果臨界區的執行結果受到多線程執行順序的影響,那麼就說存在競爭。競爭比喻了不同的線程互相爭搶臨界區的代碼,並且爭搶的結果也將影響臨界區的運行結果。

如果上面過於抽象難懂,下面的例子將幫助理解競爭與臨界區的含義。

1 臨界區

當多個線程執行其內部代碼的時候,其本身不會引起任何問題;引起問題的原因是對共享資源的使用。例如共用的內存(變量、數組和對象等)、系統資源(數據庫、網絡服務等)或者是文件。

跟進一步,事實上只有寫共享資源操作纔會引發問題。只要內容不變,讓多個線程讀取相同的資源是安全的。

下面是一個臨界區的 Java 代碼示例,例子中如果多個線程同時運行這段代碼就會發生失敗:

public class Counter {

    protected long count = 0;

    public void add(long value){
        this.count = this.count + value;
    }
}

現在假設兩個線程 A 和 B 在對相同的 Counter 類的實例進行 add 操作,沒有辦法知道操作系統何時在兩個線程間切換。Java 虛擬機無法像對待單一“原子”一樣處理 add() 函數中的代碼(就是沒辦法一下子執行完代碼中的全部內容,而是按步驟執行的),事實上 add() 中的代碼好像被一系列更小的指令執行:

  1. 將 this.count 從內存中讀到寄存器中
  2. 將 value 與寄存器中的數據相加並存入寄存器
  3. 將寄存器數據寫入內存

那麼現在考慮如下的線程 A 和 B 的執行情況:

this.count = 0;
B: 將 this.count 讀入一個寄存器 (0) (
A: 將 this.count 讀入一個寄存器 (0)
B: 將寄存器中的數據 +2 (2)
B: 將寄存器中的數據 (2) 寫回內存,此時 this.count 等於 2
A: 將寄存器中的數據 +3 (3)
A: 將寄存器中的數據 (3) 寫回內存,此時 this.count 等於 3

這兩個線程的本來目的是將 2 和 3 加到 counter 上,所以期待的結果應該是 5。然而實際運行中線程發生了交錯,導致結果與預期不同。上例中,兩個線程都將 0 從內存中讀出並且加上了 2 和 3,然後將其寫回內存。所以最後的結果取決於誰最後將結果寫回內存(2 和 3 都有可能)。

2 臨界區中的競爭

前面例子的 add() 方法中存在着臨界區,所以當多線程執行臨界區代碼的時候,競爭發生了。

對於臨界區和競爭更正規的定義是:如果兩個線程爭奪共享資源,並且資源被獲取的時機(順序)將對結果產生影響,這種情況叫做競爭;引發競爭的代碼段被稱作鄰接區

3 阻止競爭的發生

爲了阻止競爭的發生,臨界區的代碼必須以“原子”的模式被執行 —— 即一旦一個線程開始執行臨界區的代碼,直到其執行完臨界區,其他的線程就無法執行臨界區。

通過對臨界區代碼設置合理的線程同步(thread synchronization)機制,競爭就可以被阻止。而 Java 中的線程同步的一種實現方式是同步代碼塊(a synchronized block of Java code)。其他的實現途徑還有鎖(locks)、原子變量(atomic variables)(例如 java.util.concurrent.atomic.AtomicInteger)。

4 臨界區的吞吐量

相比於定義分散的小的臨界區,將所有代碼定義成一個大的臨界區也可以讓代碼正常運作。但將大的臨界區拆封成小的臨界區將帶來更多的好處,因爲這樣的話不同的線程可以同時執行這些小的臨界區代碼,從而減少了其相互之間對資源的爭奪,增加了整個臨界區吞吐量。

爲了解釋這一點,下面是一個非常簡單的示例:

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;
        }
    }
}

上例中 add() 函數嘗試着將 val1 和 val2 加到 sum1 和 sum2 上。同時爲了防止競爭的發生,這段代碼被放在了 Java 同步代碼塊中,這樣同一時間只能有一個線程執行 add() 中的具體加操作。

然而,由於例子中的 sum1 和 sum2 變量是相互獨立,完全可以分開在兩個同步塊中執行二者的加操作,如下:

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() 方法中的代碼。一個線程執行 sum1 的加操作,而另一個線程執行 sum2 的加操作。由於兩個同步塊被兩個不同的對象同步(this.sum1Lock 和 this.sum2Lock),這樣一來執行 add() 方法的多個線程相互等待的時間就被減少了。

當然上例是非常簡單的,而實際中對於臨界區的拆分可能更加複雜,並且需要仔細考量線程執行順序以及其結果的可能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章