首頁  >  文章  >  Java  >  Java 競態條件與臨界段

Java 競態條件與臨界段

黄舟
黄舟原創
2017-02-28 10:35:361207瀏覽

一個競態條件是一個特殊的條件,可能發生在一個臨界部分的內部(critical section)。一個臨界部分是一段正在被多執行緒執行的程式碼,以及執行緒執行的順序對於臨界部分並發執行的結果產生影響。

當多執行緒執行一個臨界段的結果依賴執行緒執行的順序可能是不同的,這個臨界段包含一個競態條件。這個競態條件的詞條源自於這個執行緒正在競速通過這個臨界段的暗喻,而這個競爭的結果影響著執行這個臨界段的結果。

這可能聽起來有點複雜,以至於我將會在下面的部分詳細闡述關於競態條件和臨界段。


臨界段(Critical Sections)

在相同的應用程式內部運行不只一個執行緒不會被他自己引起問題。當多個執行緒存取相同的資源問題就會出現。例如相同的記憶體(變量,數組,或物件),系統(資料庫,web服務)或檔案。

事實上,如果一個或多個執行緒寫這些資源的時候問題會出現。讓多個執行緒讀取相同的資源是安全的,只要資源不會改變。

這裡有一個例子,如果多個執行緒同事執行可能會失敗:


#
 public class Counter {

     protected long count = 0;

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


想像下如果執行緒A和B正在執行相同的Counter類別的實例的add方法。這裡沒有辦法知道作業系統什麼時間會在執行緒之間切換。 add方法中的程式碼不會被java虛擬機器作為單獨的原子指令執行。而是作為一系列的更小的指令集執行,跟這個類似:

  1. 從記憶體中讀取this.count值進入暫存器。

  2. 增加value值到暫存器。

  3. 將暫存器中的值寫回記憶體。

觀察線程A和B混合執行會發生什麼:

       this.count = 0;

   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


這兩個執行緒想新增2和3到counter中。因此這兩個執行緒執行完之後的值應該是5。然而,因為這兩個執行緒執行時交叉的,因此結果以不同而結束。

在上面提到的執行順序的例子中,兩個執行緒都從記憶體中讀取到0這個值。然後他們加他們各自的值,2和3到那個值中去,然後把這個結果寫回記憶體。取代5,在this.count中留下的值將會是最後的那個線程寫給他的那個值。在上面的例子中是線程A,但是他也可能是線程B。

在臨界段中的競態條件

#在上面的那個例子中的add方法的程式碼中包含了一個臨界段。當多個執行緒執行這個臨界段的時候,競態條件就會發生了。

更正式的講,兩個執行緒的這種情形競爭著相同的資源,資源被存取的順序是重要的,它被稱為競態條件。一個代碼部分導致競態條件就會被稱為臨界段。

預防競態條件

為了防止競態條件的發生,你必須確保被執行的臨界段作為一個原子指令被執行。那意味著一旦一個單獨的執行緒在執行它,其他的執行緒就不能執行它直到第一個執行緒已經離開了這個臨界段。

競態條件可以透過在臨界段中使用執行緒同步的方式去避免。執行緒同步可以使用一個Java程式碼的同步鎖去取得。線程同步也可以使用其他的同步概念去獲取,像鎖或像java.util.concurrent.atomic.AtomicInteger的原子變數。

臨界段的吞吐量

對於更小的臨界段使得整個臨界段的一個同步鎖定可能會運作。但是,對於更大的臨界段,去把它分解成更小的臨界段是更有意義的,使得允許多執行緒去執行每一個更小的臨界段。整個就可能降低共享資源的競爭,以及增加整個臨界段的吞吐量。

這裡有一個非常簡單的Java實例:

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方法是怎麼往這兩個sum變數中加入值得。為了預防競態條件,在內部執行的求和有一個Java同步鎖。伴隨著這個實現,同時只能有一個執行緒可以執行這個求和。

然而,因為這兩個sum變數是互相獨立的,你可以把他們分開成兩個分離的同步鎖,像這樣:

public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;
    
    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
        }
        synchronized(this){
            this.sum2 += val2;
        }
    }
}


注意,兩個執行緒可以同時執行這個add方法。一個執行緒取得到第一個同步鎖,另外一個執行緒取得第二個同步鎖。這種方式,執行緒之間將會等待的更少時間。

當然,這個例子是非常簡單的。在真正的生活中,臨界段分離的共享資源可能會更加複雜的,並且需要更多的執行順序可能性的分析。


 以上就是Java 競態條件與臨界段的內容,更多相關內容請關注PHP中文網(www.php.cn)!


#
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn