ReentrantLock完美實現了互斥,完美解決了併發問題。但是卻意外發現它對於讀多寫少的場景效率實在不行。此時ReentrantReadWriteLock來救場了!一種適用於讀多寫少場景的鎖,可以大幅度提升併發效率,你必須會哦! 序幕 為何引入讀寫鎖? ReentrantReadWriteLoc ...
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 = "胖滾豬學編程";
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!歡迎關註!形象來源於微信表情包【胖滾家族】喜歡可以下載哦~