從零開始學多執行緒之顯示鎖(十一)
synchronzied雖好,但是有其侷限性,本篇部落格為您介紹更高階的鎖--顯示鎖
ReentrantLock(重進入鎖)並不是作為內部鎖(synchronized)機制的替代,而是當內部鎖被證明受到侷限時,提供可選擇的高階特性.
1. Lock 和 ReentrantLock
Lock介面:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
與內部加鎖機制不同,Lock提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯示的.
Lock的實現必須提供具有與內部加鎖相同的記憶體可見性的語義.但是加鎖的語義、排程演算法、順序保證,效能特性這些可以不同.
ReentrantLock實現了Lock介面,提供了與synchronized相同的互斥和記憶體可見性的保證.
獲得ReentrantLock的鎖與進入synchronized塊有著相同的記憶體語義,釋放ReentrantLock鎖與退出synchronized塊有相同的記憶體語義.
ReentrantLock提供了與synchronized一樣的可重入加鎖的語義.ReentrantLock支援Lock介面定義的所有獲取鎖的模式.
一句話synchronized能做的,ReentrantLock都能做,但是ReentrantLock為處理不可用的鎖提供了更多靈活性(好吧,ReentrantLock寫起來比較麻煩)
為什麼要使用顯示鎖
內部鎖在大部分情況下都能很好地工作,但是有一些功能上的侷限--不能中斷那些正在等待獲取鎖的執行緒,並且在請求鎖失敗的情況下,必須無限等待 .
內部鎖必須在獲取他們的程式碼塊中被釋放:這很好地簡化了程式碼,與異常處理機制能夠良好的互動,但是在某些情況下,一個更靈活的加鎖機制提供了更好的活躍度和效能.
public class LockTest { Lock lock= new ReentrantLock(); public void testLock(){ lock.Lock(); try { // 需要加鎖的程式碼.. }finally { lock.unlock(); } } }
這個模式在某種程度上比使用內部鎖更加複雜:鎖必須在finally塊中釋放 .
另一方面,如果鎖守護的程式碼在try塊之外丟擲了異常,它將永遠都不會被釋放了;
如果物件能夠被置於不一致的狀態,可能需要額外的try-catch,或try-finally塊.
顯示的lock的缺點
使用lock之後必須unlock釋放鎖,這也是ReentrantLock不能完全替代synchronized的原因.
它更加危險,因為當程式的控制權離開了守護的塊時,不會自動清除鎖.
1.1 可輪詢和可定時的鎖請求
可定時的與可輪詢的鎖獲取模式,是由tryLock方法實現,與無條件的鎖獲取相比,它具有更完善的錯誤恢復機制.
使用內部鎖,發生死鎖時唯一的恢復方法是重啟程式,唯一的預防方法是在構建程式時不要出錯,所以不可能允許不一致的鎖順序.
可定時的與可輪詢的鎖提供了另一個選擇,可以規避死鎖的發生.
使用方式:
public class LockSample { //建立一個鎖的例項 Lock lock = new ReentrantLock(); public void methodA(){ lock.lock(); try { System.out.println("執行了方法A"); Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void methodB(){ lock.lock(); try { System.out.println("執行了方法B"); Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String [] args){ LockSample lockSample = new LockSample(); lockSample.methodA(); //methodB()方法必須在鎖可用的時候才會執行 lockSample.methodB(); } }
使用tryLock能解決第九篇部落格死鎖,提到過的動態的順序死鎖問題.
public class LockTest { public static void main(String [] args){ LockTest lockTest = new LockTest(); Account fromAccount = new Account(); Account toAccount = new Account(); Account account = new Account(); //開啟一個新執行緒,獲取兩個使用者的鎖,這個方法是假設,物件的鎖已經被獲得用的. new Thread(){ @Override public void run(){ //這兩個方法的內部實現就是Thread.sleep()將程式碼阻塞住. fromAccount.credit(account); toAccount.dedit(account); } }.start(); lockTest.transferMoney(fromAccount,toAccount,account); } public void transferMoney(Account fromAccount,Account toAccount,Account account){ while(true){ // lock.tryLock()返回一個布林值,告訴你當前的鎖是否可用,如果可用往下走 if(fromAccount.lock.tryLock()){ try { if (toAccount.lock.tryLock()){ try { //走到這裡,證明兩個鎖都可用,可以進行轉賬操作. fromAccount.credit(account); toAccount.dedit(account); }finally { toAccount.lock.unlock(); } } }finally { fromAccount.lock.unlock(); } } } } }
Account的內部實現:
public class Account { public Lock lock = new ReentrantLock(); public void credit(Account account) { lock.lock(); try { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } public void dedit(Account account) { lock.lock(); try { try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } }
定時鎖可以在時間預算內設定相應的超時,如果活動子啊期待的時間內沒能獲得結果,這個機制是程式能夠提前返回.
而使用內部鎖一旦開始請求,鎖就不能停止了,所以內部鎖為實現具有時限的活動帶來了風險.
.tryLock方法還有一個過載版本,可以設定等待的時間:
lock.tryLock(4, TimeUnit.SECONDS)
1.2 可中斷的鎖獲取操作
lock.lockInterruptibly上的鎖,是可以響應中斷的:
public class LockSample { //建立一個鎖的例項 Lock lock = new ReentrantLock(); public void testInterruptibly(){ try { lock.lockInterruptibly(); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void test(){ System.out.println("lock.tryLock() = " + lock.tryLock()); try { System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));; } catch (InterruptedException e) { e.printStackTrace(); } } public void methodA(){ lock.lock(); try { System.out.println("執行了方法A"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void methodB(){ lock.lock(); try { System.out.println("執行了方法B"); Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String [] args){ Long startTime = System.nanoTime(); LockSample lockSample = new LockSample(); Thread thread = Thread.currentThread(); new Thread(){ @Override public void run(){ try { //休眠兩秒,執行中斷 Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); //這裡本來是休眠5秒的,因為上面直接中斷了,可以看下面的endtime是兩秒,證明了可以被中斷 lockSample.testInterruptibly(); Long endTime = startTime - System.nanoTime(); System.out.println("endTime = " + endTime); } }
2. 對效能的考量
ReentrantLock提供的競爭上的效能要遠遠優於內部鎖.
對於同步原語而言,競態時的效能是可伸縮性的關鍵:若果有越多的資源花費在鎖的管理和排程上,那程式執行的時間就越少.
在Java5.0中,ReentrantLock相比於synchronized能給吞吐量帶來相當不錯的提升,但是在Java6中,這兩者非常接近 .
也就是說之前選擇顯示鎖,還有效能方面的考量,但是現在顯示鎖和synchronized已經差不多了.
3. 公平性
ReentrantLock建構函式提供了兩種公平性的選擇:
- 建立非公平鎖(預設)
- 公平鎖
公平鎖:如果鎖已經被其他執行緒佔有,新的請求執行緒會加入到等待佇列,或者已經有一些執行緒在等待鎖了;
非公平鎖: 非公平鎖允許闖入,當請求這樣的鎖時,如果鎖的狀態變為可用,執行緒的請求可以在等待執行緒的佇列中向前跳躍,獲得該鎖.(Semaphore同樣提供了公平和非公平的獲取順序).在非公平的鎖中,執行緒只有當鎖正在被佔用時才會等待.
為什麼要使用不公平鎖
當發生加鎖的時候,公平會因為掛起和重新開始執行緒的代價帶來巨大的效能開銷.
在多數情況下,非公平鎖的優勢超過了公平的排隊.
在競爭激烈的情況下,闖入鎖比公平鎖效能好的原因之一是:掛起的執行緒重新開始,與它真正開始執行,兩者之間會產生嚴重的延遲.
比較公平鎖和非公平鎖,使用的例子:
假設執行緒A持有一個鎖,執行緒B請求該鎖.因為此時鎖正在使用中,執行緒B被掛起,當A釋放鎖後,B重新開始.與此同時,如果C請求鎖,那麼C得到了很好的機會獲得這個鎖,使用它,並且甚至可能在B被喚醒前就已經釋放該鎖了.
在這樣的情況下,各方面都獲得了成功:B並沒有比其他任何執行緒晚得到鎖,C更早的得到了鎖,吞吐量得到了改進.
如果持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,那麼使用公平鎖是比較好的.
4. 在synchronized和ReentrantLock之間進行選擇
在內部鎖不能夠滿足使用時,ReentrantLock才被作為更高階的工具,當你需要以下高階特性時,才應該使用: 可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,或者非塊結構的鎖,否則,請使用synchronized.
5. 讀-寫鎖
讀-寫鎖:一個資源能夠被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行.
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
ReadWriteLock暴露了兩個Lock物件,一個用來讀,另一個用來寫.讀取ReadWriteLock鎖守護的資料,你必須首先獲得讀取的鎖,當需要修改ReadWriteLock守護的資料時,你必須首先獲得寫入的鎖.
讀-寫鎖實現的加鎖策略允許多個同時存在的讀者,但是隻允許一個寫者 .
讀-寫鎖的設計是用來進行效能改進的,使得特定情況下能夠有更好的併發性 .
多處理器系統中,頻繁的訪問主要為讀取資料結構的時候,讀-寫鎖能夠改進效能;
在其他情況下執行的情況比獨佔的鎖要稍差 一些,這歸因於它更大的複雜性.
ReentrantReadWriteLock也能被構造為非公平(預設)或公平的.
公平: 在公平的鎖中,選擇權交給等待時間最長的執行緒;如果鎖由讀者獲得,而一個執行緒請求寫入鎖,那麼不在允許讀者獲得讀取鎖,直到寫者被受理,並且已經釋放了寫入鎖.
非公平: 執行緒允許訪問的順序是不定的.由寫者降級為讀者是允許的;從讀者升級為寫者是不允許的(嘗試這樣的行為會導致死鎖).
使用讀寫鎖的情況
當鎖被持有的時間相對較長,並且大部分操作都不會改變鎖守護的資源,那麼讀-寫鎖能夠改進併發性.
使用讀-寫鎖包裝map:
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()使用w.lock public V get(Object key){ r.lock(); try { return map.get(key); }finally { r.unlock(); } } //其他的只讀map使用r.lock }
總結
顯示的Lock與內部鎖相比提供了一些擴充套件的特性,包括處理不可用的鎖時更好的靈活性,以及對佇列行為更好的控制,但是ReentrantLock不能完全替代synchronized;只有當你需要synchronized沒能提供的特性時才應該使用.
讀-寫鎖允許多個讀者併發訪問被守護的物件,當訪問多為讀取資料結構的時候,它具有改進可伸縮性的能力.