1. 程式人生 > >【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock

【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock

ReentrantLock完美實現了互斥,完美解決了併發問題。但是卻意外發現它對於讀多寫少的場景效率實在不行。此時ReentrantReadWriteLock來救場了!一種適用於讀多寫少場景的鎖,可以大幅度提升併發效率,你必須會哦! # 序幕 ![_1](https://yqfile.alicdn.com/f6c266d1ab1c8798aea3a1dba8d254f579332ab4.jpeg) # 為何引入讀寫鎖? ReentrantReadWriteLock,顧名思義,是可重用的讀寫鎖。 在讀多寫少的場合,讀寫鎖對系統性能是很有好處的。因為如果系統在讀寫資料時均只使用獨佔鎖,那麼讀操作和寫操作間、讀操作和讀操作間、寫操作和寫操作間均不能做到真正的併發,並且需要相互等待。而讀操作本身不會影響資料的完整性和一致性。 因此,理論上講,在大部分情況下,應該可以允許多執行緒同時讀,讀寫鎖正是實現了這種功能。 **劃重點:讀寫鎖適用於讀多寫少的情況。可以優化效能,提升易用性。** # 讀寫鎖 ReadWriteLock 讀寫鎖,並不是 Java 語言特有的,而是一個廣為使用的通用技術,所有的讀寫鎖都遵守以下三條基本原則: - 允許多個執行緒同時讀共享變數; - 只允許一個執行緒寫共享變數; - 如果一個寫執行緒正在執行寫操作,此時禁止讀執行緒讀共享變數。 讀寫鎖與互斥鎖的一個重要區別就是讀寫鎖允許多個執行緒同時讀共享變數,而互斥鎖是不允許的,這是讀寫鎖在讀多寫少場景下效能優於互斥鎖的關鍵。但讀寫鎖的寫操作是互斥的、獨佔的,當一個執行緒在寫共享變數的時候,是不允許其他執行緒執行寫操作和讀操作。只要沒有寫操作,讀取鎖可以由多個讀執行緒同時保持。讀寫鎖訪問約束如下表所示: 讀寫鎖 | 讀 | 寫 -|-|- 讀 | 非阻塞 | 阻塞 | 寫 | 阻塞 | 阻塞 | 讀寫鎖維護了一對相關的鎖,一個用於只讀操作,一個用於寫入操作。 ``` private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //讀鎖 private final Lock r = rwl.readLock(); //寫鎖 private final Lock w = rwl.writeLock(); ``` 為了對比讀寫鎖和獨佔鎖的區別,我們可以寫一個測試程式碼,分別傳入ReentrantLock 和 ReadLock,對比一下總耗時。 ``` private static final ReentrantLock lock = new ReentrantLock(); private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private static final Lock r = rwl.readLock(); public static String read(Lock lock, String key) throws InterruptedException { r.lock(); try { // 模擬讀耗時多的場景 更能看出區別 Thread.sleep(1000 * 10); return m.get(key); } finally { r.unlock(); } } ``` # 快速實現一個快取 回想一下工作中經常用到的快取,例如快取元資料,不就是一種典型的讀多寫少應用場景嗎?快取之所以能提升效能,一個重要的條件就是快取的資料一定是讀多寫少的,例如元資料和基礎資料基本上不會發生變化(寫少),但是使用它們的地方卻很多(讀多)。 我們是不是可以用ReentrantReadWriteLock來手寫一個快取呢?先畫一張圖模擬簡單的快取流程吧: ![未命名檔案.png](https://img2020.cnblogs.com/other/2027276/202005/2027276-20200515225522946-162822507.png) ![_2](https://yqfile.alicdn.com/35e273e0df05928be19b122f375cd9395e25c142.jpeg) ![_3](https://yqfile.alicdn.com/b96028b83e1d1f3792ae240d4c897aad5dd934ec.jpeg) ``` String get(String key) throws InterruptedException { String v = null; r.lock(); log.info("{}獲取讀鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); try { v = m.get(key); } finally { r.unlock(); log.info("{}釋放讀鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); } if (v != null) { log.info("{}快取存在,返回結果 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); return v; } w.lock(); log.info("{}快取中不存在,查詢資料庫,獲取寫鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); try { log.info("{}二次驗證 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); v = m.get(key); if (v == null) { log.info("{}查詢資料庫完成 time={} ",Thread.currentThread().getName(),System.currentTimeMillis()); v = "value"; log.info("-------------驗證寫鎖佔有的時候 其他執行緒無法執行寫操作和讀操作----------------"); Thread.sleep(1000*5); m.put(key, v); } } finally { log.info("{}寫鎖釋放 time={}",Thread.currentThread().getName(),System.currentTimeMillis()); w.unlock(); } return v; } ``` ![_5](https://yqfile.alicdn.com/4a892dd009d9818009273edafc6378423339c74e.jpeg) > 原創宣告:本文來源於微信公眾號【胖滾豬學程式設計】,持續更新JAVA\大資料乾貨,用漫畫形式讓程式設計so easy and interesting。轉載請註明出處。 # ReentrantReadWriteLock的特色功能 在[ J.U.C Lock包之ReentrantLock互斥鎖](https://mp.weixin.qq.com/s?__biz=MzA3MjY1MTcyNw==&mid=2247484334&idx=1&sn=a9a45cbea5155c025a30191d9dd8dee2&chksm=9f1a450ea86dcc18d418552530cc9869be0b6f8862944d7ecb8019b8ae3a3eff121a594de09e#rd),我們介紹了ReentrantLock相比synchronized的幾大特色功能,例如公平鎖、非阻塞獲取鎖、超時、中斷。那麼ReentrantReadWriteLock是否也有呢? 簡單。。看看原始碼不就清楚了。以下原始碼都是在ReentrantReadWriteLock.java中撩出來的~ 剩下的我就不用多說了吧!如果不清楚這些方法可以回頭看看[ J.U.C Lock包之ReentrantLock互斥鎖](https://mp.weixin.qq.com/s?__biz=MzA3MjY1MTcyNw==&mid=2247484334&idx=1&sn=a9a45cbea5155c025a30191d9dd8dee2&chksm=9f1a450ea86dcc18d418552530cc9869be0b6f8862944d7ecb8019b8ae3a3eff121a594de09e#rd) ``` public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } ``` ``` public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } ``` ``` public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } ``` # 讀寫鎖的升級與降級 還想跟你聊聊鎖的升級和降級。也許你是第一次聽到,鎖還有升級降級的功能。但其實不難理解,比如在讀寫鎖中,寫鎖變為讀鎖是完全可行的方案,不會有任何問題,這裡**寫鎖變讀鎖就叫做鎖的降級**。 那麼可以升級嗎?熟話說降級容易,你只要天天不來上班就行了,升級可難哦。鎖中也是,只是在鎖中更加苛刻,**完全不允許升級,即讀鎖無法升級為寫鎖**。**必須先釋放讀鎖,才可以獲取寫鎖**。為什麼不允許升級?試想有1000個讀執行緒同時執行,同時升級為寫鎖,會發生什麼?獲取寫鎖的前提是讀鎖和寫鎖均未被佔用,因此可能導致阻塞較長的時間,也可能發生死鎖。 先寫個程式碼驗證一下吧,在(2)處我們實現了降級,程式是完全ok的,在(1)處如果你註釋掉 r.unlock(),試圖升級為讀鎖,你會發現程式會跑不下去的,據此可以驗證我們所說的:讀寫鎖可以降級、無法升級。 ``` void processCachedData() { // 獲取讀鎖 r.lock(); if (!cacheValid) { // 釋放讀鎖 因為不允許讀鎖的升級 可以註釋掉該行程式碼 整個程式會阻塞 r.unlock(); //(1) // 獲取寫鎖 w.lock(); try { // 再次檢查狀態 if (!cacheValid) { data = "胖滾豬學程式設計"; cacheValid = true; } // 釋放寫鎖前 降級為讀鎖 降級是可以的 r.lock(); //(2) } finally { // 釋放寫鎖 w.unlock(); } } // 此處仍然持有讀鎖 try { System.out.println(data); } finally { r.unlock(); } } ``` # 總結 讀寫鎖適用於讀多寫少的情況。可以優化效能,提升易用性。快取就是個很好的例子。 讀寫鎖最大的特徵是允許多個執行緒同時讀共享變數。但是隻允許一個執行緒寫共享變數,且如果一個寫執行緒正在執行寫操作,此時禁止讀執行緒讀共享變數。 ReentrantReadWriteLock讀寫鎖類似於 ReentrantLock,支援公平模式和非公平模式、支援非阻塞獲取鎖、超時、中斷等特性。但是有一點需要注意,那就是隻有寫鎖支援條件變數,讀鎖是不支援條件變數的,讀鎖呼叫 newCondition() 會丟擲 UnsupportedOperationException 異常。 所以!我們必須瞭解各種鎖的用途,才能在生產上選擇最合適高效的方式。 > 原創宣告:本文來源於微信公眾號【胖滾豬學程式設計】,持續更新JAVA\大資料乾貨,用漫畫形式讓程式設計so easy and interesting。轉載請註明出處。 > 本文轉載自公眾號【胖滾豬學程式設計】 用漫畫讓程式設計so easy and interesting!歡迎關注!形象來源於微信表情包【胖滾家族】喜歡可以