從0學習java併發編程實戰-讀書筆記-顯式鎖(11)

Java5.0增長了一種新的機制:ReentrantLock,ReentrantLock並非一種替代內置加鎖的方法,而是當內置加鎖機制不適用時,做爲一種可選擇的高級功能。java

Lock和ReentrantLock

與內置的加鎖機制不一樣,Lcok提供了一種無條件的、可輪訓的、定時的以及可中斷的鎖獲取操做,全部的加鎖和解鎖的方法都是顯式的。在Lock的實現中必須提供與內部鎖相同的內存可見性語義,但在加鎖語義、調度算法、順序保證以及性能特性等方面能夠有所不一樣。算法

public interface Lock{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tyrLock(long timeout, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • ReentrantLock實現了Lock接口,並提供了與synchronized相同的互斥性內存可見性
  • 在獲取ReentrantLock時,有着與進入同步代碼塊相同的內存語義
  • 在釋放ReentrantLock時,有着與退出同步代碼塊相同的內存語義。
  • 與synchronized同樣,ReentrantLock還提供了可重入的加鎖語義
  • ReetrantLock支持在Lock接口中定義的全部獲取鎖模式
  • 而且與synchronized相比,它還爲處理鎖的不可用性提供了更好的靈活性

爲何要建立一種與內置鎖如此類似的新加鎖機制?性能優化

  • 在大多數狀況下,內置鎖都能很好的工做,可是在功能上存在一些侷限性,例如沒法中斷一個正在等待獲取鎖的線程,沒法在請求一個鎖時無限的等待下去。內置鎖必須在獲取該鎖的代碼塊中釋放,這就簡化了編碼,而且與異常處理操做實現了很好的交互,但卻沒法實現非阻塞結構的加鎖規則。在某些狀況下,一種更靈活的加鎖機制一般能提供更好的活躍性或性能。

Lock接口的標準使用形式

Lock lock = new ReentrantLock();
...
lock.lock();
try{
    // 更新對象狀態
    // 捕獲異常,並在必要時恢復不變性條件
} finally {
    lock.unlock();
}
Lock的使用比內置鎖複雜點,必須在finally中釋放鎖。不然若是在被保護的代碼中拋出了異常,那麼這個鎖將永遠不能被釋放。

輪訓鎖與定時鎖

可定時的可輪訓的鎖獲取模式是由tryLock方法實現的,與無條件的鎖獲取模式相比,它具備更完善的錯誤恢復機制。
在內置鎖中,死鎖是個至關嚴重的問題,恢復程序的惟一方法是從新啓動,而防止死鎖的惟一方法是在構造和編寫程序的時候避免出現不一致的鎖獲取順序。
而可定時的與可輪訓的鎖提供了另一種選擇,避免死鎖的發生。
若是不能得到全部須要的鎖,那麼可使用定時的或可輪訓的鎖獲取方式,從而使你從新得到控制權,它會釋放已經得到的鎖,而後從新嘗試獲取全部鎖。數據結構

經過tryLock來避免順序死鎖

public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException{
    long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
    long randMod = getRandomDelayModuluNanos(timeout, unit);
    long stopTime = System.nanoTime() + unit.toNanos(timeout);

    while(true){
        if(fromAcct.lock.tryLock()){
            try{
                if(toAcct.lock.tryLock()){
                    try{
                        doSomething();
                        return true;
                    } finally{
                        toAcct.lock.unlock();
                    }
                } 
            } finally{
                fromAcct.lock.unlock();
            }
        }

        if(System.nanoTime() < stopTime){
            return false;
        }
        NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
    }
}

使用tryLock來獲取兩個鎖,若是不能同時得到,那麼就回退並從新嘗試。在休眠時間裏包括固定部分和隨機部分,從而下降發生活鎖的可能性。若是在指定時間內不能得到全部須要的鎖,那麼將會返回一個false,從而使該操做平緩的失敗。併發

帶有時間限制的加鎖

在實現一個具備時間限制的操做時,定時鎖一樣很是有用。當在帶有時間限制的操做中調用了一個阻塞方法,它能根據剩餘時間來提供一個時限,若是操做不能在指定的時間給出結果,那麼就會使程序提早結束。使用內置鎖時,在開始請求鎖後,這個操做就沒法取消,所以內置鎖很難實現帶有時間限制的操做。dom

if(!lock.tryLock(nanosToLocks,NANOSECONDS)){
    return false;
}
try{
    doSomething();
} finally{
    lock.unlock();
}

可中斷的鎖獲取操做

正如定時的鎖獲取操做能在帶有時間限制的操做中使用獨佔鎖,可中斷的鎖獲取操做一樣能在可取消的操做中使用加鎖。
lockInterruptibly方法可以在得到鎖的同時保持對中斷的響應,而且因爲它包含在Lock中,所以無需建立其餘類型的不可中斷阻塞機制。jvm

public boolean sendOnSharedLine(String message) throws InterruptedException{
    lock.lockInterruptibly();
    try{
        return doSomething(message);
    } finally{
        lock.unlock();
    }
    private boolean doSomething(String message) throws InterruptedException{}
}

可中斷的鎖獲取操做的標準結構比普通的鎖獲取操做略微複雜一些,由於須要兩個try塊(若是在可中斷鎖操做中拋出InterruptedException,那麼只須要常規的try-finally便可)。函數

性能考慮因素

對於同步原語來講,競爭性能是可伸縮性的關鍵要素之一:若是有越多的資源被耗費在鎖的管理和調度上,那麼應用程序獲得的資源就越少。
鎖的實現越好,將須要越少的系統調用和上下文切換,而且在共享內存總線上的內存同步通訊量也越少,而一些耗時的操做將佔用應用程序的計算資源。
jdk 6使用了改進後的算法來管理內置鎖,與在ReentrantLock中使用的算法相似,該算法有效地提升了可伸縮性。內置鎖的性能不會因爲競爭而急劇降低,而且二者的可伸縮性也基本至關。高併發

公平性

在ReentrantLock的構造函數中提供了兩種公平性選擇:性能

  • 非公平的鎖(默認):容許插隊,當一個線程請求非公平的鎖時,若是在發出請求的同時,該鎖狀態變爲可用,那麼這個線程將跳過隊列中全部的等待線程並得到這個鎖(Semaphonre中一樣能夠選擇採起公平或者非公平的獲取順序)。
  • 公平的鎖:線程按照它們發出請求的順序來得到鎖

非公平的ReentrantLock並不提倡「插隊」行爲,但沒法防止某個線程在合適的時候進行「插隊」。(若是使用tryLock()方法,則得到一次插隊機會)

  • 在公平鎖中,若是有另外一個線程持有這個鎖或者有其餘線程在隊列中等待這個鎖,那麼新發出請求的線程將被放入隊列中。
  • 在非公平的鎖中,只有當鎖被某個線程持有的時候,新發出的請求才被放入隊列中。

咱們爲何不但願全部的鎖都是公平的

當執行加鎖操做時,公平性將因爲在掛起線程和恢復線程時候存在開銷而極大的下降性能。在實際狀況下,統計上的公平性保證(確保被阻塞的線程能最終得到鎖)一般已經夠用了,而且實際上開銷也能小不少。在大多數狀況下,非公平鎖的性能要高於公平鎖的性能。
在激烈競爭的狀況下,非公平鎖的性能高於公平鎖性能的一個緣由是:在恢復一個被掛起的線程於該線程真正開始運行之間存在着嚴重的延遲。
假設:線程A持有一個鎖,而且線程B請求這個鎖,因爲這個鎖已經被A持有了,所以B將被掛起,當A釋放鎖的時候,B將被喚醒,所以會再次嘗試獲取鎖。於此同時,若是C也請求鎖,那麼C有可能會在B被徹底喚醒以前,得到、使用以及釋放這個鎖。這樣是一個共贏局面:B得到鎖的時刻並無推遲,C更早地得到了鎖,而且吞吐量也得到了提升。

當持有鎖的時間比較長,或者請求鎖的平均時間間隔比較長,那麼應該使用公平鎖。在這種狀況下,經過插隊來提高吞吐量的狀況可能不會出現。
與默認的ReentrantLock同樣,內置加鎖並不會提供肯定的公平性保證,可是在大多數狀況下,在鎖實現上實現統計的上的公平性保證已經足夠了。java語言規範並無要求jvm以公平的方式來實現內置鎖,jvm也沒有這樣作。

讀寫鎖

ReentrantLock實現了一種標準的互斥鎖:每次最多隻有一個線程能持有ReentrantLock。但對於維護數據的完整性來講,互斥鎖一般是一種過於強硬的加鎖規則,因此也不太必要地限制了併發性。可是在許多狀況下,數據結構上的操做都是讀操做。此時,若是能放寬加鎖需求,容許多個讀操做同時訪問數據結構,而且讀取數據時候不會有其餘線程修改數據,那麼就不會有問題。
在如下狀況下可使用讀寫鎖:

  • 一個資源能夠被多個讀操做訪問,或者被一個寫操做訪問,可是二者不能同時進行。
ReadWriteLock接口
public interface ReadWriteLock{
    Lock readLock();
    Lock writeLock();
}

在讀寫鎖的加鎖策略中,容許多個讀同時進行,可是每次只容許一個寫操做。與Lock同樣,ReadWriteLock能夠採用多種不一樣的實現方式,這些方式在性能、調度保證、獲取優先性、公平性以及加鎖語義等方面可能有所不一樣。
讀寫鎖是一種性能優化措施,在一些特定的狀況下能夠實現更好的併發性,在實際狀況下,對於在多處理器上被頻繁讀取的數據結構,讀寫鎖可以提升性能。而在其餘狀況下,讀寫鎖的性能會比互斥鎖更差一點,由於它們的複雜性更高。
因爲ReadWriteLock使用Lock來實現鎖的讀寫部分,所以若是分析結果代表讀寫鎖沒有提升性能,那麼能夠很容易的將讀寫鎖換成獨佔鎖。
在讀取鎖和寫入鎖之間的交互能夠採用多種可選的實現方式

  • 釋放優先:當一個寫入操做釋放寫入鎖時,而且隊列中同時存在讀線程和寫線程,那麼應該優先選擇讀線程,寫線程,仍是最早發出請求的線程?
  • 讀線程插隊:若是鎖是由讀線程持有,可是寫線程正在等待,那麼新到達的讀線程可否當即得到訪問權,仍是應該在寫線程後面等待?若是容許讀線程插隊到寫線程以前,那麼能提升併發性,但卻可能形成寫線程發生飢餓問題?
  • 重入性:讀取鎖和寫入鎖是否能夠重入
  • 降級:若是一個線程持有寫入鎖,那麼它可否在不釋放該鎖的狀況下得到讀取鎖?這樣可能會使得寫入鎖被降級爲讀取鎖,同時不容許其餘寫線程修改被保護的資源。
  • 升級:讀取鎖可否優先於其餘正在等待的讀線程和寫線程而升級爲一個寫入鎖?在大多數的讀寫鎖實現中並不支持升級,由於若是沒有顯式的升級操做,很容易形成死鎖(若是兩個讀線程都試圖升級鎖,那麼兩者都不會釋放讀取鎖)。

ReentrantReadWriteLock爲這兩種鎖都提供了可重入的加鎖語義。與ReentrantLock相似,ReentrantReadWriteLock在構造的時候也能夠選擇一個非公平的鎖(默認)或者一個公平鎖。在公平的鎖中,等待最長的線程將優先得到鎖。若是這個鎖由讀線程持有,而另外一個線程請求寫入鎖,那麼其餘讀線程都不能得到讀取鎖,知道寫線程使用完成後並釋放寫入鎖。在非公平的鎖中,線程得到訪問許可的順序是不肯定的。寫線程能夠降級爲讀線程,可是不能從讀線程升級到寫線程。

與ReentrantLock相似的是,ReentrantReadWriteLock中的寫入鎖只能有惟一的全部者,而且只能由得到該鎖的線程來釋放。而讀取鎖經過記錄那些線程已經獲取了讀取鎖。

小結

與內置鎖相比,顯式的Lock提供了一些拓展功能,在處理鎖的不可用性方面有着更高的靈活性,而且對隊列行有着更好的控制。但ReentrantLock不能徹底替代synchronized,只有在synchronized沒法知足需求時,才應該使用它。讀寫鎖容許多個讀線程併發地訪問被保護的對象,當訪問以讀取操做爲主的數據結構時,它能提升程序的可伸縮性。

相關文章
相關標籤/搜索