Java 并發/多線程教程(八)-競態條件和臨界區

? ? ? 本系列譯自jakob jenkov的Java并發多線程教程,個人覺得很有收獲。由于個人水平有限,不對之處還望矯正!

? ? ? 競態條件是在臨界區內可能發生的一種特殊情況。臨界區是多線程并發執行一代碼,根據線程的執行順序可能產生多種結果的區域。多線程在臨界區執行代碼的結果可能不一樣,不同的結果取決于線程的執行順序。也就是說,臨界區包含競態條件。競態一詞源于隱喻,線程在臨界區進進行資源競爭,在臨界區的資源競爭影響最后的結果。

? ? ?這聽起來有點復雜,在下面的篇幅中,將會在下面的篇幅中介紹更多與競態條件和臨界區的相關內空。

臨界區

? ? ?在一個應用的內部執行多個線程本身不會導致什么問題,當多個線程同時訪問相同的資源,問題就出現了。舉個例子:相同的內存資源(變量、數組、或者對象),系統資源(數據庫、webservice)或者文件。

? ? ? 實事上,這種問題僅出現在多個線程同時對這些資源進行寫操作時。只要資源不更改,多個線程同時對相同的資源進行讀是安全的。

? ? ? 下面的這個例子當多個線程同時執行時可能會出錯:

public class Counter{

? ? protected long count= 0; ? ?

? ? public void add(long value){

? ? ? ? ?this.count=this.count+value;

? ?}

}

假設有兩個線程A和B,在Counter的同一個實例上同時執行add方法,我們沒辦法預知操作系統在這兩個線之間的調度順序。這段代碼在JVM中不是以原子操作的方式執行。而是把他們當作一組指令去執行:

1、從內存中讀取this.count的值到寄存器

2、把值添添加后寫到寄存器

3、把寄存器中的值寫回到內存中

觀察線程A和線程B的執行,將會發生些什么。

this.count = 0;

A: Reads this.count into register(0);

B:Reads this.count into register(0);

B:Add value 2 to register;

B:Writes register value(2) back to memory, this.count now equals 2

A:Add value 3 to register;

A:Writes register value(3) back to memory,this.count now equals 3

? ? ? 這兩個線程的目的是想對count進行加2,加3操作。因此這兩個線程的執行結果預期應該為5,然而,由于這兩個線程的交錯執行,結果將會不同。 在上面的例子中,A線程和B線程剛開始從內存中讀取到的數據都是0,然后它們各自將值加到counter上,然后將值寫回到內存中,而不是5,this.count的值的最后的值就是最后一個寫這個值的線程,在上面的例子中,他可能是A線程,也有可能會是B線程。

臨界區的競態條件

? ? ? ?在上面的例子中,當執行add()方法時,當多線程去執行這段代碼時,在臨界區就產生了競態條件。

? ? ? 當兩個線程訪問相同的資源,他們的訪問順序就叫做競態條件,導致競態條件產生的代碼區就是臨界區。

預防競態條件

? ? ? ? 要預防競態條件的產生,你要確保臨界區的代碼以原子指令執行。也就是說一次只有單一的線程去執行它,在這個線程執行完,離開臨界區之前,沒有其他的線程可以執行。

? ? ? 競態條件也可以通過臨界區的線程同步來避免。線程同步可以采用java同步代碼塊、鎖或者原子變量(java.util.concurrent.atomic.AtomicInteger)來實現 。

臨界區吞吐量

對于較小的臨界區,使得整個臨界區一起同步工作。但是,較大的臨界區分解成較小的臨界區有可能會有好處,它允許多個線程在較小的臨界區中執行,這樣可以減少共享資源的競爭,提升整個臨界區的吞吐量。

下面由簡單的例子來說明,我想要表達的意思:

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()方法把相加后的值賦給兩個不同的變量。為了避免競態條件,求和的代碼在java的同步代碼塊中執行。通過這種方式,在同時執行這段代碼時,只有一個線程執行求和操作。然而,由于兩個求和參數是兩個完全獨立的變量,你可以把他們分散到兩個同步代碼塊中去求和。就像下面一樣:

public class TwoSums{

? ?private int sum1 = 0;

? ?private int sum2 = 0;

? ?private Integer sum1Lock ?= new Integer(1);

? ?private Integer sum2Lock = new Integer(2);

? ?private void add(int val1,int val2){

? ? ? ? ?synchronized(this.sum1Lock){

? ? ? ? ? ? ? ?this.sum1 += val1;

? ? ? ? ?}

? ? ? ? ?synchronized(this.sum2Lock){

? ? ? ? ? ? ? ?this.sum2 += val2;

? ? ? ? ? }

? ?}

}

現在兩個線程可以同時執行add()方法。其中一個線程進行第一個同步代碼塊,第二線程進入到第二個同步代碼塊,由于這兩個同步代碼對不同的對象進行同步操作,所以兩個不同的線程可以各自獨立的去執行這兩個代碼塊。通過這種方式,線程可以盡量少的減少等待去執行add()方法。

這個例子非常簡單,當然,在實際的運用中,共享資源可能分解可能更為復雜,需要更多的去分析他們執行順序的可能性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容