淺析Java高并發(fā)下的ReadWriteLock讀寫鎖

對于高頻讀/低頻寫的應(yīng)用場景,使用Lock或者使用synchronized來做同步顯然是不太合理的,那么有其他的方式來提高并發(fā)性能嗎?

在Java的并發(fā)包中有許多功能不同的類,今天我們介紹其中的一個,讀寫鎖ReadWriteLock。這種鎖在工作中應(yīng)用場景非常廣泛,普遍的使用場景是:對于讀多寫少的場景。經(jīng)常用到的例如存儲元數(shù)據(jù),緩存基礎(chǔ)數(shù)據(jù)等等,這些都是典型的讀多寫少的應(yīng)用場景。使用緩存可以極大提升應(yīng)用程序的處理能力。

讀寫鎖有下面這幾個特征:

  • 兩個或多個線程可以同時進行讀操作

  • 線程正在進行讀操作,另一個線程想要進行寫操作,另一個線程將會被阻塞直到所有讀操作結(jié)束

  • 線程正在進行寫操作,另一個線程想要進行操作(讀或?qū)懀硪粋€線程將會阻塞直到寫入方完成操作

讀寫鎖與互斥鎖一個重要的區(qū)別就是讀寫鎖允許多個線程共享臨界段,而互斥鎖是不允許的。這是在讀多寫少場景下讀寫鎖性能優(yōu)于互斥鎖的原因。

但是讀寫鎖在讀寫操作時是互斥的,當(dāng)一個線程在進行寫操作時,其他的讀寫線程都是是處于阻塞狀態(tài)的

讀寫鎖實現(xiàn)緩存

下面我們會來實踐,用ReadWriteLock將非線程安全的Map包裝為一個簡單的緩存工具類

在下面的代碼中,我們聲明了一個 ICache<K, V> 類,其中類型參數(shù) K 代表緩存里 key 的類型,V 代表緩存里 value 的類型。緩存的數(shù)據(jù)保存在 ICache類內(nèi)部的 TreeMap里面,TreeMap不是線程安全的,這里我們使用讀寫鎖 ReadWriteLock 來保證其線程安全。ReadWriteLock 是一個接口,它的實現(xiàn)類是 ReentrantReadWriteLock,通過名字你應(yīng)該就能判斷出來,它是支持可重入的。下面我們通過 rwl 創(chuàng)建了一把讀鎖和一把寫鎖。

ICache這個工具類,我們提供了幾種簡單常用的方法,其中有讀緩存方法 get(),寫緩存方法 put()。讀緩存需要用到讀鎖,讀鎖的使用和前面我們介紹的 Lock 的使用是相同的,都是 try{}finally{}這個編程范式。寫緩存則需要用到寫鎖,寫鎖的使用和讀鎖是類似的。這樣看來,讀寫鎖的使用還是非常簡單的。

class ICache<K, V> {
    private final Map<K, V> m = new TreeMap<>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

    public V get(K k) {
        r.lock();
        try {
            return m.get(k);
        } finally {
            r.unlock();
        }
    }
    public Object[] allKeys() {
        r.lock();
        try {
            return m.keySet().toArray();
        } finally {
            r.unlock();
        }
    }
    public V put(K key, V value) {
        w.lock();
        try {
            return m.put(key, value);
        } finally {
            w.unlock();
        }
    }
    public void clear() {
        w.lock();
        try {
            m.clear();
        } finally {
            w.unlock();
        }
    }
}

如果你曾經(jīng)使用過緩存的話,你應(yīng)該知道使用緩存首先要解決緩存數(shù)據(jù)的初始化問題。緩存數(shù)據(jù)的初始化,可以采用一次性加載的方式,也可以使用按需加載的方式。

image

如果源頭數(shù)據(jù)的數(shù)據(jù)量不大,就可以采用一次性加載的方式,這種方式最簡單,只需在應(yīng)用啟動的時候把源頭數(shù)據(jù)查詢出來,依次調(diào)用類似上面示例代碼中的 put() 方法就可以了。

如果源頭數(shù)據(jù)量非常大,那么就需要按需加載了,按需加載也叫懶加載,指的是只有當(dāng)應(yīng)用查詢緩存,并且數(shù)據(jù)不在緩存里的時候,才觸發(fā)加載源頭相關(guān)數(shù)據(jù)進緩存的操作。下面你可以結(jié)合文中示意圖看看如何利用 ReadWriteLock 來實現(xiàn)緩存的按需加載。

緩存按需加載

文中下面的這段代碼實現(xiàn)了按需加載的功能,這里我們假設(shè)緩存的源頭是數(shù)據(jù)庫。需要注意的是,如果緩存中沒有緩存目標對象,那么就需要從數(shù)據(jù)庫中加載,然后寫入緩存,寫緩存需要用到寫鎖,所以在代碼注釋中的5處,我們調(diào)用了 w.lock() 來獲取寫鎖。

另外,還需要注意的是,在獲取寫鎖之后,我們并沒有直接去查詢數(shù)據(jù)庫,而是在代碼注釋6、7處,重新驗證了一次緩存中是否存在,再次驗證如果還是不存在,我們才去查詢數(shù)據(jù)庫并更新本地緩存。為什么我們要再次驗證呢?

