天天看點

【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock序幕為何引入讀寫鎖?讀寫鎖 ReadWriteLock快速實作一個緩存ReentrantReadWriteLock的特色功能讀寫鎖的更新與降級總結

ReentrantLock完美實作了互斥,完美解決了并發問題。但是卻意外發現它對于讀多寫少的場景效率實在不行。此時ReentrantReadWriteLock來救場了!一種适用于讀多寫少場景的鎖,可以大幅度提升并發效率,你必須會哦!

序幕

【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock序幕為何引入讀寫鎖?讀寫鎖 ReadWriteLock快速實作一個緩存ReentrantReadWriteLock的特色功能讀寫鎖的更新與降級總結

為何引入讀寫鎖?

ReentrantReadWriteLock,顧名思義,是可重用的讀寫鎖。

在讀多寫少的場合,讀寫鎖對系統性能是很有好處的。因為如果系統在讀寫資料時均隻使用獨占鎖,那麼讀操作和寫操作間、讀操作和讀操作間、寫操作和寫操作間均不能做到真正的并發,并且需要互相等待。而讀操作本身不會影響資料的完整性和一緻性。

是以,理論上講,在大部分情況下,應該可以允許多線程同時讀,讀寫鎖正是實作了這種功能。

劃重點:讀寫鎖适用于讀多寫少的情況。可以優化性能,提升易用性。

讀寫鎖 ReadWriteLock

讀寫鎖,并不是 Java 語言特有的,而是一個廣為使用的通用技術,所有的讀寫鎖都遵守以下三條基本原則:

  • 允許多個線程同時讀共享變量;
  • 隻允許一個線程寫共享變量;
  • 如果一個寫線程正在執行寫操作,此時禁止讀線程讀共享變量。

讀寫鎖與互斥鎖的一個重要差別就是讀寫鎖允許多個線程同時讀共享變量,而互斥鎖是不允許的,這是讀寫鎖在讀多寫少場景下性能優于互斥鎖的關鍵。但讀寫鎖的寫操作是互斥的、獨占的,當一個線程在寫共享變量的時候,是不允許其他線程執行寫操作和讀操作。隻要沒有寫操作,讀取鎖可以由多個讀線程同時保持。讀寫鎖通路限制如下表所示:

讀寫鎖
非阻塞 阻塞

讀寫鎖維護了一對相關的鎖,一個用于隻讀操作,一個用于寫入操作。

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    //讀鎖
    private final Lock r = rwl.readLock();
    //寫鎖
    private final Lock w = rwl.writeLock();           

為了對比讀寫鎖和獨占鎖的差別,我們可以寫一個測試代碼,分别傳入ReentrantLock 和 ReadLock,對比一下總耗時。

private static final ReentrantLock lock = new ReentrantLock();
    private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private static final Lock r = rwl.readLock();

    public static String read(Lock lock, String key) throws InterruptedException {
        r.lock();
        try {
            // 模拟讀耗時多的場景 更能看出差別
            Thread.sleep(1000 * 10);
            return m.get(key);
        } finally {
            r.unlock();
        }
    }
           

快速實作一個緩存

回想一下工作中經常用到的緩存,例如緩存中繼資料,不就是一種典型的讀多寫少應用場景嗎?緩存之是以能提升性能,一個重要的條件就是緩存的資料一定是讀多寫少的,例如中繼資料和基礎資料基本上不會發生變化(寫少),但是使用它們的地方卻很多(讀多)。

我們是不是可以用ReentrantReadWriteLock來手寫一個緩存呢?先畫一張圖模拟簡單的緩存流程吧:

