首頁 > 軟體

一文了解Java讀寫鎖ReentrantReadWriteLock的使用

2022-10-18 14:00:31

概述

ReentrantReadWriteLock不知道大家熟悉嗎?其實在實際的專案中用的比較少,反正我所在的專案沒有用到過。

ReentrantReadWriteLock稱為讀寫鎖,它提供一個讀鎖,支援多個執行緒共用同一把鎖。它也提供了一把寫鎖,是獨佔鎖,和其他讀鎖或者寫鎖互斥,表明只有一個執行緒能持有鎖資源。通過兩把鎖的協同工作,能夠最大化的提高讀寫的效能,特別是讀多寫少的場景,而往往大部分的場景都是讀多寫少的。

本文主要講解ReentrantReadWriteLock的使用和應用場景。

ReentrantReadWriteLock介紹

ReentrantReadWriteLock實現了ReadWriteLock介面,可以獲取到讀鎖(共用鎖),寫鎖(獨佔鎖)。同時,通過構造方法可以建立鎖本身是公平鎖還是非公鎖。

讀寫鎖機制:

 讀鎖寫鎖
讀鎖共用互斥
寫鎖互斥互斥

執行緒進入讀鎖的前提條件:

  • 沒有其他執行緒的寫鎖
  • 沒有寫請求,或者有寫請求但呼叫執行緒和持有鎖的執行緒是同一個執行緒

進入寫鎖的前提條件:

  • 沒有其他執行緒的讀鎖
  • 沒有其他執行緒的寫鎖

鎖升級、降級機制:

我們知道ReentrantLock具備可重入的能力,即同一個執行緒多次獲取鎖,不引起阻塞,那麼ReentrantReadWriteLock關於可重入性是怎麼樣的呢?

關於這個問題需要引入兩個概念,鎖升級,鎖降級。

  • 鎖升級:從讀鎖變成寫鎖。
  • 鎖降級:從寫鎖變成讀鎖;

重入時鎖升級不支援:持有讀鎖的情況下去獲取寫鎖會導致獲取寫鎖永久等待,需要先釋放讀,再去獲得寫

重入時鎖降級支援:持有寫鎖的情況下去獲取讀鎖,造成只有當前執行緒會持有讀鎖,因為寫鎖會互斥其他的鎖

API介紹

構造方法:

  • public ReentrantReadWriteLock():預設構造方法,非公平鎖
  • public ReentrantReadWriteLock(boolean fair):true 為公平鎖

常用API:

  • public ReentrantReadWriteLock.ReadLock readLock():返回讀鎖
  • public ReentrantReadWriteLock.WriteLock writeLock():返回寫鎖
  • public void lock():加鎖
  • public void unlock():解鎖
  • public boolean tryLock():嘗試獲取鎖

程式碼正規化

加解鎖格式

r.lock();
try {
    // 臨界區
} finally {
	r.unlock();
}

鎖降級

w.lock();
try {
    r.lock();// 降級為讀鎖, 釋放寫鎖, 這樣能夠讓其它執行緒讀取快取
    try {
        // ...
    } finally{
    	w.unlock();// 要在寫鎖釋放之前獲取讀鎖
    }
} finally{
	r.unlock();
}

實戰案例

驗證讀讀共用模式

@Test
    public void readReadMode() throws InterruptedException {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock r = rw.readLock();
        ReentrantReadWriteLock.WriteLock w = rw.writeLock();

        Thread thread0 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 1 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t1");

        Thread thread1 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 2 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t2");

        thread0.start();
        thread1.start();

        thread0.join();
        thread1.join();
    }

執行結果:

兩個執行緒同時執行,都獲取到了讀鎖

驗證讀寫互斥模式

@Test
    public void readWriteMode() throws InterruptedException {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock r = rw.readLock();
        ReentrantReadWriteLock.WriteLock w = rw.writeLock();

        Thread thread0 = new Thread(() -> {
            r.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 1 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }
        },"t1");

        Thread thread1 = new Thread(() -> {
            w.lock();
            try {
                Thread.sleep(1000);
                System.out.println("Thread 2 running " + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                w.unlock();
            }
        },"t2");

        thread0.start();
        thread1.start();

        thread0.join();
        thread1.join();
    }

執行結果:

兩個執行緒間隔1秒,互斥執行

真實快取例子

什麼場景下讀多寫少? 想必最先想到的就是快取把,ReentrantReadWriteLock在快取場景中就是一個很典型的應用。

快取更新時,是先清快取還是先更新資料庫?

  • 先清快取:可能造成剛清除快取還沒有更新資料庫,高並行下,其他執行緒直接查詢了資料庫過期資料到快取中,這種情況非常嚴重,直接導致後續所有的請求快取和資料庫不一致。
  • 先更新據庫:可能造成剛更新資料庫,還沒清空快取就有執行緒從快取拿到了舊資料,這種情況概率比較小,影響範圍有限,只對這一次的查詢結果有問題。

顯而易見,通常情況下,先更新資料庫,然後清空快取。

public class GenericCachedDao {

    // 快取物件,這裡用jvm快取
    Map<String, String> cache = new HashMap<>();
    // 讀寫鎖
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 讀取操作
    public String getData(String key) {
        // 加讀鎖,防止其他執行緒修改快取
        readWriteLock.readLock().lock();
        try {
            String value = cache.get(key);
            // 如果快取命中,返回
            if(value != null) {
                return value;
            }
        } finally {
            // 釋放讀鎖
            readWriteLock.readLock().unlock();
        }

        //如果快取沒有命中,從資料庫中載入
        readWriteLock.writeLock().lock();
        try {
            // 細節,為防止重複查詢資料庫, 再次驗證
            // 因為get 方法上面部分是可能多個執行緒進來的, 可能已經向快取填充了資料
            String value = cache.get(key);
            if(value == null) {
                // 這裡可以改成從資料庫查詢
                value = "alvin";
                cache.put(key, value);
            }
            return value;
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 更新資料
    public void updateData(String key, String value) {
        // 加寫鎖
        readWriteLock.writeLock().lock();
        try {
            // 更新操作TODO

            // 清空快取
            cache.remove(key);
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

getData方法是讀取操作,先加讀鎖,從快取讀取,如果沒有命中,加寫鎖,此時其他執行緒就不能讀取了,等寫入成功後,釋放讀鎖。

updateData方法是寫操作,更新時加寫鎖,其他執行緒此時無法讀取,然後清空快取中的舊資料。

到此這篇關於一文了解Java讀寫鎖ReentrantReadWriteLock的使用的文章就介紹到這了,更多相關Java讀寫鎖ReentrantReadWriteLock內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com