注:本篇博客部分內容引用自: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的內置屬性。
總結
- 清楚爲什麼有Lock接口;
- 清楚使用ReentrantLock有什麼優缺點;
- 掌握如何使用ReentrantLock(定時鎖,輪詢鎖,中斷鎖以及一些其他功能);
- 能夠在synchronized與Lock中做出選擇。
參考閱讀
Java併發編程實戰