java多線程9.使用顯式鎖
在協調共享對象的訪問時可以使用的機制有synchronized和volatile。java 5.0新增了一種新的機制:ReentrankLock。
ReentrankLock並不是一種替代內置加鎖的方法,而是當內置加鎖機制不適用時,作為一種可選擇的高級功能。與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制,而且它能夠支持中斷。
Lock與ReentrantLock
Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯示地。
在Lock的實現中必須提供與內部鎖相同的內存可見性語義,但在加鎖語義、調度算法、順序保證以及性能特性等方面可以有所不同。
publicinterface Lock{ void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
ReentrantLock實現了Lock接口,並提供了與synchronized相同的互斥性和內存可見性。在獲取ReentrantLock時,有著與進入同步代碼塊相同的內存語義,在釋放ReentrantLock時,同樣有著與退出同步代碼塊相同的內存語義。
ReentrantLock支持在Lock接口中定義的所有獲取鎖模式,並且為處理鎖的不可用性問題提供了更高的靈活性。
Lock接口的標準使用形式:
/** * 必須在finally塊中釋放鎖,否則,如果在被保護的代碼中拋出了異常,那麽這個鎖永遠都無法釋放。 * 當使用加鎖時,還必須考慮在try塊中拋出異常的情況 * 如果可能使對象處於某種不一致的狀態,那麽就需要更多的try-catch或try-finally代碼塊。 * 如果沒有使用finally來釋放Lock,那麽相當於啟動了一個定時炸彈。當炸彈爆炸時,將很難追蹤到最初發生錯誤的位置,因為沒有記錄應該釋放鎖的位置和時間。 * 這就是ReentrantLock不能完全替代synchronized的原因,它更加危險。在java6之後,改進了算法來管理內置鎖。*/ protected void terminated(){ Lock lock = new ReentrantLock(); lock.lock(); try{ //更新對象狀態 //捕獲異常,並在必要時恢復不變性條件 }finally{ lock.unlock(); } }
輪詢鎖與定時鎖
可定時與可輪詢的鎖獲取模式由tryLock方法實現,與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。
在內置鎖中,死鎖是一個嚴重的問題,恢復程序的唯一方法是重新啟動程序,而防止死鎖的唯一方法就是在構造程序時避免出現不一致的鎖順序。
可定時與可輪詢的鎖提供了另一種選擇:避免死鎖的發生。
/** * 示例:通過tryLock來避免鎖順序死鎖。 * 使用tryLock來獲取兩個鎖,如果不能同時獲得,那麽就回退並重新嘗試。在休眠時間中包括固定部分和隨機部分,從而降低發生活鎖的可能性。 * 如果在指定時間內不能獲得所有需要的鎖,那麽transferMoney將返回一個失敗的狀態。 * * @param fromAcct * @param toAcct * @param amount * @param timeout * @param unit * @return * @throws InterruptedException */ public boolean transferMoney(Account fromAcct,Account toAcct, DollarAmount amount,long timeout,TimeUnit unit)throws 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.getBlance()compareTo(amount) < 0){ throw new InsufficientFundsException(); }else{ fromAcct.debit(amount); toAcct.credit(amount); return true; } }finally{ toAcct.lock.unlock(); } } }finally{ fromAcct.lock.unlock(); } } if(System.nanoTime() < stopTime){ return false; } TimeUnit.NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod); } }
/** * 示例:帶有時間限制的加鎖 * 確保對資源進行串行訪問的方法:一個單線程Executor;另一個方法是使用一個獨占鎖來保護對資源的訪問。 * 試圖在Lock保護的共享通信線路上發送一條消息,如果不能再指定時間內完成,代碼就會失敗。tryLock能夠在這種帶有時間限制的操作中實現獨占的加鎖行為。 */ Lock lock = new ReentrantLock(); public boolean trySendOnSharedLine(String meassage,long timeout,TimeUnit unit) throws InterruptedException{ long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(meassage); if(lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)){ return false; } try{ return sendOnSharedLine(meassage); }finally{ lock.unlock(); } }
/** * 示例:可中斷的鎖獲取操作 * lockInterruptibly方法能夠在獲得鎖的同時保持對中斷的響應,並且由於它包含在Lock中,因此無須創建其他類型的不可中斷阻塞機制。 * * @param message * @return * @throws InterruptedException */ public boolean sendOnSharedLine(String message) throws InterruptedException{ lock.lockInterruptibly(); try{ return cancellableSendOnSharedLine(meassage); }finally{ lock.unlock(); } } private boolean cancellableSendOnSharedLine(String message) throws InterruptedException{ //... return false; }
公平性
ReentrantLock的構造函數中提供了兩種公平性的選擇:創建一個非公平的鎖默認(或者一個公平的鎖)。
在公平的鎖上,線程將按照它們發出請求的順序來獲得鎖,但在非公平的鎖上,則允許插隊:
當一個線程請求非公平的鎖時,如果在發出請求的同時該鎖的狀態變為可用,那麽這個線程將跳過隊列中所有的等待線程並獲得這個鎖,而在公平的鎖中,如果另一個線程持有這個鎖或者有其他線程在隊列中等待這個鎖,那麽新發出請求的線程將被放入隊列中。在非公平的鎖中,只有當鎖被某個線程持有時,新發出請求的線程才會被放入隊列中。因此當執行加鎖操作時,公平性將由於在掛起線程和恢復線程時存在的開銷而極大地降低性能。
在synchronized和ReentrantLock之間的選擇
在一些內置鎖無法滿足需求的情況下,ReentrantLock可以作為一種高級工具。當需要一些高級功能時才應該使用ReentrantLock,這些功能包括:可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。否則,還是應該優先使用synchronized。
java5中,內置鎖與ReentrantLock相比還有另一個優點:在線程轉儲中能給出在哪些調用幀中獲得了哪些鎖,並能檢測和識別發生死鎖的線程。而JVM並不知道哪些線程持有ReentrantLock,因此在調試使用ReentrantLock的線程的問題時,將起不到幫助作用。java6解決了這個問題,它提供了一個管理和調試接口,鎖可以通過該接口進行註冊,從而與ReentrantLock相關的加鎖信息就能出現在線程轉儲中,並通過其他的管理接口和調試接口來訪問。
synchronized是JVM的內置屬性,它能執行一些優化,例如對線程封閉的鎖對象的鎖消除優化,通過增加鎖的粒度來消除內置鎖的同步,而如果通過基於類庫的鎖來實現這些功能,則可能性不大。
讀 - 寫鎖
public interface ReadWriteLock{ Lock readLock(); Lock WriteLock(); }
實現ReadWriteLock需要考慮的一些問題:
釋放優先:當一個寫入操作釋放寫入鎖時,並且隊列中同時存在讀線程和寫線程,那麽應該優先選擇讀線程,寫線程,還是最先發出請求的線程。
讀線程插入:如果鎖是由讀線程持有,但有寫線程正在等待,那麽新到達的讀線程能否立即獲得訪問權,還是應該在寫線程後面等待?如果允許讀線程插隊到寫線程之前,那麽將提高並發性,但卻可能造成寫線程發生饑餓問題。
重入性:讀取鎖和寫入鎖是否是可重入的。
降級:如果一個線程持有寫入鎖,那麽它能否在不釋放該鎖的情況下獲得讀取鎖?這可能會使得寫入鎖被降級為讀取鎖,同時不允許其他寫線程修改被保護的資源。
升級:讀取鎖能否優先於其他正在等待的讀線程和寫線程而升級為一個寫入鎖?在大多數的讀寫鎖實現中並不支持升級,因為如果沒有顯示地升級操作,那麽很容易造成死鎖(如果兩個讀線程試圖同時升級為寫入鎖,那麽二者都不會釋放讀取鎖)。
ReentrantReadWriteLock為這兩種鎖都提供了可重入的加鎖語義,默認也是非公平鎖。
當鎖的持有時間較長並且大部分操作都不會修改被守護的資源時,那麽讀寫鎖能提高並發性。
/** * ReadWriteMap中使用了ReentrantReadWriteLock來包裝Map,從而使它能夠在多個線程之間被安全的共享,並且能夠避免讀寫和謝謝沖突。 * 實際上ConcurrentHashMap的性能已經很好了,如果需要對另一種Map實現如LinkedHashMap提供並發性更高的訪問,可以考慮。 * * @param <K> * @param <V> */ public class ReadWriteMap<K,V> { private final Map<K,V> map; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock r = lock.readLock(); private final Lock w = lock.writeLock(); public ReadWriteMap(Map<K,V> map){ this.map = map; } public V put(K key,V value){ w.lock(); try{ return map.put(key, value); }finally{ w.unlock(); } } //remove() putAll() clear() public V get(Object key){ r.lock(); try{ return map.get(key); }finally{ r.unlock(); } } }
#筆記內容參考 《java並發編程實戰》
java多線程9.使用顯式鎖