讀寫鎖 ReentrantReadWriteLock

黄橙發表於2024-03-14

ReentrantReadWriteLock 介紹

ReentrantReadWriteLock 是 Java 中的一個讀寫鎖實現,它包含了兩種鎖:讀鎖和寫鎖。讀鎖是共享鎖,多個執行緒可以同時獲取讀鎖進行讀操作,但是寫鎖是排他鎖,只有一個執行緒可以獲取寫鎖進行寫操作。ReentrantReadWriteLock 提供了比單一鎖更高效的併發效能,特別適用於讀多寫少的場景。

JDK 提供了 ReentrantReadWriteLock 讀寫鎖,使用它可以加快效率,在某些不需要操作例項變數的方法中,完全可以使用讀寫鎖 ReemtrantReadWriteLock 來提升該方法的執行速度。

定義:讀寫鎖表示有兩個鎖,一個是讀操作相關的鎖,也稱為共享鎖;另一個是寫操作相關的鎖,也叫排他鎖。

定義解讀:也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥、寫鎖與寫鎖互斥。在沒有執行緒 Thread 進行寫入操作時,進行讀取操作的多個 Thread 都可以獲取讀鎖,而進行寫入操作的 Thread 只有在獲取寫鎖後才能進行寫入操作。即多個 Thread 可以同時進行讀取操作,但是同一時刻只允許一個 Thread 進行寫入操作。

ReentrantReadWriteLock 的主要特點包括:

  1. 讀寫分離:讀操作可以併發進行,寫操作是排他的。
  2. 可重入性:讀鎖和寫鎖都支援重入。
  3. 公平性:可以選擇是否公平地獲取讀寫鎖。
  4. 鎖降級:可以將寫鎖降級為讀鎖,但不能將讀鎖升級為寫鎖。
  5. 條件變數:支援 Condition 條件變數。

ReentrantReadWriteLock 的主要方法包括:

  1. readLock():獲取讀鎖。
  2. writeLock():獲取寫鎖。
  3. readLock().lock():獲取讀鎖。
  4. writeLock().lock():獲取寫鎖。
  5. readLock().unlock():釋放讀鎖。
  6. writeLock().unlock():釋放寫鎖。

ReentrantReadWriteLock 的特點

性質 1 :可重入性。

ReentrantReadWriteLock 與 ReentrantLock 以及 synchronized 一樣,都是可重入性鎖,這裡不會再多加贅述所得可重入性質,之前已經做過詳細的講解。

性質 2 :讀寫分離。

我們知道,對於一個資料,不管是幾個執行緒同時讀都不會出現任何問題,但是寫就不一樣了,幾個執行緒對同一個資料進行更改就可能會出現資料不一致的問題,因此想出了一個方法就是對資料加鎖,這時候出現了一個問題:

執行緒寫資料的時候加鎖是為了確保資料的準確性,但是執行緒讀資料的時候再加鎖就會大大降低效率,這時候怎麼辦呢?那就對寫資料和讀資料分開,加上兩把不同的鎖,不僅保證了正確性,還能提高效率。

性質 3 :可以鎖降級,寫鎖降級為讀鎖。

執行緒獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

性質 4 :不可鎖升級。

執行緒獲取讀鎖是不能直接升級為寫入鎖的。需要釋放所有讀取鎖,才可獲取寫鎖。

public class LockExample3 {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}

ReentrantReadWriteLock 讀鎖共享

我們之前說過,ReentrantReadWriteLock 之所以優秀,是因為讀鎖與寫鎖是分離的,當所有的執行緒都為讀操作時,不會造成執行緒之間的互相阻塞,提升了效率,那麼接下來,我們透過程式碼例項進行學習。

場景設計:

  • 建立三個執行緒,執行緒名稱分別為 t1,t2,t3,執行緒實現方式自行選擇;
  • 三個執行緒同時執行獲取讀鎖,讀鎖成功後列印執行緒名和獲取結果,並沉睡 2000 毫秒,便於觀察其他執行緒是否可共享讀鎖;
  • finally 模組中釋放鎖並列印執行緒名和釋放結果;
  • 執行程式,觀察結果。

結果預期:三條執行緒能同時獲取鎖,因為讀鎖共享。

public class DemoTest {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖
    private int i;
    public String readI() {
        try {
            lock.readLock().lock();// 佔用讀鎖
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用讀鎖,i->" + i);
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i);
            lock.readLock().unlock();// 釋放讀鎖
        }
        return i + "";
    }

    public static void main(String[] args) {
        final DemoTest demo1 = new DemoTest();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                demo1.readI();
            }
        };
        new Thread(runnable, "t1"). start();
        new Thread(runnable, "t2"). start();
        new Thread(runnable, "t3"). start();
    }
}

ReentrantReadWriteLock 讀寫互斥

當共享變數有寫操作時,必須要對資源進行加鎖,此時如果一個執行緒正在進行讀操作,那麼寫操作的執行緒需要等待。同理,如果一個執行緒正在寫操作,讀操作的執行緒需要等待。

場景設計:細節操作不詳細闡述,看示例程式碼即可。

  • 建立兩個執行緒,執行緒名稱分別為 t1,t2;
  • 執行緒 t1 進行讀操作,獲取到讀鎖之後,沉睡 5000 毫秒;
  • 執行緒 t2 進行寫操作;
  • 開啟 t1,1000 毫秒後開啟 t2 執行緒;
  • 執行程式,觀察結果。

結果預期:執行緒 t1 獲取了讀鎖,在沉睡的 5000 毫秒中,執行緒 t2 只能等待,不能獲取到鎖,因為讀寫互斥。

public class DemoTest {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖
    private int i;
    public String readI() {
        try {
            lock.readLock().lock();// 佔用讀鎖
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用讀鎖,i->" + i);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i);
            lock.readLock().unlock();// 釋放讀鎖
        }
        return i + "";
    }

    public void addI() {
        try {
            lock.writeLock().lock();// 佔用寫鎖
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 佔用寫鎖,i->" + i);
            i++;
        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放寫鎖,i->" + i);
            lock.writeLock().unlock();// 釋放寫鎖
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final DemoTest demo1 = new DemoTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.readI();
            }
        }, "t1"). start();
        Thread.sleep(1000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.addI();
            }
        }, "t2"). start();
    }
}

相關文章