Java中的鎖——ReentrantReadWriteLock(讀寫鎖)

上一篇裏講的ReentrankLock是一種排他鎖,即同一時間只能有一個線程進入。而讀寫鎖在同一時刻允許多個線程訪問,但是在線程訪問時,所有的讀線程和其他線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀寫鎖,使得併發性比一般的排它鎖有了很大提升。因爲大多數應用場景都是讀多於寫的,因此在這樣的情況下,讀寫鎖可以提高吞吐量。下圖描述了關於讀寫鎖的三個特性:公平性、重入性和鎖降級


1 讀寫鎖的使用

現在有這樣一個需求,需要構造一個數據結構作爲一個系統中的臨時緩存,需要保證線程安全。我們在這裏就可以利用讀寫鎖來實現。
/**
 * 利用讀寫鎖實現的一個數據結構
 * @author songxu
 *
 */
public class ReadWriteCache 
{
	//利用hashmap作爲底層數據結構
	private Map<String, Object> cache=new HashMap<String, Object>();
	//構造讀寫鎖
	private ReentrantReadWriteLock readwritelock=new ReentrantReadWriteLock();
	//讀鎖
	private Lock readLock=readwritelock.readLock();
	//寫鎖
	private Lock writeLock=readwritelock.writeLock();
	
	/**
	 * 存入數據
	 * @param key  鍵
	 * @param value 值
	 */
	public void put(String key,Object value)
	{
		writeLock.lock();
		//鎖一定在try塊之外
		try {
			cache.put(key, value);
		} 
		finally
		{
			writeLock.unlock();
		}
	}
	/**
	 * 獲取數據
	 * @param key 鍵
	 * @return  值
	 */
	public Object get(String key)
	{
		readLock.lock();
		try {
			return cache.get(key);
		} 
		finally
		{
			readLock.unlock();
		}
	}

}
在上述代碼中,put方法在更新或插入數據前必須提前獲取寫鎖,當獲取寫鎖之後,其他線程對於讀鎖和寫鎖的獲取均被阻塞,只有寫鎖釋放後,其他讀操作才能繼續。在get方法中,需要獲取讀鎖,而此時其他線程均可訪問該方法而不被阻塞。可以說這是一個類似於concurrentHashMap的原型,但它的效率肯定沒有concurrentHashMap高,但應該比HashTable要強一些

2 讀寫鎖的實現原理

2.1 讀寫狀態

讀寫鎖同樣利用同步器實現鎖的功能,在ReetrantLock中,同步狀態表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態上維護多個讀線程和一個寫線程的狀態。如果想在一個整型變量上維護這樣一個狀態,那麼採用按位分割的方式是一個不錯的選擇。如下圖所示,將一個變量分爲兩個部分,高16位表示讀,低16位表示寫


上圖中同步狀態表示一個線程已經獲取了3次讀鎖,同時也連續獲取2次寫鎖。想要快速讀取這個狀態,可以通過位運算。當前狀態爲S,寫狀態等於S&0x0000FFFF,讀狀態等S>>>16。當讀狀態+1時,等於S+1,寫狀態加1等於S+(1<<16)。其實現代碼如下所示

/*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }



2.2 寫鎖的獲取與釋放

寫鎖是一個支持重進入的排它鎖。如果當前線程獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已經獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態。
  • 讀鎖的獲取實際定義在內部同步器Sync的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) {
                //如果存在讀鎖 或者當前線程不是獲取寫鎖的線程 返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 增加寫狀態
                setState(c + acquires);
               return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
該方法的邏輯很簡單,首先判斷讀寫狀態。如果不爲0且存在讀鎖或者已存在的寫鎖並非當前線程獲取到,則寫鎖不能獲取,只能等待其他線程都釋放了鎖,才能獲取。

  • 讀鎖的釋放實際定義在內部同步器Sync的tryRelease方法中,其源碼如下:

 protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
這個釋放過程與ReentrantLock的過程基本類似,每次釋放均減少些狀態,當寫狀態爲0時表示寫鎖已經被釋放。

2.3 讀鎖的獲取與釋放


讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他線程訪問時,讀鎖總會被成功地獲取。如果當前線程已經獲取了讀鎖,則增加讀狀態,如果獲取讀鎖時寫鎖已經被其他線程獲取,則進入等待狀態。

  • 讀鎖的獲取定義在內部同步器Sync的tryAcquireShared方法中
	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 &&
		//CAS更新狀態 因爲可能多線程操作
                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 != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }
這個tryRelease方法首先需要檢查是否其他線程已經獲取了寫鎖,如果被獲取則當前線程失敗,進入等待狀態。否則如果當前線程滿足對state的操作條件,就利用CAS設置state+SHARED_UNIT,實際上就是讀狀態+1。但是需要注意,這個state是全局的,即所有線程獲取讀鎖次數的總和,而爲了方便計算本線程的讀鎖次數以及釋放掉鎖,需要在ThreadLocal中維護一個變量。這就是HoldCounter。源碼下半部分的基本做的事情就是在讓HoldCounter加一。


  • 讀鎖的釋放在內部同步器Sync的tryReleaseShared方法中

	protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
	    //如果當前線程是第一個讀者
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
		// 如果第一個讀者的讀鎖已經爲1 那麼第一個讀者置爲null
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
		//第一個讀者讀鎖數量減一
                    firstReaderHoldCount--;
            } else {
		//否則從緩存中獲取當前線程的讀鎖數量
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;//讀鎖數量減一
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;// state讀鎖狀態減一
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0; //如果state爲0,表示無鎖狀態,返回true
            }
        }


這個釋放的過程大致分爲兩步,第一步減少holdCounter的讀鎖值,如果已經減爲一,則移除holdCounter。第二步就是將state的讀狀態減一。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章