class Cache<K, V> {
    final Map<K, V> m = new HashMap<>();
    final ReadWriteLock rwl = new ReentrantReadWriteLock();
    final Lock r = rwl.readLock();
    final Lock w = rwl.writeLock();

    V cache(K key) {
        V v = null;
        // 讀緩存
        r.lock();         //1
        try {
            v = m.get(key); //2
        } finally {
            r.unlock();     //3
        }
        // 緩存中存在,返回
        if (v != null) {   //4
            return v;
        }
        // 緩存中不存在,查詢數(shù)據(jù)庫
        w.lock();         //5
        try {
            // 再次驗證
            // 其他線程可能已經(jīng)查詢過數(shù)據(jù)庫
            v = m.get(key); //6
            if (v == null) {  //7
                // 查詢數(shù)據(jù)庫
                v = null;//省略代碼無數(shù)
                m.put(key, v);
            }
        } finally {
            w.unlock();
        }
        return v;
    }
}

在5處寫緩存,需要寫鎖,在代碼6和7處,為什么要重新判斷是否存在?

原因是在高并發(fā)的場景下,有可能會有多線程競爭寫鎖。

假設(shè)緩存是空的,沒有緩存任何東西,如果此時有三個線程 T1、T2 和 T3 同時調(diào)用 get() 方法,并且參數(shù) key 也是相同的。那么它們會同時執(zhí)行到代碼注釋5處,但此時只有一個線程能夠獲得寫鎖,假設(shè)是線程 T1,線程 T1 獲取寫鎖之后查詢數(shù)據(jù)庫并更新緩存,最終釋放寫鎖。此時線程 T2 和 T3 會再有一個線程能夠獲取寫鎖,假設(shè)是 T2,如果不采用再次驗證的方式,此時 T2 會再次查詢數(shù)據(jù)庫。T2 釋放寫鎖之后,T3 也會再次查詢一次數(shù)據(jù)庫。而實際上線程 T1 已經(jīng)把緩存的值設(shè)置好了,T2、T3 完全沒有必要再次查詢數(shù)據(jù)庫。所以,再次驗證的方式,能夠避免高并發(fā)場景下重復(fù)查詢數(shù)據(jù)的問題。

讀寫鎖的升級與降級

上面按需加載的示例代碼中,在1處獲取讀鎖,在3處釋放讀鎖,那是否可以在2處的下面增加驗證緩存并更新緩存的邏輯呢?詳細的代碼如下。

// 讀緩存
r.lock(); //1
try {
  v = m.get(key); //2
  if (v == null) {
    w.lock();
    try {
      // 再次驗證并更新緩存
      // 省略詳細代碼
    } finally{
      w.unlock();
    }
  }
} finally{
  r.unlock(); //3
}

先是獲取讀鎖,然后再升級為寫鎖,這叫鎖的升級。問題:讀鎖還沒有釋放,此時獲取寫鎖,會導(dǎo)致寫鎖永久等待,最終導(dǎo)致相關(guān)線程都被阻塞,永遠也沒有機會被喚醒。

不過,雖然鎖的升級是不允許的,但是鎖的降級卻是允許的。

以下代碼來源自 ReentrantReadWriteLock 的官方示例,略做了改動。你會發(fā)現(xiàn)在代碼注釋1處,獲取讀鎖的時候線程還是持有寫鎖的,這種鎖的降級是支持的。

class CachedData {
    private Object data;
    private volatile boolean cacheValid;  // 緩存無效   true:有效  false 無效
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {// 在獲取寫鎖之前必須釋放讀鎖
            rwl.readLock().unlock();  //釋放讀鎖
            rwl.writeLock().lock();      //獲取寫鎖
            try {
                // 重新檢查狀態(tài),因為另一個線程可能已經(jīng)獲得寫鎖并在我們之前更改了狀態(tài)。
                if (!cacheValid) {
                    data = ...
                    cacheValid = true;
                }
                // 降級通過獲取讀鎖之前釋放寫鎖
                rwl.readLock().lock(); // 1
            } finally {
                rwl.writeLock().unlock(); // 釋放寫鎖,仍然保持讀
            }
        }
        try {
            use(data); // 此處仍然持有讀鎖
        } finally {
            rwl.readLock().unlock();
        }
    }
    private void use(Object data) {
        System.out.println(data.toString());
    }
}

總結(jié)

讀寫鎖類似于 ReentrantLock,也支持公平模式和非公平模式。讀鎖和寫鎖都實現(xiàn)了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。

但是有一點需要注意,那就是只有寫鎖支持條件變量,讀鎖是不支持條件變量的,讀鎖調(diào)用 newCondition() 會拋出 UnsupportedOperationException 異常。另外,官方文檔中還提到了,讀寫鎖支持最多65535個遞歸寫鎖和65535個讀鎖。如果超過這個限制會導(dǎo)致鎖定方法拋出錯誤。

如果文章的內(nèi)容對你有幫助,歡迎關(guān)注公眾號:優(yōu)享JAVA(ID:YouXiangJAVA),那里有更多的技術(shù)干貨,并精心準備了一份程序員書單。期待你的到來!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容