【漫畫】互斥鎖ReentrantLock很差用?試試讀寫鎖ReadWriteLock

ReentrantLock完美實現了互斥,完美解決了併發問題。可是卻意外發現它對於讀多寫少的場景效率實在不行。此時ReentrantReadWriteLock來救場了!一種適用於讀多寫少場景的鎖,能夠大幅度提高併發效率,你必須會哦!java

序幕

_1

爲什麼引入讀寫鎖?

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來手寫一個緩存呢?先畫一張圖模擬簡單的緩存流程吧:
未命名文件.png
_2

_3

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;
    }

_5

原創聲明:本文來源於微信公衆號【胖滾豬學編程】,持續更新JAVA\大數據乾貨,用漫畫形式讓編程so easy and interesting。轉載請註明出處。

ReentrantReadWriteLock的特點功能

J.U.C Lock包之ReentrantLock互斥鎖,咱們介紹了ReentrantLock相比synchronized的幾大特點功能,例如公平鎖、非阻塞獲取鎖、超時、中斷。那麼ReentrantReadWriteLock是否也有呢?

簡單。。看看源碼不就清楚了。如下源碼都是在ReentrantReadWriteLock.java中撩出來的~ 剩下的我就不用多說了吧!若是不清楚這些方法能夠回頭看看 J.U.C Lock包之ReentrantLock互斥鎖

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。轉載請註明出處。

本文轉載自公衆號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!歡迎關注!形象來源於微信表情包【胖滾家族】喜歡能夠下載哦~

相關文章
相關標籤/搜索