1. 程式人生 > >Java的執行緒同步和併發問題示例

Java的執行緒同步和併發問題示例

併發問題

多執行緒是一個非常強大的工具,它使我們能夠更好地利用系統的資源,但我們需要在讀取和寫入多個執行緒共享的資料時特別小心。

當多個執行緒嘗試同時讀取和寫入共享資料時,會出現兩種型別的問題 -

執行緒干擾錯誤記憶體一致性錯誤讓我們逐一理解這些問題。

執行緒干擾錯誤(競爭條件)

考慮以下Counter類,其中包含一個increment()方法,每次呼叫它時計數增加一次 -

現在,讓我們假設幾個執行緒試圖通過increment()同時呼叫方法來增加計數-

您認為上述計劃的結果如何?最終計數是1000,因為我們呼叫增量1000次?

但實際上答案是否定的!只需執行上面的程式,自己檢視輸出。它不是產生最終計數1000,而是每次執行時都會產生不一致的結果。我在計算機上運行了上述程式三次,輸出為992,996和993。

讓我們深入研究該程式並理解程式輸出不一致的原因 -

當執行緒執行increment()方法時,執行以下三個步驟:1。檢索計數的當前值2.將檢索的值增加1 3.將增加的值重新儲存到計數中

現在讓我們假設兩個執行緒 - ThreadA和ThreadB按以下順序執行這些操作 -

ThreadA:檢索計數,初始值= 0ThreadB:檢索計數,初始值= 0ThreadA:增加檢索值,結果= 1ThreadB:增加檢索值,結果= 1ThreadA:儲存遞增的值,count現在為1ThreadB:儲存遞增的值,count現在為1兩個執行緒都嘗試將計數遞增1,但最終結果是1而不是2,因為執行緒執行的操作相互交錯。在上述情況下,ThreadA完成的更新將丟失。

上述執行順序只是一種可能性。可能有許多這樣的命令可以執行這些操作,使程式的輸出不一致。

當多個執行緒嘗試同時讀取和寫入共享變數,並且這些讀取和寫入操作在執行中重疊時,最終結果取決於讀取和寫入發生的順序,這是不可預測的。這種現象稱為種族狀況。

訪問共享變數的程式碼部分稱為Critical Section。

通過同步對共享變數的訪問可以避免執行緒干擾錯誤。

讓我們首先看一下多執行緒程式中出現的第二種錯誤 - 記憶體一致性錯誤。

記憶體一致性錯誤

當不同的執行緒具有相同資料的不一致檢視時,會發生記憶體不一致錯誤。當一個執行緒更新某些共享資料時會發生這種情況,但此更新不會傳播到其他執行緒,並且最終會使用舊資料。

為什麼會這樣?嗯,這可能有很多原因。編譯器會對您的程式進行多次優化以提高效能。它還可能重新排序指令以優化效能。處理器也嘗試優化事物,例如,處理器可能從臨時暫存器(包含變數的最後讀取值)讀取變數的當前值,而不是主儲存器(具有變數的最新值) 。

請考慮以下示例,該示例演示了操作中的記憶體一致性錯誤 -

在理想情況下,上述計劃應 -

等待一秒鐘,然後列印Hello World!後sayHello變為真。等待一秒鐘,然後列印Good Bye!後sayHello變為假。

但是在執行上述程式後我們是否得到了所需的輸出?好吧,如果你執行程式,你會看到以下輸出 -

此外,該程式甚至沒有終止。

執行緒等待。什麼?怎麼可能?

是! 這就是記憶體一致性錯誤。第一個執行緒不知道主執行緒對sayHello變數所做的更改。

您可以使用volatile關鍵字來避免記憶體一致性錯誤。我們很快就會詳細瞭解volatile關鍵字。

同步

通過確保以下兩件事可以避免執行緒干擾和記憶體一致性錯誤 -

一次只有一個執行緒可以讀寫共享變數。當一個執行緒正在訪問共享變數時,其他執行緒應該等到第一個執行緒完成。這保證了對共享變數的訪問是Atomic,並且多個執行緒不會干擾。每當任何執行緒修改共享變數時,它都會自動建立與其他執行緒後續讀取和寫入共享變數的先發生關係。這可以保證一個執行緒所做的更改對其他人可見。幸運的是,Java有一個synchronized關鍵字,您可以使用該關鍵字同步對任何共享資源的訪問,從而避免這兩種錯誤。

同步方法

以下是Counter類的同步版本。我們synchronized在increment()方法上使用Java的關鍵字來防止多個執行緒同時訪問它 -

如果執行上述程式,它將產生1000的所需輸出。不會出現競爭條件,並且最終輸出始終保持一致。該synchronized關鍵字可確保只有一個執行緒可以進入increment()一次的方法。

請注意,同步的概念始終繫結到物件。在上面的例子中,increment()在同一個例項上多次呼叫方法SynchonizedCounter會導致競爭條件。我們正在使用synchronized關鍵字防範這種情況。但是執行緒可以安全地increment()在不同的例項上SynchronizedCounter同時呼叫方法,這不會導致競爭條件。

在靜態方法的情況下,同步與Class物件相關聯。

同步程式碼塊

Java內部使用所謂的內部鎖或監視器鎖來管理執行緒同步。每個物件都有一個與之關聯的內在鎖。

當一個執行緒呼叫一個物件的synchronized方法時,它會自動獲取該物件的內部鎖,並在該方法退出時釋放它。即使方法丟擲異常,也會發生鎖定釋放。

在靜態方法的情況下,執行緒獲取Class與類關聯的物件的內部鎖,這與該類的任何例項的內部鎖不同。

synchronizedkeyword也可以用作塊語句,但與synchronized方法不同,synchronized語句必須指定提供內部鎖的物件 -

 

當執行緒獲取物件的內部鎖時,其他執行緒必須等到鎖被釋放。但是,當前擁有鎖的執行緒可以多次獲取它而沒有任何問題。

允許執行緒多次獲取同一個鎖的想法稱為“ 重入同步”。

易變的關鍵字

Volatile關鍵字用於避免多執行緒程式中的記憶體一致性錯誤。它告訴編譯器避免對變數進行任何優化。如果將變數標記為volatile,則編譯器不會優化或重新排序該變數的指令。

此外,變數的值將始終從主儲存器而不是臨時暫存器中讀取。

以下是我們在上一節中看到的相同MemoryConsistencyError示例,不同之處在於,這次我們sayHello使用volatile關鍵字標記了變數。

 

執行上述程式會產生所需的輸出 -

 

結論

通過示例我們瞭解了多執行緒程式中可能出現的不同併發問題以及如何使用synchronized方法和塊來避免它們。同步是一個強大的工具,但請注意,不必要的同步可能會導致其他問題,