[本文是我對Java Concurrency In Practice C13的歸納和總結. ?轉載請注明作者和出處, ?如有謬誤, 歡迎在評論中指正. ]
任何java對象都可以用作同步的鎖, 為了便于區分, 將其稱為內置鎖.
JDK5.0引入了顯式鎖: Lock及其子類(如ReentrantLock, ReadWriteLock等).?
內置鎖和顯式鎖的區別有:
?
1. 可中斷申請
如果使用synchronized申請一個內置鎖時鎖被其他線程持有, 那么當前線程將被掛起, 等待鎖重新可用, 而且等待期間無法中斷. 而顯式鎖提供了可中斷申請: ??
public class InterruptedLock extends Thread { private static Lock lock = new ReentrantLock(); @Override public void run() { try { // 可中斷申請, 在申請鎖的過程中如果當前線程被中斷, 將拋出InterruptedException異常 lock.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("interruption happened"); return; } // 如果運行到這里, 說明已經申請到鎖, 且沒有發生異常 try { System.out.println("run is holding the lock"); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { try { lock.lock(); System.out.println("main is holding the lock."); Thread thread = new InterruptedLock(); thread.start(); // 1s后中斷thread線程, 該線程此時應該阻塞在lockInterruptibly方法上 Thread.sleep(1000); // 中斷thread線程將導致其拋出InterruptedException異常. thread.interrupt(); Thread.sleep(1000); } finally { lock.unlock(); } } }?
?
2. 嘗試型申請
Lock.tryLock和Lock.tryLock(long time, TimeUnit unit)方法用于嘗試獲取鎖. 如果嘗試沒有成功, 則返回false, 否則返回true. 而內置鎖則不提供這種特性, 一旦開始申請內置鎖, 在申請成功之前, 線程無法中斷, 申請也無法取消. Lock的嘗試型申請通常用于實現時間限定的task:
public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, 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 (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true; } } finally { // 成功申請到鎖時才需要釋放鎖 toAcct.lock.unlock(); } } } finally { // 成功申請到鎖時才需要釋放鎖 fromAcct.lock.unlock(); } } // 如果已經超過截止時間直接返回false, 說明轉賬沒有成功. 否則進行下次嘗試. if (System.nanoTime() < stopTime) return false; NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
嘗試型申請也是可中斷的.
?
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節.