ReentrantLock完美實作了互斥,完美解決了并發問題,但是卻意外發現它對于讀多寫少的場景效率實在不行,此時ReentrantReadWriteLock來救場了!一種適用于讀多寫少場景的鎖,可以大幅度提升并發效率,你必須會哦!
序幕

為何引入讀寫鎖?
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來手寫一個快取呢?先畫一張圖模擬簡單的快取流程吧:



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;
}

原創宣告:本文來源于微信公眾號【胖滾豬學編程】,持續更新JAVA\大資料干貨,用漫畫形式讓編程so easy and interesting,轉載請注明出處,
ReentrantReadWriteLock的特色功能
在 J.U.C Lock包之ReentrantLock互斥鎖,我們介紹了ReentrantLock相比synchronized的幾大特色功能,例如公平鎖、非阻塞獲取鎖、超時、中斷,那么ReentrantReadWriteLock是否也有呢?
簡單,,看看原始碼不就清楚了,以下原始碼都是在ReentrantReadWriteLock.java中撩出來的~ 剩下的我就不用多說了吧!如果不清楚這些方法可以回頭看看 J.U.C Lock包之ReentrantLock互斥鎖
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 = "https://www.cnblogs.com/liuyanling/p/胖滾豬學編程";
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!歡迎關注!形象來源于微信表情包【胖滾家族】喜歡可以下載哦~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/193091.html
標籤:Java
