1. 程式人生 > >Java讀寫鎖ReentrantReadWriteLock原理詳解

Java讀寫鎖ReentrantReadWriteLock原理詳解

轉載自:https://blog.csdn.net/fuyuwei2015/article/details/72597192

介紹

ReentrantLock屬於排他鎖,這些鎖在同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個執行緒訪問,但是在寫執行緒訪問時,所有的讀和其他寫執行緒都被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。 
下面我們來看看讀寫鎖ReentrantReadWriter特性 

  1. 公平性選擇:支援非公平(預設)和公平的鎖獲取模式,吞吐量還是非公平優於公平 
  2. 重入性
    :該鎖支援重入鎖,以讀寫執行緒為例:讀執行緒在獲取讀鎖之後,能夠再次讀取讀鎖,而寫執行緒在獲取寫鎖之後可以同時再次獲取讀鎖和寫鎖 
  3. 鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級為讀鎖

讀寫鎖介面詳解

ReentrantReadWriterLock是ReadWriterLock的介面實現類,但是ReadWriterLock介面僅有讀鎖、寫鎖兩個方法

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

ReentrantReadWriteLock自己提供了一些內部工作狀態方法,例如

  /**
     * 返回當前讀鎖被獲取的次數,該次數不等於獲取鎖的執行緒數,因為同一個執行緒可以多次獲取支援重入鎖
     * Queries the number of read locks held for this lock. This
     * method is designed for use in monitoring system state, not for
     * synchronization control.
     * @return the number of read locks held.
     */
    public int getReadLockCount() {
        return sync.getReadLockCount();
    }

    /**
     * 返回當前執行緒獲取讀鎖的次數,Java6之後使用ThreadLocal儲存當前執行緒獲取的次數
     */
    final int getReadHoldCount() {
        if (getReadLockCount() == 0)
            return 0;
        Thread current = Thread.currentThread();
        if (firstReader == current)
            return firstReaderHoldCount;
        HoldCounter rh = cachedHoldCounter;
        if (rh != null && rh.tid == current.getId())
            return rh.count;
        int count = readHolds.get().count;
        if (count == 0) readHolds.remove();
        return count;
    }

    /**
     * 判斷寫鎖是否被獲取
     * @return
     */
    final boolean isWriteLocked() {
        return exclusiveCount(getState()) != 0;
    }

    /**
     * 判斷當前寫鎖被獲取的次數
     * @return
     */
    final int getWriteHoldCount() {
        return isHeldExclusively() ? exclusiveCount(getState()) : 0;
    }

下面我們來看一個通過快取示例說明讀寫鎖的使用

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();

    public static final Object get(String key){
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    public static final Object put(String key,String 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();
        }
    }

}

上面的HashMap雖然是非執行緒安全,但是我們在get和put時候分別使用讀寫鎖,保證了執行緒安全。

讀寫鎖的實現原理分析

讀寫狀態的設計

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個執行緒重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如下圖所示 

讀寫鎖狀態

當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態呢?答案是通過位運算。假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000。根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。

寫鎖的獲取與釋放

寫鎖是一個支援重進入的排他鎖。如果當前執行緒已經獲取了寫鎖,則增加寫狀態。如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態,我們看下ReentrantReadWriteLock的tryAcquire方法

        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) {
                // (Note: if c != 0 and w == 0 then shared count != 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時表示寫鎖已被釋放,從而等待的讀寫執行緒能夠繼續訪問讀寫鎖,同時前次寫執行緒的修改對後續讀寫執行緒可見

讀鎖的獲取與釋放

讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(執行緒安全的)增加讀狀態。如果當前執行緒已經獲取了讀鎖,則增加讀狀態。如果當前執行緒在獲取讀鎖時,寫鎖已被其他執行緒獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回當前執行緒獲取讀鎖的次數。讀狀態是所有執行緒獲取讀鎖次數的總和,而每個執行緒各自獲取讀鎖的次數只能選擇儲存在ThreadLocal中,由執行緒自身維護,這使獲取讀鎖的實現變得複雜

      protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId())
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

在tryAcquireShared(int unused)方法中,如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(執行緒安全的,可能有多個讀執行緒同時釋放讀鎖)均減少讀狀態,減少的值是(1<<16)

鎖降級

鎖降級指的是寫鎖降級成為讀鎖。如果當前執行緒擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

在瀏覽ReentrantReadWriteLock的官方文件時,看到鎖降級的示例程式碼

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
       // Must release read lock before acquiring write lock
       rwl.readLock().unlock();
       rwl.writeLock().lock();
       try {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

 

在釋放寫鎖前,需要先獲得讀鎖,然後再釋放寫鎖。如果不先獲取讀鎖,那麼其他執行緒在這個執行緒釋放寫鎖後可能會修改data,而這種修改對於這個執行緒是不可見的,從而在之後的use(data)中使用的是錯誤的值 。

使用java的ReentrantReadWriteLock讀寫鎖時,鎖降級是必須的麼?

答:不是必須的。

在這個問題裡,如果不想使用鎖降級

  1. 可以繼續持有寫鎖,完成後續的操作。
  2. 也可以先把寫鎖釋放,再獲取讀鎖。

但問題是

  1. 如果繼續持有寫鎖,如果 use 函式耗時較長,那麼就不必要的阻塞了可能的讀流程
  2. 如果先把寫鎖釋放,再獲取讀鎖。在有些邏輯裡,這個 cache 值可能被修改也可能被移除,這個看能不能接受。另外,降級鎖比釋放寫再獲取讀效能要好,因為當前只有一個寫鎖,可以直接不競爭的降級。而先釋放寫鎖,再獲取讀鎖的過程就需要面對其他讀鎖請求的競爭,引入額外不必要的開銷。

鎖降級只是提供了一個手段,這個手段可以讓流程不被中斷的降低到低級別鎖,並且相對同樣滿足業務要求的其他手段效能更為良好。

讀寫鎖雖然分離了讀和寫的功能,使得讀與讀之間可以完全併發,但是讀和寫之間依然是衝突的,讀鎖會完全阻塞寫鎖,它使用的依然是悲觀的鎖策略.如果有大量的讀執行緒,他也有可能引起寫執行緒的飢餓。

在JDK 1.8中引進了讀寫鎖的一個改進版本,StampedLock,有興趣可以瞭解一下https://www.cnblogs.com/huangjuncong/p/9191760.html