【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock序幕為何引入讀寫鎖?讀寫鎖 ReadWriteLock快速實作一個緩存ReentrantReadWriteLock的特色功能讀寫鎖的更新與降級總結
【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock序幕為何引入讀寫鎖?讀寫鎖 ReadWriteLock快速實作一個緩存ReentrantReadWriteLock的特色功能讀寫鎖的更新與降級總結
String get(String key) throws InterruptedException {
        String v = null;
        r.lock();
        log.info("{}擷取讀鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        try {
            v = m.get(key);
        } finally {
            r.unlock();
            log.info("{}釋放讀鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        }
        if (v != null) {
            log.info("{}緩存存在,傳回結果 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            return v;
        }
        w.lock();
        log.info("{}緩存中不存在,查詢資料庫,擷取寫鎖 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
        try {
            log.info("{}二次驗證 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            v = m.get(key);
            if (v == null) {
                log.info("{}查詢資料庫完成 time={} ",Thread.currentThread().getName(),System.currentTimeMillis());
                v = "value";
                log.info("-------------驗證寫鎖占有的時候 其他線程無法執行寫操作和讀操作----------------");
                Thread.sleep(1000*5);
                m.put(key, v);
            }
        } finally {
            log.info("{}寫鎖釋放 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
            w.unlock();
        }
        return v;
    }           
【漫畫】互斥鎖ReentrantLock不好用?試試讀寫鎖ReadWriteLock序幕為何引入讀寫鎖?讀寫鎖 ReadWriteLock快速實作一個緩存ReentrantReadWriteLock的特色功能讀寫鎖的更新與降級總結

ReentrantReadWriteLock的特色功能

J.U.C Lock包之ReentrantLock互斥鎖

,我們介紹了ReentrantLock相比synchronized的幾大特色功能,例如公平鎖、非阻塞擷取鎖、逾時、中斷。那麼ReentrantReadWriteLock是否也有呢?

簡單。。看看源碼不就清楚了。以下源碼都是在ReentrantReadWriteLock.java中撩出來的~ 剩下的我就不用多說了吧!如果不清楚這些方法可以回頭看看

public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }           
public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }           
public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }           

讀寫鎖的更新與降級

還想跟你聊聊鎖的更新和降級。也許你是第一次聽到,鎖還有更新降級的功能。但其實不難了解,比如在讀寫鎖中,寫鎖變為讀鎖是完全可行的方案,不會有任何問題,這裡寫鎖變讀鎖就叫做鎖的降級。

那麼可以更新嗎?熟話說降級容易,你隻要天天不來上班就行了,更新可難哦。鎖中也是,隻是在鎖中更加苛刻,完全不允許更新,即讀鎖無法更新為寫鎖。必須先釋放讀鎖,才可以擷取寫鎖。為什麼不允許更新?試想有1000個讀線程同時執行,同時更新為寫鎖,會發生什麼?擷取寫鎖的前提是讀鎖和寫鎖均未被占用,是以可能導緻阻塞較長的時間,也可能發生死鎖。

先寫個代碼驗證一下吧,在(2)處我們實作了降級,程式是完全ok的,在(1)處如果你注釋掉 r.unlock(),試圖更新為讀鎖,你會發現程式會跑不下去的,據此可以驗證我們所說的:讀寫鎖可以降級、無法更新。

void processCachedData() {
        // 擷取讀鎖
        r.lock();
        if (!cacheValid) {
            // 釋放讀鎖 因為不允許讀鎖的更新 可以注釋掉該行代碼 整個程式會阻塞
            r.unlock(); //(1)
            // 擷取寫鎖
            w.lock();
            try {
                // 再次檢查狀态
                if (!cacheValid) {
                    data = "胖滾豬學程式設計";
                    cacheValid = true;
                }

                // 釋放寫鎖前 降級為讀鎖 降級是可以的
                r.lock(); //(2)
            } finally {
                // 釋放寫鎖
                w.unlock();

            }

        }
        // 此處仍然持有讀鎖
        try {
            System.out.println(data);
        } finally {
            r.unlock();
        }

    }           

總結

讀寫鎖适用于讀多寫少的情況。可以優化性能,提升易用性。緩存就是個很好的例子。

讀寫鎖最大的特征是允許多個線程同時讀共享變量。但是隻允許一個線程寫共享變量,且如果一個寫線程正在執行寫操作,此時禁止讀線程讀共享變量。

ReentrantReadWriteLock讀寫鎖類似于 ReentrantLock,支援公平模式和非公平模式、支援非阻塞擷取鎖、逾時、中斷等特性。但是有一點需要注意,那就是隻有寫鎖支援條件變量,讀鎖是不支援條件變量的,讀鎖調用 newCondition() 會抛出 UnsupportedOperationException 異常。

是以!我們必須了解各種鎖的用途,才能在生産上選擇最合适高效的方式。

原創聲明:本文來源于微信公衆号【胖滾豬學程式設計】,持續更新JAVA大資料幹貨,用漫畫形式讓程式設計so easy and interesting。轉載請注明出處。