面試必備:ReentrantReadWriteLock原理解析[精品長文]

願我所遇之人,所歷之事,哪怕由於我有一點點變好,我就心滿意足了。 java

Java JDK 11 ReentrantReadWriteLock 原理分析git

一、前言

但願在閱讀本文以前,建議先看一下如下三篇文章:github

一、面試必備:Java AQS 實現原理(圖文)分析面試

二、面試必備:Java AQS Condition的實現分析編程

三、面試必備:Java volatile的內存語義與AQS鎖內存可見性緩存

讀完了以上三篇文章,先看一下ReentrantReadWriteLock的代碼路徑:安全

package java.util.concurrent.locks;
複製代碼

來先猜一下ReentrantReadWriteLock會如何實現?bash

都在java.util.concurrent包下,那麼能夠明確一點,那就是關於鎖的實現,應該用的就是AQS,那麼,讀鎖、寫鎖會不會對應的就是AQS中的共享模式與獨佔模式?微信

哈哈 我也不清楚 來一塊兒看看吧。併發

二、讀寫鎖使用場景

讀是多於寫(好比cache)

通常狀況下,讀寫鎖的性能都會比排它鎖好,由於大多數場景讀是多於寫的。在讀多於寫的狀況下,讀寫鎖可以提供比排它鎖更好的併發性和吞吐量。

三、讀寫鎖接口:ReadWriteLock

代碼地址:ReadWriteLock

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}
複製代碼

四、讀寫鎖的接口與示例

ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()方法和writeLock()方法,而其實現:ReentrantReadWriteLock,除了接口方法以外,還提供了一些便於外界監控其內部工做狀態的方法,這些方法以及描述如表所示:

