Java併發--深入理解顯式鎖

:本篇博客部分內容引用自:Java併發編程:Lock

引言

在Java 5.0之前,協調對共享對象的訪問可以使用到的機制只有synchronized和volatile。在Java 5.0之後,增加了一種新的機制:ReentrantLock。ReentrantLock並不是一種替代內置鎖的方法,而是在內置鎖不再適用的情況下,作爲一種可選擇的高級功能。


既生synchronized,何生Lock

synchronized主要在功能上存在一些侷限性。

如果獲取鎖的線程由於要等待IO或者其他原因(比如調用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能乾巴巴地等待,試想一下,這多麼影響程序執行效率。因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。

如果採用synchronized關鍵字來實現同步的話,就會導致一個問題:如果多個線程都只是進行讀操作,那麼當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生衝突,通過Lock就可以辦到。

另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。

值得注意的是:在使用Lock時,我們必須在finally塊中釋放鎖!

如果在被保護的代碼塊中拋出了異常,那麼這個鎖永遠都無法被釋放。如果沒有使用finally來釋放鎖,當出現問題時,將很難追蹤到最初發生錯誤的位置,因爲我們沒有記錄應該釋放鎖的位置與時間。

這就是ReentrantLock不能完全替代synchronized的原因:它更加危險,因爲當程序的執行控制離開被保護的代碼塊時,不會自動清除鎖。

:FindBugs可以幫助你找到未釋放的鎖。

Lock接口

認識Lock

我們先來看一下Lock接口的實現:

public interface Lock {
    // 加鎖
    void lock();
    // 可中斷的鎖
    void lockInterruptibly() throws InterruptedException;
    // 輪詢鎖與定時鎖
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解鎖
    void unlock();
    // 本節並不需要關注
    Condition newCondition();
}

ReentrantLock是唯一實現了Lock接口的類。在獲取(釋放)ReentrantLock時,有着與進入(退出)同步代碼塊相同的內存語義,與synchronized一樣,ReentrantLock還提供了可重入的加鎖語義。

tryLock方法

tryLock只有在成功獲取了鎖的情況下才會返回true,如果別的線程當前正持有鎖,則會立即返回false!如果爲這個方法加上timeout參數,則會等待timeout的時間纔會返回false或者在獲取到鎖的時候返回true。

在內置鎖中,死鎖是一個嚴重的問題,恢復程序的唯一方法是重啓程序,而防止死鎖的唯一方法就是在構造程序時避免出現不一致的鎖順序。可定時與可輪詢的鎖提供了另一種方式:避免死鎖的發生。

如果不能獲取所有需要的鎖,那麼可以使用可定時或可輪詢的鎖獲取方式,從而使你重新獲得控制權,它會釋放已經獲得的鎖,然後重新嘗試獲取所有鎖。無參數的tryLock一般用作輪詢鎖,而帶有TimeUnit參數的一般用作定時鎖。

考慮如下程序,它將資金從一個賬戶轉入另一個賬戶。在開始轉賬之前,首先要獲得這兩個Account對象的鎖,以確保通過原子方式來更新兩個賬戶中的餘額,同時又不破壞一些不變性條件,如:“賬戶的餘額不能爲負數”。

public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) 
        throws InsufficientResourcesException {
    synchronized (fromAccount) {
        synchronized (toAccount) {
            if (fromAccount.getBalance().compareTo(amount) < 0) {
                throw  new InsufficientResourcesException();
            } else {
                fromAccount.debit(amount);
                toAccount.credit(amount);
            }
        }
    }
}

這個程序看似無害,實則會發生死鎖。如果兩個線程同時調用transferMoney,其中一個線程從X向Y轉賬,另一個線程從Y向X轉賬,那麼就會發生死鎖:

A: transferMoney(myAccount, yourAccount, 10);
B: transferMoney(yourAccount, myAccount, 20);

如果執行順序不當,那麼A可能獲得myAccount的鎖並等待yourAccount的鎖,然而B此時持有yourAccount的鎖並等待myAccount的鎖,就會發生死鎖。

我們可以使用tryLock用作輪詢鎖來解決這樣的問題,使用tryLock來獲取兩個鎖,如果不能同時獲得,則退回並重新嘗試。程序中鎖獲取的休眠時間包括固定部分和隨機部分,從而降低了發生活鎖的可能性。如果在指定時間內不能獲得所有需要的鎖,那麼transferMoney將返回一個失敗狀態,從而使該操作平緩的失敗。

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

tryLock用作定時鎖的程序如下:

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 trySendOnSharedLine(message);
    } finally {
        lock.unlock();
    }
} 

上述程序試圖在Lock保護的共享通信線路上發送一條消息,如果不能在指定的時間內完成,代碼就會失敗。定時的tryLock能夠在這種帶有時間限制的操作中實現獨佔加鎖的行爲。

lockInterruptibly方法

lockInterruptibly方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

由於在lockInterruptibly方法的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly的方法外聲明拋出InterruptedException。

因此lockInterruptibly一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     // .....
    } finally {
        lock.unlock();
    }  
}

注意,當一個線程獲取了鎖之後,是不會被interrupt方法中斷的。因爲單獨調用interrupt方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。因此當通過lockInterruptibly方法獲取某個鎖時,如果不能獲取到,只有在進行等待的情況下,是可以響應中斷的。

定時的tryLock同樣能夠響應中斷,因此當需要實現一個定時的和可中斷的鎖獲取操作時,可以使用tryLock方法。


公平鎖

公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖,這種就是公平鎖。

非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。

在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置爲公平鎖。

在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。我們可以在創建ReentrantLock對象時,通過以下方式來設置鎖的公平性:

ReentrantLock lock = new ReentrantLock(true);

參數爲true表示爲公平鎖,爲fasle爲非公平鎖。默認情況下,如果使用無參構造器,則是非公平鎖。

另外在ReentrantLock類中定義了很多方法,比如:

isFair()            //判斷鎖是否是公平鎖

isLocked()          //判斷鎖是否被任何線程獲取了

isHeldByCurrentThread()     //判斷鎖是否被當前線程獲取了

hasQueuedThreads()          //判斷是否有線程在等待該鎖

在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置爲公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。


在synchronized與ReentrantLock之間進行抉擇

在性能上,Java 5.0中ReentrantLock遠遠優於內置鎖,而在Java 6.0中則是略有勝出。

我們建議,僅當內置鎖不能滿足需求時,纔可以考慮使用ReentrantLock。

在Java 8.0中,內置鎖的性能已經不壓於ReentrantLock,並且未來更可能會繼續提升synchronized的性能,畢竟synchronized是JVM的內置屬性。


總結

  1. 清楚爲什麼有Lock接口;
  2. 清楚使用ReentrantLock有什麼優缺點;
  3. 掌握如何使用ReentrantLock(定時鎖,輪詢鎖,中斷鎖以及一些其他功能);
  4. 能夠在synchronized與Lock中做出選擇。

參考閱讀

Java併發編程實戰

Java併發編程:Lock

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