多線程系列提高(2)--線程安全性

如果當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式可以修復這個問題:
(1)不在線程之間共享該狀態變量
(2)將狀態變量修改爲不可變的變量
(3)在訪問狀態變量時使用同步

注意:完全由線程安全類構成的程序並不一定就是線程安全的,而在線程安全類中也可以包含非線程安全的類。

線程安全性:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就稱這個類是線程安全的。——Java併發編程實戰

競態條件:這種由於不恰當的執行時序而出現不正確的結果是一種非常重要的情況。當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。最常見的競態條件類型就是“先檢查後執行”操作,即通過有個可能失效的觀測結果來決定下一步的動作。

競態條件的本質:基於一種可能失效的觀察結果來做出判斷或者執行某個計算,這種類型的競態條件稱爲“先檢查後執行”:首先觀察到某個條件爲真(例如文件X不存在),然後根據這個觀察結果採用相應的動作(創建文件X),但事實上,在你觀察到這個結果以及開始創建文件之間,觀察結果可能變得無效(另一個線程在這期間創建了文件X),從而導致各種問題(未預期的異常、數據被覆蓋、文件被破壞等),這是一系列的動作,並不是一個原子操作。

(1)延遲初始化中的競態條件:
單例模式中的懶漢式加載方式:

public class SingleTon{
    private static SingleTon instance;
    private static SingleTon(){}
    public static SingleTon getInstance(){
        if(instance==null){
            instance=new SingleTon();
        }
        return instance;
    }
}

在SingleTon中包含了一個競態條件,它可能會破壞這個類的正確性。假定線程A和線程B同時執行getInstance。A看到instance爲空,因而創建一個新的SingleTon實例。B同樣需要判斷instance是否爲空。此時的instance是否爲空,要取決於不可預測的時序,包括線程的調度方式,以及A需要花多長時間來初始化SingleTon並設置instance。如果當B檢查時,instance爲空,那麼在兩次調用getInstance時可能會得到不同的結果,即使getInstance通常被認爲是返回相同的實例。

(2)複合條件
假定有兩個操作A和B,如果從執行A的線程來看,當另一個線程執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說是原子的。原子操作是指,對於訪問同一個狀態的所有操作(包括該操作本身)來說,這個操作是一個以原子方式(不可分割)執行的操作。

(3)內置鎖
Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包括兩部分:一個作爲鎖的對象引用,一個作爲由這個鎖保護的代碼塊。以Synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。靜態的synchronized方法以Class對象所謂鎖。
synchronized(lock){
//訪問或修改由鎖保護的共享狀態
}

每個Java對象都可以用來做一個實現同步的鎖,這些鎖稱爲內置鎖或監視器鎖。線程在進入同步代碼塊之前hi自動獲取鎖,並且在退出同步代碼塊時自動釋放鎖。獲得內置鎖的唯一途徑就是進入這個鎖保護的同步代碼塊或方法。
Java的內置鎖相當於一種互斥鎖,這意味着最多隻有一個線程能持有這種鎖,當線程A嘗試獲取一個由線程B持有的鎖時,線程A必須等待或者阻塞,直到線程B釋放這個鎖。如果B永遠不釋放鎖,那麼A也將永遠等待。
由這個鎖保護的同步代碼塊會以原子方式執行,多個線程在執行該代碼塊時也不會相互干擾。併發環境中的原子性和事務應用程序中的原子性有着相同的含義–一組語句作爲一個不可分割的單元被執行。任何一個執行同步代碼塊的線程,都不可能看到有其它線程正在執行由同一個鎖保護的同步代碼塊。
同步方法塊:
public synchronized void service(){

}

(4)重入
當某個線程請求一個由其它線程持有的鎖時,發出的請求的線程就會阻塞。然而,由於內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。“重入”意味着獲取鎖的操作的粒度是“線程”,而不是“調用”。
重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個持有者線程。當計數值爲0時,這個鎖就被認爲是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置爲1,如果當一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應的遞減。當計數值爲0時,這個鎖將被釋放。

發佈了104 篇原創文章 · 獲贊 56 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章