寫在開頭
最近是和java.util.concurrent.locks包下的同步類幹上了,素有 併發根基
之稱的concurrent包中全是精品,今天我們繼續哈,今天學習的主題要由一個大廠常問的Java面試題開始:
小夥子,來說一說Java中的讀寫鎖,你都用過哪些讀寫鎖吧?
這個問題小夥伴們遇到了該如何回答呢?心裡琢磨去吧,哈哈😄,不過build哥的回答要用從ReentrantReadWriteLock開始說起了,這個類也就是今天的主角,而它們同樣是來自於java.util.concurrent.locks之下!
讀寫鎖誕生的背景
在過去學習的過程中我們學過 synchronized、 ReentrantLock這種獨佔式鎖,他們的好處是保證了執行緒的安全,缺點是同一時刻只能有一個執行緒持有鎖,大大的影響了效率,而之前學過的Semaphore(訊號量)這種呢,雖然支援同一時刻被多個執行緒獲取,但它不能很好的保障執行緒安全性,我們需要的是一種效率高、安全性好的同步鎖。
考慮到真正的生產生活中,對於資料的讀取要比寫入更為頻繁,偉大的開發者們,將讀資料的時候設定為共享鎖,支援多個執行緒持有讀鎖,而在寫的時候,考慮到執行緒安全,採用獨佔鎖,同一時候僅允許一個執行緒持有寫鎖,在這種背景下讀寫鎖應運而生!
讀寫鎖:ReentrantReadWriteLock
ReentrantReadWriteLock是ReadWriteLock 介面的預設實現類,從名字可以看得出它也是一種具有可重入性的鎖,同時也支援公平與非公平的配置,底層有兩把鎖,一把是 WriteLock (寫鎖),一把是 ReadLock(讀鎖) 。讀鎖是共享鎖,寫鎖是獨佔鎖。讀鎖可以被同時讀,可以同時被多個執行緒持有,而寫鎖最多隻能同時被一個執行緒持有,也是基於AQS實現的底層鎖獲取與釋放邏輯。
內部構造
根據上面的構造圖如果還沒有搞清楚ReentrantReadWriteLock的底層構造的話,那我們跟入原始碼中取一探究竟吧!
【原始碼分析】
// 內部結構
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/*1、用以繼承AQS,獲得AOS的特性,以及AQS的鉤子函式*/
abstract static class Sync extends AbstractQueuedSynchronizer {
// 具體實現
}
/*非公平模式,預設為這種模式*/
static final class NonfairSync extends Sync {
// 具體實現
}
/*公平模式,透過構造方法引數設定*/
static final class FairSync extends Sync {
// 具體實現
}
/*讀鎖,底層是共享鎖*/
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 具體實現
}
/*寫鎖,底層是獨佔鎖*/
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 具體實現
}
// 構造方法,初始化兩個鎖
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
// 獲取讀鎖和寫鎖的方法
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
上面為底層的主要構造內容,ReentrantReadWriteLock中共寫了5個靜態內部類,各有功效,在上面的註釋中也有提及。
使用案例
那麼這個讀寫鎖如何使用呢?我們寫一個小小的測試案例,也感受一下。
【測試案例】
public class Test {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
/**
* 寫方法
* @param value
*/
public void write(int value) {
//注意,獲取鎖的操作要在try/finally外面
lock.writeLock().lock(); // 獲取寫鎖
try {
data = value;
System.out.println("執行緒:"+Thread.currentThread().getName() + "寫" + data);
} finally {
lock.writeLock().unlock(); // 釋放寫鎖
}
}
public void read() {
lock.readLock().lock(); // 獲取讀鎖
try {
System.out.println("執行緒:" + Thread.currentThread().getName() + "讀" + data);
} finally {
lock.readLock().unlock(); // 釋放讀鎖
}
}
public static void main(String[] args) {
Test test = new Test();
// 建立讀執行緒
Thread readThread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
test.read();
}
});
Thread readThread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
test.read();
}
});
// 建立寫執行緒
Thread writeThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
test.write(i);
}
});
readThread1.start();
readThread2.start();
writeThread.start();
try {
readThread1.join();
readThread2.join();
writeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出:
執行緒:Thread-1讀0
執行緒:Thread-0讀0
執行緒:Thread-1讀0
執行緒:Thread-2寫0
執行緒:Thread-2寫1
執行緒:Thread-2寫2
執行緒:Thread-2寫3
執行緒:Thread-0讀3
執行緒:Thread-1讀3
執行緒:Thread-2寫4
執行緒:Thread-0讀4
執行緒:Thread-1讀4
執行緒:Thread-0讀4
執行緒:Thread-1讀4
執行緒:Thread-0讀4
透過輸出內容,我們進一步得證,在ReentrantReadWriteLock在使用讀鎖時,可以支援多個執行緒獲取讀資源,而在呼叫寫鎖時,其他讀執行緒和寫執行緒均阻塞等待當前執行緒寫完。
存在的問題
雖然ReentrantReadWriteLock最佳化了原有的獨佔鎖對於程式讀寫的效能,但它仍然存在一個弊端,就是 “寫飢餓”
,因為在寫的時候,是獨佔模式,其他執行緒不能讀也不能寫,這時候若有大量的讀操作的話,那這些執行緒也只能等待著,從而帶來寫飢餓。
那這個問題怎麼解決呢?我們在下一篇StampedLock(鎖王)的講解中,進行解答哈,敬請期待!
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!