1. 程式人生 > >內建鎖和顯式鎖的區別(java併發程式設計第13章)

內建鎖和顯式鎖的區別(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.     @Override  
  4.     public void run() {  
  5.         try {  
  6.             // 可中斷申請, 在申請鎖的過程中如果當前執行緒被中斷, 將丟擲InterruptedException異常  
  7.             lock.lockInterruptibly();  
  8.         } catch (InterruptedException e) {  
  9.             System.out.println("interruption happened");  
  10.             return
    ;  
  11.         }  
  12.         // 如果執行到這裡, 說明已經申請到鎖, 且沒有發生異常  
  13.         try {  
  14.             System.out.println("run is holding the lock");  
  15.         } finally {  
  16.             lock.unlock();  
  17.         }  
  18.     }  
  19.     public static void main(String[] args) throws InterruptedException {  
  20.         try {  
  21.             lock.lock();  
  22.             System.out.println("main is holding the lock.");  
  23.             Thread thread = new InterruptedLock();  
  24.             thread.start();  
  25.             // 1s後中斷thread執行緒, 該執行緒此時應該阻塞在lockInterruptibly方法上  
  26.             Thread.sleep(1000);  
  27.             // 中斷thread執行緒將導致其丟擲InterruptedException異常.  
  28.             thread.interrupt();  
  29.             Thread.sleep(1000);  
  30.         } finally {  
  31.             lock.unlock();  
  32.         }  
  33.     }  
  34. }   

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.     while (true) {  
  8.         if (fromAcct.lock.tryLock()) {  
  9.             try {  
  10.                 if (toAcct.lock.tryLock()) {  
  11.                     try {  
  12.                         if (fromAcct.getBalance().compareTo(amount) < 0)  
  13.                             throw new InsufficientFundsException();  
  14.                         else {  
  15.                             fromAcct.debit(amount);  
  16.                             toAcct.credit(amount);  
  17.                             return true;  
  18.                         }  
  19.                     } finally {  
  20.                         // 成功申請到鎖時才需要釋放鎖  
  21.                         toAcct.lock.unlock();  
  22.                     }  
  23.                 }  
  24.             } finally {  
  25.                 // 成功申請到鎖時才需要釋放鎖  
  26.                 fromAcct.lock.unlock();  
  27.             }  
  28.         }  
  29.         // 如果已經超過截止時間直接返回false, 說明轉賬沒有成功. 否則進行下次嘗試.  
  30.         if (System.nanoTime() < stopTime)  
  31.             return false;  
  32.         NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);  
  33.     }  
  34. }  

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

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節.