Java併發編程學習筆記(二)線程安全性 2

內置鎖

    Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)。

/*
 *原文出處:http://liuxp0827.blog.51cto.com/5013343/1414349
 */
synchronzied (lock){
    //訪問或修改由鎖保護的共享狀態
}

    每個Java獨享都可以用作一個實現同步的鎖,這些鎖被稱爲內置鎖(Itrinsic Lock)或者監事鎖(Monitor Lock)。線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖。Java的內置鎖相當於一種互斥體,這意味着最多隻有一個線程能持有這種鎖。當線程1嘗試獲取一個由線程2持有的鎖時,線程1必須等待或者阻塞,知道線程2釋放這個鎖。由於這個鎖保護的同步代碼塊會以原子方式執行(一組語句作爲一個不可分割的單元被執行),多個線程在執行改代碼塊時互不干擾。我們再來改善下上篇博客 Java併發編程學習筆記(一)線程安全性 1 最後那段Servlet處理因數分解的代碼:

/*
 *原文出處:http://liuxp0827.blog.51cto.com/5013343/1414349
 */
 @ThreadSafe             
//並非好的代碼          
public class SynchronizedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    
    public synchronized void service(ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if(i.equals(lastNumber))
            encodeIntoResponse(resp,lastNumber.get());
        else{
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp,factors);
        }
    }
 }

    這個Servlet能正確地緩存最新的計算結果,但併發性卻非常糟糕,服務的響應性非常低,無法令人接受。這是一個性能問題,並不是線程安全的問題。


可重入   

線程安全:被多個併發的線程反覆調用時,他會產生正確的結果。可重入:當被多個線程調用的時候,不會引用任何共享數據。

    當某個線程請求一個由其他線程持有的鎖時,發出請求的線程就會阻塞。不過,內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。可重入的一種實現方法,是爲每個鎖關聯一個獲取計數值和一個所有者線程。當計數值爲0時,這個鎖就認爲是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置爲1.如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數值會相應地遞減。

/*
 *原文出處:http://liuxp0827.blog.51cto.com/5013343/1414349
 */
public class Widget {
    public synchronized void doSomething() {
      ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
      System.out.println(toString() + ": calling doSomething");
      super.doSomething();
    }
}

    上面代碼中,子類改寫了父類的synchronized方法,然後調用父類中的方法,此時如果沒有可重入的鎖,那麼這段代碼將死鎖。每個doSomething方法在執行前都會獲取Widget上的鎖,如果這個內置鎖不是可重入的,那麼在調用super.doSomething時將無法獲得Widget上的鎖。重入則避免了這樣的情況。可重入函數一定是線程安全的;線程安全的函數可能是重入的,也可能是不重入的;線程不安全的函數一定是不可重入的。


用鎖來保護狀態

    由於鎖能使其保護的代碼路徑以串行形式(多個線程依次以獨佔的方式訪問對象,而不是併發的訪問)來訪問,因此可以通過鎖來構造一些協議已實現對共享狀態的獨佔訪問。

    訪問共享狀態的複合操作,例如上篇博客中提到的命中計數器的遞增操作或者延遲初始化,都必須是原子操作。但僅僅將複合操作封裝到同步代碼塊是不夠的。如果用同步來協調對某個變量的訪問,那麼在訪問這個變量的所有位置上都需要使用同步,而且,使用鎖來協調對某個變量的訪問時,在訪問變量的所有位置上都要使用同一個鎖。對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,稱狀態變量是由這個鎖保護的。

    上述代碼SynchronizedFactorizer中,lastNumber和lastFactors都是有Servlet的內置鎖保護的,對象的內置鎖與其狀態之間沒有內在的關聯。雖然大多數類都將內置鎖用作一種有效的加鎖機制,但對象的域並不一定要通過內置所來保護。當線程獲取與對象關聯的鎖時,並不能阻止其他線程訪問對象,只能阻止其他線程獲得同一個鎖。

    一種常見的加鎖約定,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態的代碼路徑進行同步,使得在該對象上不會發生併發訪問。但是,如果在添加新的方法或者代碼路徑忘記了同步,那麼這種加鎖協議會很容易被破壞。

    當某個變量由鎖保護時,每次訪問這個變量時都需要首先獲得鎖,這樣就保證了同一時刻只有一個線程可以訪問這個變量。當類的不變形條件涉及到多個狀態變量的時候,每個變量都必須由同一個鎖來保護。因此可以在單個原子操作中訪問這些變量。

    未完待續... Java併發編程學習筆記(三)線程安全性 3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章