synchronized and Reentrantlock
多線程編程中,當代碼需要同步時我們會用到鎖。Java為我們提供了內置鎖(synchronized
)和顯式鎖(ReentrantLock
)兩種同步方式。顯式鎖是JDK1.5引入的,這兩種鎖有什么異同呢?是僅僅增加了一種選擇還是另有其因?本文為您一探究竟。
內置鎖
Java內置鎖通過synchronized關鍵字使用,使用其修飾方法或者代碼塊,就能保證方法或者代碼塊以同步方式執行。使用起來非常近簡單,就像下面這樣:
// synchronized關鍵字用法示例
public synchronized void add(int t){// 同步方法
this.v += t;
}
public static synchronized void sub(int t){// 同步靜態方法
value -= t;
}
public int decrementAndGet(){
synchronized(obj){// 同步代碼塊
return --v;
}
}
這就是內置鎖的全部用法,你已經學會了。
內置鎖使用起來非常方便,不需要顯式的獲取和釋放,任何一個對象都能作為一把內置鎖。使用內置鎖能夠解決大部分的同步場景。“任何一個對象都能作為一把內置鎖”也意味着出現synchronized關鍵字的地方,都有一個對象與之關聯,具體說來:
- 當synchronized作用於普通方法是,鎖對象是this;
- 當synchronized作用於靜態方法是,鎖對象是當前類的Class對象;
- 當synchronized作用於代碼塊時,鎖對象是synchronized(obj)中的這個obj。
顯式鎖
內置鎖這么好用,為什么還需多出一個顯式鎖呢?因為有些事情內置鎖是做不了的,比如:
- 我們想給鎖加個等待時間超時時間,超時還未獲得鎖就放棄,不至於無限等下去;
- 我們想以可中斷的方式獲取鎖,這樣外部線程給我們發一個中斷信號就能喚起等待鎖的線程;
- 我們想為鎖維持多個等待隊列,比如一個生產者隊列,一個消費者隊列,一邊提高鎖的效率。
顯式鎖(ReentrantLock)正式為了解決這些靈活需求而生。ReentrantLock的字面意思是可重入鎖,可重入的意思是線程可以同時多次請求同一把鎖,而不會自己導致自己死鎖。下面是內置鎖和顯式鎖的區別:
-
可定時:
RenentrantLock.tryLock(long timeout, TimeUnit unit)
提供了一種以定時結束等待的方式,如果線程在指定的時間內沒有獲得鎖,該方法就會返回false並結束線程等待。 -
可中斷:你一定見過InterruptedException,很多跟多線程相關的方法會拋出該異常,這個異常並不是一個缺陷導致的負擔,而是一種必須,或者說是一件好事。可中斷性給我們提供了一種讓線程提前結束的方式(而不是非得等到線程執行結束),這對於要取消耗時的任務非常有用。對於內置鎖,線程拿不到內置鎖就會一直等待,除了獲取鎖沒有其他辦法能夠讓其結束等待。
RenentrantLock.lockInterruptibly()
給我們提供了一種以中斷結束等待的方式。 -
條件隊列(condition queue):線程在獲取鎖之后,可能會由於等待某個條件發生而進入等待狀態(內置鎖通過
Object.wait()
方法,顯式鎖通過Condition.await()
方法),進入等待狀態的線程會掛起並自動釋放鎖,這些線程會被放入到條件隊列當中。synchronized對應的只有一個條件隊列,而ReentrantLock可以有多個條件隊列,多個隊列有什么好處呢?請往下看。 -
條件謂詞:線程在獲取鎖之后,有時候還需要等待某個條件滿足才能做事情,比如生產者需要等到“緩存不滿”才能往隊列里放入消息,而消費者需要等到“緩存非空”才能從隊列里取出消息。這些條件被稱作條件謂詞,線程需要先獲取鎖,然后判斷條件謂詞是否滿足,如果不滿足就不往下執行,相應的線程就會放棄執行權並自動釋放鎖。使用同一把鎖的不同的線程可能有不同的條件謂詞,如果只有一個條件隊列,當某個條件謂詞滿足時就無法判斷該喚醒條件隊列里的哪一個線程;但是如果每個條件謂詞都有一個單獨的條件隊列,當某個條件滿足時我們就知道應該喚醒對應隊列上的線程(內置鎖通過
Object.notify()
或者Object.notifyAll()
方法喚醒,顯式鎖通過Condition.signal()
或者Condition.signalAll()
方法喚醒)。這就是多個條件隊列的好處。
使用內置鎖時,對象本身既是一把鎖又是一個條件隊列;使用顯式鎖時,RenentrantLock的對象是鎖,條件隊列通過RenentrantLock.newCondition()
方法獲取,多次調用該方法可以得到多個條件隊列。
一個使用顯式鎖的典型示例如下:
// 顯式鎖的使用示例
ReentrantLock lock = new ReentrantLock();
// 獲取鎖,這是跟synchronized關鍵字對應的用法。
lock.lock();
try{
// your code
}finally{
lock.unlock();
}
// 可定時,超過指定時間為得到鎖就放棄
try {
lock.tryLock(10, TimeUnit.SECONDS);
try {
// your code
}finally {
lock.unlock();
}
} catch (InterruptedException e1) {
// exception handling
}
// 可中斷,等待獲取鎖的過程中線程線程可被中斷
try {
lock.lockInterruptibly();
try {
// your code
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
// exception handling
}
// 多個等待隊列,具體參考[ArrayBlockingQueue](https://github.com/CarpenterLee/JCRecipes/blob/master/markdown/ArrayBlockingQueue.md)
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();
注意,上述代碼將unlock()
放在finally塊里,這么做是必需的。顯式鎖不像內置鎖那樣會自動釋放,使用顯式鎖一定要在finally塊中手動釋放,如果獲取鎖后由於異常的原因沒有釋放鎖,那么這把鎖將永遠得不到釋放!將unlock()放在finally塊中,保證無論發生什么都能夠正常釋放。
結論
內置鎖能夠解決大部分需要同步的場景,只有在需要額外靈活性是才需要考慮顯式鎖,比如可定時、可中斷、多等待隊列等特性。
顯式鎖雖然靈活,但是需要顯式的申請和釋放,並且釋放一定要放到finally塊中,否則可能會因為異常導致鎖永遠無法釋放!這是顯式鎖最明顯的缺點。
綜上,當需要同步時請優先考慮更安全的更易用的隱式鎖。
參考文獻
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html