Java併發程式設計-讀寫鎖(ReentrantReadWriteLock)

markfork發表於2018-05-27

章節目錄

  • ReentrantReadWriteLock 特性
  • 讀寫鎖介面示例
  • 讀寫鎖的實現分析
    • 讀寫狀態設計
    • 寫鎖的釋放與獲取
    • 讀鎖的釋放與獲取
    • 鎖降級

1. ReentrantReadWriteLock 特性

1.1 讀寫鎖定義

讀寫鎖維護了一對鎖,一個讀鎖,一個寫鎖,通過分離讀鎖寫鎖,使得併發性相比一般的排他鎖有了很大提升。

1.2 讀寫鎖使用場景

1.讀寫鎖比較適用於讀多寫少的應用場景。
2.讀寫鎖在統一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒、其他寫執行緒均被阻塞。

1.3 讀寫鎖的優點

1.保證寫操作對讀操作的可見性
2.在讀多寫少的情況下的併發性的提升
3.讀寫鎖簡化可讀寫互動場景的程式設計方式

2.讀寫鎖介面示例

如下為使用讀寫鎖操作快取的示例

package org.seckill.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteCache {
    //充當cache
    static Map<String, Object> map = new HashMap<String, Object>();
    //例項化讀寫鎖物件
    static ReentrantReadWriteLock reentrantReadWriteLock =
            new ReentrantReadWriteLock();
    //例項化讀鎖
    static Lock r = reentrantReadWriteLock.readLock();
    //例項化寫鎖
    static Lock w = reentrantReadWriteLock.writeLock();

    //獲取快取中值
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    //寫快取中值,並返回對應value
    public static final Object set(String key, Object obj) {
        w.lock();
        try {
            return map.put(key, obj);
        } finally {
            w.unlock();
        }
    }

    //清空所有內容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

如上所示:

1.Cache組合一個非執行緒安全的HashMap做為快取實現,同時使用讀寫鎖的
讀鎖和寫鎖來保證Cache是執行緒安全的。
2.在讀操作get(String key)方法中,需要使用讀鎖,這使得併發訪問該方法時不
會被阻塞。
3.寫鎖put(String key,Object object)方法和clear()方法,在更新HashMap時必須
提前獲取寫鎖,當獲取寫鎖後,其他執行緒對於讀鎖和寫鎖的獲取都被阻塞,只
有寫鎖釋放之後,其他的讀寫操作才能繼續操作,也就是說寫鎖其實是排他
鎖、互斥鎖。
4.最終,讀鎖提升了讀操作的併發性,也保證了每次寫操作對所有後續讀操作
的可見性,同時簡化了程式設計方式,對應1.3
 

3.讀寫鎖的實現分析

3.1 讀寫狀態設計

1.讀寫鎖同樣依賴自定義同步器實現同步功能
2.ReentrantLock 中同步狀態表示鎖被一個執行緒重複獲取的次數。
3.讀寫鎖自定義同步器需要在同步狀態上維護多個讀執行緒和一個寫執行緒的狀態。
4.讀寫鎖同步器採用在一個4位元組的整形變數上使用 按位切割 的方式來維護讀
寫執行緒的同步狀態。高16位用來表示讀,低16位用來表示寫。
5.寫狀態增加1,表示當前執行緒獲取寫鎖,則 Status = S(當前同步狀態)+1,當讀
狀態加1時,Status = S+(1<<16)

3.2 寫鎖的獲取與釋放
如下原始碼所示:

 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)
                //1.如果同步狀態不為0,且寫狀態為0,則表示當前同步狀態被讀鎖獲取
                //2.或者當前擁有寫鎖的執行緒不是當前執行緒
                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;
        }

3.3 讀鎖的釋放與獲取

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

如果其他執行緒獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。
如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒安全,依靠CAS保證增加讀狀態,成功獲取鎖。

3.4 鎖降級

鎖降級是指當前把持住寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

鎖降級過程中的讀鎖的獲取是否有必要,答案是必要的。主要是為了保證資料的可見性,如果當前執行緒不獲取讀鎖而直接釋放寫鎖,假設此刻另一個執行緒獲取的寫鎖,並修改了資料,那麼當前執行緒就步伐感知到執行緒T的資料更新,如果當前執行緒遵循鎖降級的步驟,那麼執行緒T將會被阻塞,直到當前執行緒使資料並釋放讀鎖之後,執行緒T才能獲取寫鎖進行資料更新。


相關文章