Java線程與並發編程實踐----同步
進行交互,就會引發很多線程不安全問題,如,競態條件,數據競爭以及緩存變量。
競態條件:當計算的正確性取決於相對時間或者調度器所控制的多線程交叉時,競態條件就會發生。如下例子:
if(a == 10.0){ b = a / 2.0; }
假如一條線程已經執行完了if(a == 10.0),突然被調度器所停止,另一條線程開始執行,並且修改a = 20,此時繼續執行第一條線程,則b = 10。
這就產生了問題
數據競爭:數據競爭指的是兩條或兩條以上的線程並發的訪問同一塊內存區域,同時其中至少有一條是為了寫,而且這些線程沒有協調對那塊內存
域的訪問。當滿足這些條件的時候,訪問順序就是不確定的。依據這種順序,每次運行都可能會產生不同的結果
緩存變量:為了提升性能,編譯器Java虛擬機以及操作系統會協調在寄存器中或處理器緩存中緩存變量,而不是依賴主存,每條線程都會有其自己的
變量拷貝。當線程寫入這個變量的時候,其實是寫入自己的拷貝;其他線程不太可能看到自己的變量拷貝發生更改。
同步可以解決以上問題,方式有同步代碼塊和同步方法。
//同步代碼塊 synchronized(lock){ //需要同步的代碼 } //同步方法 public synchronized void method(){ //需要同步的代碼 }
使用synchronized
變量的值,離開時又將變量的值寫入主存,因此,他總能看到共享變量最近的修改。而同步是通過監聽器實現的,,每一個Java對象都和一個監聽器相關聯,這樣線
程就可以通過釋放和獲取監聽器的鎖來上鎖和解鎖,而一個線程只能持有一個鎖,以此來實現同步。但是鎖的使用雖然可以實現同步,但也會面臨死鎖,活鎖,餓死
的挑戰。
死鎖:線程1等待線程2互斥持有的資源,而線程2也在等待線程1互斥持有的資源,兩個線程都無法繼續執行
活鎖:一個線程持續重試一個失敗的操作,無法繼續執行
餓死:一個線程一直被調度器延時訪問其賴以執行的資源。無法繼續執行
Java語言和JVM並未提供避免以上問題的方式以及差錯辦法,因此主要靠我們在編程過程中自己註意,最簡單的就是盡量減少同步方法、同步塊之間的相互調用
Java中還提供了一種更弱的僅僅包含可見性的同步形式,即volatile關鍵字,有的情況下,我們只需要關註代碼的可見性問題,而不在乎他的互斥性,此時使用synchronized
就顯得沒有必要,我們應該考慮使用volatile,使用volatile標記的屬性,線程在訪問他時不會讀取緩存變量的數據,而是從主存中讀取。還有值的註意的是,使用volatile
修飾double和long類型時,應該避免在32位的操作系統上這樣做,因為double、long是8個字節64個bit,在32位操作系統上它的讀取分為兩步,每一步取32位的數據,
volatile保證了可見性,但不能保證操作的原子性,因此應該註意編碼。使用volatile時不能與final進行連用,因為final本來就可以確保線程訪問的安全性,它修飾一個
屬性,屬性的引用不能被修改,而且引用也不能被緩存。
Java線程與並發編程實踐----同步