《JAVA併發程式設計實戰》顯式鎖

sayWhat_sayHello發表於2018-10-30

Lock和ReentrantLock

public interface Lock{
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

Lock提供的是一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的。

使用Lock時,必須在finally塊中釋放鎖。否則,在被保護的程式碼中丟擲了異常,那麼這個鎖永遠都無法釋放。

Lock Lock = new ReentrantLock();
lock.lock();
try{
    
} finally {
    lock.unlock();
}

輪詢鎖和定時鎖

可定時的和可輪詢的鎖獲取模式是由tryLock方法實現的。在內建鎖中,死鎖是一個嚴重的問題,恢復程式的唯一方法是重新啟動程式,而防止死鎖的唯一方法就是在構造程式時避免出現不一致的鎖順序。可定時的和可輪詢的鎖提供了另外一種選擇:避免死鎖的發生。

如果不能獲得所有需要的鎖,那麼可以使用可定時的或可輪詢的鎖獲取方式,從而使你重新獲得控制權,它會釋放已經獲得的鎖,然後重新嘗試獲取所有鎖(或者至少會將這個失敗記錄到日誌,並採取其他措施)。

public boolean transferMoney(Account from,Account to,DollarAmount amount,long timeout,TimeUnit unit) throws InsufficientFundsException,InterruptedException {
    long fixedDelay = getFixedDelayComponentNanos(timeout,unit);
    long randMod = getRandomDelayComponentNanos(timeout,unit);
    long stopTime = System.nanoTime() + unit.toNanos(timeout);
    
    while(true){
        if(from.lock.tryLock()){
            try{
               if(to.lock.tryLock()){
                    try{
                        if(from.getBalance().compareTo(amount) < 0){
                            throw new InsufficientFundsException();
                        }else{
                            from.debit(amount);
                            to.credit(amount);
                            return true;
                        }
                    } finally {
                        to.lock.unlock();
                    } 
               } finally {
                   from.lock.unlock();
               }
        }
        if(System.nanoTime < stopTime){
            return false;
        }
        NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
    }
}

帶時間限制的加鎖

public boolean trySendOnsharedLine(String message,long timeout,TimeUnit unit) throws InterruptedException {
    long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
    
    if(!lock.tryLock(nanosToLock,NANOSECONDS)){
        return false;
    }
    try{
        return sendOnSharedLine(message);
    } finally {
        lock.unlock();
    }
}

PS:不先加鎖直接解鎖:

public class ReentrantLock_learning {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args){
        try {

        } finally {
            lock.unlock();
        }
    }
}
java.lang.IllegalMonitorStateException

可中斷的鎖獲取操作

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

private boolean cancellableSendOnSharedLine(String message) throws InterruptedException{
    ...
}

非塊結構的加鎖

在內建鎖中,鎖的獲取和釋放都是基於程式碼塊的。而降低鎖的粒度能提高程式碼的可伸縮性。

效能考慮因素

java5.0中,從單執行緒到多執行緒內建鎖的效能將急劇下降,但在java6.0中對內建鎖進行了優化,內建鎖的效能不會由於競爭而急劇下降。

公平性

在ReentrantLock的建構函式中提供了兩種公平性選擇:建立一個非公平的鎖或公平的鎖。在公平鎖上,執行緒將按照他們發出請求的順序來獲得鎖,但在非公平的鎖上,允許“插隊”。

在激烈競爭的情況下,非公平鎖額效能高於公平鎖的效能的一個原因是:在恢復一個被掛起的執行緒和該執行緒真正開始執行之間存在著嚴重的延遲。

在synchronized和ReentrantLock中選擇

在一些內建鎖無法滿足需求的情況下,ReentrantLock可以作為一種高階工具。當需要一些高階功能時才應該使用ReentrantLock,這些功能包括:可定時的、可輪詢的、可中斷的鎖獲取操作,公平佇列,以及非塊結構的鎖。

ReentrantLock的非塊結構特性仍然意味著獲取鎖的操作不能和特定的棧幀關聯起來,而內建鎖卻可以。線上程轉儲時有一定區別。

讀-寫鎖

ReentrantLock實現了一種標準的互斥鎖:每次最多隻有一個執行緒能持有ReentrantLock。但對於維護資料的完整性,互斥通常是一種過於強硬的加鎖規則,因此也就不必要的限制了併發性。

讀/寫鎖:一個資源可以被多個讀操作訪問,或者被一個寫操作訪問,但兩者不能同時進行。

public interface ReadWriteLock{
    Lock readLock();
    Lock writeLock();
}

在讀取鎖和寫入鎖之間的互動可以採用多種實現方式。ReadWriteLock中的一些可選實現包括:

  1. 釋放優先。當一個寫入操作釋放寫入鎖時,並且佇列中同時存在讀執行緒和寫執行緒,那麼應該優先選擇讀執行緒,寫執行緒還是最先請求的執行緒?
  2. 讀執行緒插隊。如果鎖是由讀執行緒持有,但有寫執行緒等待,那麼新到達的讀執行緒能否立即獲得訪問權,還是應該在寫執行緒後等待?如果允許讀執行緒插隊到寫執行緒之前,那麼將提高併發性,但卻可能造成寫執行緒飢餓的問題。
  3. 重入性。讀鎖和寫鎖能否重入?
  4. 降級。如果一個執行緒持有寫入鎖,那麼它能否在不釋放該鎖的情況下獲得讀鎖?這可能會使寫入鎖被降級為讀取鎖,同時不允許其他寫執行緒修改被保護的資源。
  5. 升級。讀鎖能否優先於其他正在等待的讀執行緒和寫執行緒而升級為一個寫入鎖?在大多數的讀-寫鎖實現中並不支援升級,因為如果沒有顯式的升級操作,那麼很容易造成死鎖。(如果兩個讀執行緒同時升級為寫入鎖,那麼兩者都不會釋放讀鎖)

ReentrantReadWriteLock在構造時也可以選擇是否為公平鎖。在公平鎖中,等待時間最長的執行緒將優先獲取鎖。如果這個鎖由讀執行緒持有,而另一個執行緒請求寫入鎖,那麼其他讀執行緒都不能獲得讀取鎖,直到寫執行緒使用完並且釋放了寫入鎖。在非公平的鎖中,執行緒獲得訪問許可的順序是不確定的。寫執行緒降級為讀執行緒是可以的,讀執行緒升級寫執行緒是不可以的。

public class ReadWriteMap<K,V>{
    private final Map<K,V> map;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();
    
    public ReadWriteMap(Map<K,V> map){
        this.map = map;
    }
    
    public V put(K key,V value){
        w.lock();
        try{
            return map.put(key,value);
        } finally {
            w.unlock();
        }
    }
    
    public V get(Object key){
        r.lock();
        try{
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
}

相關文章