接下來,經過一個緩存示例說明讀寫鎖的使用方式,示例代碼以下:

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 獲取一個key對應的value
    public static final Object get(String key) {
        r.lock();

        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 設置key對應的value,並返回舊的value
    public static final Object put(String key, Object value) {
        w.lock();

        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空全部的內容
    public static final void clear() {
        w.lock();

        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}
複製代碼

上述示例中,Cache組合一個非線程安全的HashMap做爲緩存的實現,同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是線程安全的。在讀操做get(String key)方法中,須要獲取讀鎖,這使得併發訪問該方法時不會被阻塞。寫操做put(String key,Object value)方法和clear()方法,在更新HashMap時必須提早獲取寫鎖,當獲取寫鎖後,其餘線程對於讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放以後,其餘讀寫操做才能繼續。Cache使用讀寫鎖提高讀操做的併發性,也保證每次寫操做對全部的讀寫操做的可見性,同時簡化了編程方式。

五、ReentrantReadWriteLock脈絡梳理

代碼地址:ReentrantReadWriteLock

先看一下繼承結構:

再看一下代碼結構:

圖中能夠看出ReentrantReadWriteLock的實現仍是比較複雜的,因此接下來主要分析ReentrantReadWriteLock實現關鍵點,包括:

  • 讀寫狀態的設計
  • 寫鎖的獲取與釋放
  • 讀鎖的獲取與釋放
  • 鎖降級

5.1 讀寫狀態的設計

讀寫鎖一樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器須要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫鎖實現的關鍵。

若是在一個整型變量上維護多種狀態,就必定須要「按位切割使用」這個變量,讀寫鎖將變量切分紅了兩個部分,高16位表示讀,低16位表示寫,劃分方式以下圖所示:

當前同步狀態表示一個線程已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速肯定讀和寫各自的狀態呢?

答案是經過位運算。假設當前同步狀態值爲S,寫狀態等於S&0x0000FFFF(將高16位所有抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增長1時,等於S+1,當讀狀態增長1時,等於S+(1<<16),也就是S+0x00010000。

一、0x0000FFFF=00000000000000001111111111111111(16個0 16個1)

二、>>>: 無符號右移,忽略符號位,空位都以0補齊

三、0x00010000=10000000000000000(1個1 16個0)

根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。

5.2 寫鎖的獲取與釋放

寫鎖是一個支持重進入的排它鎖。若是當前線程已經獲取了寫鎖,則增長寫狀態。若是當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態,獲取寫鎖的代碼如代碼以下:

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
               // 存在讀鎖或者當前獲取線程不是已經獲取寫鎖的線程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
複製代碼

該方法除了重入條件(當前線程爲獲取了寫鎖的線程)以外,增長了一個讀鎖是否存在的判斷。若是存在讀鎖,則寫鎖不能被獲取,緣由在於:讀寫鎖要確保寫鎖的操做對讀鎖可見,若是容許讀鎖在已被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程就沒法感知到當前寫線程的操做。所以,只有等待其餘讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其餘讀寫線程的後續訪問均被阻塞。

寫鎖的釋放與ReentrantLock的釋放過程基本相似,每次釋放均減小寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程可以繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。

5.3 讀鎖的獲取與釋放

讀鎖是一個支持重進入的共享鎖,它可以被多個線程同時獲取,在沒有其餘寫線程訪問(或者寫狀態爲0)時,讀鎖總會被成功地獲取,而所作的也只是(線程安全的)增長讀狀態。若是當前線程已經獲取了讀鎖,則增長讀狀態。若是當前線程在獲取讀鎖時,寫鎖已被其餘線程獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要緣由是新增了一些功能,例如getReadHoldCount()方法,做用是返回當前線程獲取讀鎖的次數。讀狀態是全部線程獲取讀鎖次數的總和,而每一個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。所以,這裏將獲取讀鎖的代碼作了刪減,保留必要的部分,如代碼以下:

protected final int tryAcquireShared(int unused) {
          for (;;) {
                  int c = getState();
                  int nextc = c + (1 << 16);
                  if (nextc < c)
                    throw new Error("Maximum lock count exceeded");
                  if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
                    return -1;
                  if (compareAndSetState(c, nextc))
                    return 1;
  }
}
複製代碼

在tryAcquireShared(int unused)方法中,若是其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。 若是當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增長讀狀態,成功獲取讀鎖。

讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減小讀狀態,減小的值是(1<<16)。

5.4 鎖降級

鎖降級指的是寫鎖降級成爲讀鎖。若是當前線程擁有寫鎖,而後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

接下來看一個鎖降級的示例。由於數據不常變化,因此多個線程能夠併發地進行數據處理,當數據變動後,若是當前線程感知到數據變化,則進行數據的準備工做,同時其餘處理線程被阻塞,直到當前線程完成數據的準備工做,如代碼以下所示:

public void processData() {
        readLock.lock();
        if (!update) {
            // 必須先釋放讀鎖
            readLock.unlock();
            // 鎖降級從寫鎖獲取到開始
            writeLock.lock();
            try {
                if (!update) {
                    // 準備數據的流程(略)
                    update = true;
                }
                readLock.lock();
            } finally {
                writeLock.unlock();
            }
            // 鎖降級完成,寫鎖降級爲讀鎖
        }
        try {
            // 使用數據的流程(略)
        } finally {
            readLock.unlock();
        }
    }
複製代碼

上述示例中,當數據發生變動後,update變量(布爾類型且volatile修飾)被設置爲false,此時全部訪問processData()方法的線程都可以感知到變化,但只有一個線程可以獲取到寫鎖,其餘線程會被阻塞在讀鎖和寫鎖的lock()方法上。當前線程獲取寫鎖完成數據準備以後,再獲取讀鎖,隨後釋放寫鎖,完成鎖降級。

鎖降級中讀鎖的獲取是否必要呢?答案是必要的。主要是爲了保證數據的可見性,若是當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另外一個線程(記做線程T)獲取了寫鎖並修改了數據,那麼當前線程沒法感知線程T的數據更新。若是當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據並釋放讀鎖以後,線程T才能獲取寫鎖進行數據更新。

RentrantReadWriteLock不支持鎖升級(把持讀鎖、獲取寫鎖,最後釋放讀鎖的過程)。目的也是保證數據可見性,若是讀鎖已被多個線程獲取,其中任意線程成功獲取了寫鎖並更新了數據,則其更新對其餘獲取到讀鎖的線程是不可見的。

六、小結

RentrantReadWriteLock的具體流程梳理完了,回過頭來想一下前言的問題,好像並無獲得答案,那麼來到ReentrantReadWriteLock代碼中,此處主要看一下讀鎖的獲取、釋放是否對應AQS中的共享模式。

6.1 讀鎖的獲取、釋放

public void lock() {
            //看到這裏是否是就明白了,咱們的猜測是正確的
            sync.acquireShared(1);
        }
       public void unlock() {
            //看到這裏是否是就明白了,咱們的猜測是正確的
            sync.releaseShared(1);
        }
複製代碼

先來看一下ReadLock的具體實現,在ReentrantReadWriteLock初始化的時候,會在構造函數中初始化ReadLock、WriteLock,具體代碼以下:

public ReentrantReadWriteLock() {
        this(false);
    }
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
複製代碼

從ReentrantReadWriteLock構造函數的代碼中,能夠看到ReadLock初始化的參數是ReentrantReadWriteLock,那麼ReadLock須要ReentrantReadWriteLock來作什麼呢?

來看一下ReadLock:

private final Sync sync;
      protected ReadLock(ReentrantReadWriteLock lock) {
          sync = lock.sync;
      }
複製代碼

從ReadLock的構造函數中,能夠看出,ReadLock須要獲取到Sync,那麼Sync是誰,又是用來作什麼的?

其實,若是看過JUC下面代碼的話,看到Sync,就明白它應該就是AQS的實現類,經過它來實現相關鎖的操做。

來看一下代碼驗證一下:

/**
     * Synchronization implementation for ReentrantReadWriteLock.
     * Subclassed into fair and nonfair versions.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
    //具體代碼略
    }
複製代碼

看到這裏能夠大致得出這麼一個結果:ReadLock獲取鎖的時候,是經過ReentrantReadWriteLock 內部Sync類來獲取的共享鎖,也就是讀鎖的獲取是對應AQS中的共享模式。

點進 sync.acquireShared(1)方法,能夠看到是調用Sync的父類AQS中方法:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
複製代碼

看到這裏,也就明白爲啥AQS子類須要重寫:

  • tryAcquire
  • tryRelease
  • tryReleaseShared
  • isHeldExclusively

等方法了。

七、參考資料

本文第四、5小節整理自:《Java併發編程的藝術》

我的微信公衆號:

我的CSDN博客:

blog.csdn.net/jiankunking

我的github:

github.com/jiankunking

我的博客:

www.jiankunking.com

相關文章
相關標籤/搜索