標籤:調度 相關 包含 標記 執行 相互 實現 位元組 zed
上一節我們知道了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線程與並發編程實踐----同步