Java中的讀寫鎖ReentrantReadWriteLock詳解,存在一個小缺陷

JavaBuild發表於2024-04-28

寫在開頭

最近是和java.util.concurrent.locks包下的同步類幹上了,素有 併發根基 之稱的concurrent包中全是精品,今天我們繼續哈,今天學習的主題要由一個大廠常問的Java面試題開始:

小夥子,來說一說Java中的讀寫鎖,你都用過哪些讀寫鎖吧?

這個問題小夥伴們遇到了該如何回答呢?心裡琢磨去吧,哈哈😄,不過build哥的回答要用從ReentrantReadWriteLock開始說起了,這個類也就是今天的主角,而它們同樣是來自於java.util.concurrent.locks之下!

image

讀寫鎖誕生的背景

在過去學習的過程中我們學過 synchronized、 ReentrantLock這種獨佔式鎖,他們的好處是保證了執行緒的安全,缺點是同一時刻只能有一個執行緒持有鎖,大大的影響了效率,而之前學過的Semaphore(訊號量)這種呢,雖然支援同一時刻被多個執行緒獲取,但它不能很好的保障執行緒安全性,我們需要的是一種效率高、安全性好的同步鎖。

考慮到真正的生產生活中,對於資料的讀取要比寫入更為頻繁,偉大的開發者們,將讀資料的時候設定為共享鎖,支援多個執行緒持有讀鎖,而在寫的時候,考慮到執行緒安全,採用獨佔鎖,同一時候僅允許一個執行緒持有寫鎖,在這種背景下讀寫鎖應運而生!

讀寫鎖:ReentrantReadWriteLock

ReentrantReadWriteLock是ReadWriteLock 介面的預設實現類,從名字可以看得出它也是一種具有可重入性的鎖,同時也支援公平與非公平的配置,底層有兩把鎖,一把是 WriteLock (寫鎖),一把是 ReadLock(讀鎖) 。讀鎖是共享鎖,寫鎖是獨佔鎖。讀鎖可以被同時讀,可以同時被多個執行緒持有,而寫鎖最多隻能同時被一個執行緒持有,也是基於AQS實現的底層鎖獲取與釋放邏輯。

image

內部構造

根據上面的構造圖如果還沒有搞清楚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哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章