內置鎖和顯式鎖的區別(java併發編程第13章)

任何java對象都可以用作同步的鎖, 爲了便於區分, 將其稱爲內置鎖.

JDK5.0引入了顯式鎖: Lock及其子類(如ReentrantLock, ReadWriteLock等). 

內置鎖和顯式鎖的區別有:

 

1. 可中斷申請

如果使用synchronized申請一個內置鎖時鎖被其他線程持有, 那麼當前線程將被掛起, 等待鎖重新可用, 而且等待期間無法中斷. 而顯式鎖提供了可中斷申請:   

Java代碼  收藏代碼
  1. public class InterruptedLock extends Thread {  
  2.     private static Lock lock = new ReentrantLock();  
  3.   
  4.     @Override  
  5.     public void run() {  
  6.         try {  
  7.             // 可中斷申請, 在申請鎖的過程中如果當前線程被中斷, 將拋出InterruptedException異常  
  8.             lock.lockInterruptibly();  
  9.         } catch (InterruptedException e) {  
  10.             System.out.println("interruption happened");  
  11.             return;  
  12.         }  
  13.   
  14.         // 如果運行到這裏, 說明已經申請到鎖, 且沒有發生異常  
  15.         try {  
  16.             System.out.println("run is holding the lock");  
  17.         } finally {  
  18.             lock.unlock();  
  19.         }  
  20.     }  
  21.   
  22.     public static void main(String[] args) throws InterruptedException {  
  23.         try {  
  24.             lock.lock();  
  25.             System.out.println("main is holding the lock.");  
  26.             Thread thread = new InterruptedLock();  
  27.             thread.start();  
  28.             // 1s後中斷thread線程, 該線程此時應該阻塞在lockInterruptibly方法上  
  29.             Thread.sleep(1000);  
  30.             // 中斷thread線程將導致其拋出InterruptedException異常.  
  31.             thread.interrupt();  
  32.             Thread.sleep(1000);  
  33.         } finally {  
  34.             lock.unlock();  
  35.         }  
  36.     }  
  37. }   

 

2. 嘗試型申請

Lock.tryLock和Lock.tryLock(long time, TimeUnit unit)方法用於嘗試獲取鎖. 如果嘗試沒有成功, 則返回false, 否則返回true. 而內置鎖則不提供這種特性, 一旦開始申請內置鎖, 在申請成功之前, 線程無法中斷, 申請也無法取消. Lock的嘗試型申請通常用於實現時間限定的task:

Java代碼  收藏代碼
  1. public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit)  
  2.         throws InsufficientFundsException, InterruptedException {  
  3.     long fixedDelay = getFixedDelayComponentNanos(timeout, unit);  
  4.     long randMod = getRandomDelayModulusNanos(timeout, unit);  
  5.     // 截止時間  
  6.     long stopTime = System.nanoTime() + unit.toNanos(timeout);  
  7.   
  8.     while (true) {  
  9.         if (fromAcct.lock.tryLock()) {  
  10.             try {  
  11.                 if (toAcct.lock.tryLock()) {  
  12.                     try {  
  13.                         if (fromAcct.getBalance().compareTo(amount) < 0)  
  14.                             throw new InsufficientFundsException();  
  15.                         else {  
  16.                             fromAcct.debit(amount);  
  17.                             toAcct.credit(amount);  
  18.                             return true;  
  19.                         }  
  20.                     } finally {  
  21.                         // 成功申請到鎖時才需要釋放鎖  
  22.                         toAcct.lock.unlock();  
  23.                     }  
  24.                 }  
  25.             } finally {  
  26.                 // 成功申請到鎖時才需要釋放鎖  
  27.                 fromAcct.lock.unlock();  
  28.             }  
  29.         }  
  30.         // 如果已經超過截止時間直接返回false, 說明轉賬沒有成功. 否則進行下次嘗試.  
  31.         if (System.nanoTime() < stopTime)  
  32.             return false;  
  33.         NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);  
  34.     }  
  35. }  

嘗試型申請也是可中斷的.

 

3. 鎖的釋放

對於內置鎖, 只要代碼運行到同步代碼塊之外, 就會自動釋放鎖, 開發者無需擔心拋出異常, 方法返回等情況發生時鎖會沒有被釋放的問題. 然而對於顯式鎖, 必須調用unlock方法才能釋放鎖. 此時需要開發者自己處理拋出異常, 方法返回等情況. 通常會在finally代碼塊中進行鎖的釋放, 還需注意只有申請到鎖之後才需要釋放鎖, 釋放未持有的鎖可能會拋出未檢查異常.

所以使用內置鎖更容易一些, 而顯式鎖則繁瑣很多. 但是顯式鎖釋放方式的繁瑣也帶來一個方便的地方: 鎖的申請和釋放不必在同一個代碼塊中.

 

4. 公平鎖

通過ReentrantLock(boolean fair)構造函數創建ReentranLock鎖時可以爲其指定公平策略, 默認情況下爲不公平鎖.

多個線程申請公平鎖時, 申請時間早的線程優先獲得鎖. 然而不公平鎖則允許插隊, 當某個線程申請鎖時如果鎖恰好可用, 則該線程直接獲得鎖而不用排隊. 比如線程B申請某個不公平鎖時該鎖正在由線程A持有, 線程B將被掛起. 當線程A釋放鎖時, 線程B將從掛起狀態中恢復並打算再次申請(這個過程需要一定時間). 如果此時恰好線程C也來申請鎖, 則不公平策略允許線程C立刻獲得鎖並開始運行. 假設線程C在很短的一段時間之後就釋放了鎖, 那麼可能線程B還沒有完成恢復的過程. 這樣一來, 節省了線程C從掛起到恢復所需要的時間, 還沒有耽誤線程B的運行. 所以在鎖競爭激烈時, 不公平策略可以提高程序吞吐量.

內置鎖採用不公平策略, 而顯式鎖則可以指定是否使用不公平策略.

 

5. 喚醒和等待

線程可以wait在內置鎖上, 也可以通過調用內置鎖的notify或notifyAll方法喚醒在其上等待的線程. 但是如果有多個線程在內置鎖上wait, 我們無法精確喚醒其中某個特定的線程.

顯式鎖也可以用於喚醒和等待. 調用Lock.newCondition方法可以獲得Condition對象, 調用Condition.await方法將使得線程等待, 調用Condition.singal或Condition.singalAll方法可以喚醒在該Condition對象上等待的線程. 由於同一個顯式鎖可以派生出多個Condition對象, 因此我們可以實現精確喚醒. 具體的應用請參考我早期的一篇博文:http://coolxing.iteye.com/blog/1236696

 

鎖優化

JDK5.0加入顯式鎖後, 開發者發現顯式鎖相比內置鎖具有明顯的性能優勢, 再加上顯式鎖的諸多新特性, 很多文章和書籍都推薦使用顯式鎖代替內置鎖. 然而JDK6.0對內置鎖做了大量優化, 顯式鎖已經不具備明顯的性能優勢. 所以如果使用的是JDK6.0及之後的版本, 且沒有使用到顯式鎖提供的新特性, 則沒有必要刻意使用顯式鎖, 原因如下:

1. 內置鎖是JVM的內置特性, 更容易進行優化.

2. 監控程序(如thread dump)對內置鎖具有更好的支持. 

3. 大多數開發者更熟悉內置鎖.

JDK6.0對內置鎖所做的優化措施可以參見"深入理解java虛擬機"13.3節.

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