Java的線程同步和併發問題示例

併發問題安全

多線程是一個很是強大的工具,它使咱們可以更好地利用系統的資源,但咱們須要在讀取和寫入多個線程共享的數據時特別當心。多線程

當多個線程嘗試同時讀取和寫入共享數據時,會出現兩種類型的問題 -併發

線程干擾錯誤內存一致性錯誤讓咱們逐一理解這些問題。工具

線程干擾錯誤(競爭條件)性能

考慮如下Counter類,其中包含一個increment()方法,每次調用它時計數增長一次 -優化

如今,讓咱們假設幾個線程試圖經過increment()同時調用方法來增長計數-線程

您認爲上述計劃的結果如何?最終計數是1000,由於咱們調用增量1000次?對象

但實際上答案是否認的!只需運行上面的程序,本身查看輸出。它不是產生最終計數1000,而是每次運行時都會產生不一致的結果。我在計算機上運行了上述程序三次,輸出爲992,996和993。blog

讓咱們深刻研究該程序並理解程序輸出不一致的緣由 -排序

當線程執行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方法和塊來避免它們。同步是一個強大的工具,但請注意,沒必要要的同步可能會致使其餘問題,如死鎖和飢餓。

相關文章
相關標籤/搜索