java多線程之共享鎖和ReentrantReadWriteLock源碼解析

前言

前面我們分析了Synchronized(同步鎖),ReentrantLock(獨佔鎖),本篇開始分析ReentrantReadWriteLock(讀是共享鎖,寫是獨佔鎖)。

1、ReentrantReadWriteLock結構圖

在這裏插入圖片描述

2、調用的方法關係圖

在這裏插入圖片描述

3、獲取共享鎖

  1. ReadLock中的lock方法,源碼如下:
public void lock() {
	//Sync繼承AQS,此方法實現在AQS中
     sync.acquireShared(1);
}
  1. AQS中的acquireShared方法,源碼如下
public final void acquireShared(int arg) {
	//先嚐試獲取鎖,獲取成功則返回,失敗則執行doAcquireShared方法再去獲取鎖
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
}
  1. tryAcquireShared()定義在ReentrantReadWriteLock.java的Sync中,源碼如下:
protected final int tryAcquireShared(int unused) {
           
	Thread current = Thread.currentThread();
	// 在讀寫鎖模式下,高16位存儲的是共享鎖(讀鎖)被獲取的次數,低16位存儲的是互斥鎖(寫鎖)被獲取的次數
	int c = getState();
	//如果獨佔鎖(寫鎖)已經被獲取並且獲取獨佔鎖的線程不是當前線程的話,則返回-1
	//如果是獨佔鎖是當前線程獲取,則當前線程也可以獲取讀鎖,鎖降級
	if (exclusiveCount(c) != 0 &&
		getExclusiveOwnerThread() != current)
		return -1;
	//獲取“讀取鎖”的共享計數
	int r = sharedCount(c);
	// 如果不需要阻塞等待,並且“讀取鎖”的共享計數小於MAX_COUNT;
    // 則通過CAS函數更新“讀取鎖”的共享計數+1
	if (!readerShouldBlock() &&
		r < MAX_COUNT &&
		compareAndSetState(c, c + SHARED_UNIT)) {
		//第一次獲取讀取鎖
		if (r == 0) {
			firstReader = current;
			firstReaderHoldCount = 1;
		//如果當前線程是第1個獲取鎖的線程
		} else if (firstReader == current) {
			firstReaderHoldCount++;
		} else {
			// HoldCounter統計的是當前線程獲取“讀取鎖”的次數
			//下面這幾行,就是將 cachedHoldCounter 設置爲當前線程
			HoldCounter rh = cachedHoldCounter;
			if (rh == null || rh.tid != getThreadId(current))
				// cachedHoldCounter 是否緩存的是當前線程,不是的話要到 ThreadLocal 中取
				cachedHoldCounter = rh = readHolds.get();
			else if (rh.count == 0)
				readHolds.set(rh);
			rh.count++;
		}
		return 1;
	}
	//if條件失敗,則進入這個方法
	return fullTryAcquireShared(current);
}
  1. fullTryAcquireShared()在ReentrantReadWriteLock.java的Sync中,源碼如下:
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        // 如果“鎖”被“寫鎖”持有,並且獲取鎖的線程不是current線程;則返回-1。
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 需要阻塞等待
        } else if (readerShouldBlock()) {
        	//進入這裏,說明寫鎖被釋放,讀鎖被阻塞
        	//那麼邏輯就很清楚了,這裏是處理讀鎖重入的
            if (firstReader == current) {
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != current.getId()) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 如果當前線程獲取鎖的計數=0,則返回-1,去排隊。
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 將線程獲取“讀取鎖”的次數+1。
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                // 更新線程的獲取“讀取鎖”的共享計數
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}
  1. doAcquireShared()定義在AQS函數中,源碼如下:
private void doAcquireShared(int arg) {
    // addWaiter(Node.SHARED)方法前面分析過,作用是:
	//創建“當前線程”對應的節點,並將該線程添加到CLH隊列中	
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 獲取當前節點上一個節點
            final Node p = node.predecessor();
            // 如果p是頭節點,則嘗試獲取共享鎖。
            if (p == head) {
				//此處是上面分析過的tryAcquireShared方法,這裏可以看出:
				//doAcquireShared方法其實就是在循環的調用tryAcquireShared來嘗試獲取共享鎖
                int r = tryAcquireShared(arg);
                //如果成功
                if (r >= 0) {
	                // 頭節點後移並傳播
					// 傳播即喚醒後面連續的讀節點
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //此處判斷是否進行阻塞等待,若阻塞等待過程中,線程被中斷過,則設置interrupted爲true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

4、釋放共享鎖

  1. ReadLock中的unLock方法,源碼如下:
public  void unlock() {
	//Sync繼承AQS,此方法實現在AQS中
    sync.releaseShared(1);
}
  1. releaseShared()在AQS中實現,源碼如下:
public final boolean releaseShared(int arg) {
	//這裏的思路和獲取鎖一樣:
	//通過tryReleaseShared()去嘗試釋放共享鎖,成功直接返回
	//失敗,則通過doReleaseShared釋放共享鎖
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
  1. tryReleaseShared()定義在ReentrantReadWriteLock.java的Sync中,源碼如下:
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 如果當前線程是第1個獲取鎖的線程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
		//如果等於 1,那麼這次釋放鎖後就不再持有鎖了,把 firstReader 置爲 null,給後來的線程用
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
		// 判斷 cachedHoldCounter 是否緩存的是當前線程,不是的話要到 ThreadLocal 中取
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
			// 這一步將 ThreadLocal remove 掉,防止內存泄漏。因爲已經不再持有讀鎖了
            readHolds.remove();
            if (count <= 0)
				//這裏的情況是,lock() 一次,unlock() 好幾次纔會觸發
                throw unmatchedUnlockException();
        }
		//當前線程獲取“讀取鎖”的次數-1
        --rh.count;
    }
    for (;;) {
        // 獲取鎖的狀態
        int c = getState();
        // 將鎖的獲取次數-1。
        int nextc = c - SHARED_UNIT;
        // 通過CAS更新鎖的狀態。
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
  1. doReleaseShared()定義在AQS中,源碼如下:
private void doReleaseShared() {
    for (;;) {
        // 獲取CLH隊列的頭節點
        Node h = head;
        // 如果頭節點不爲null,並且頭節點不是隊列最後一個節點
        if (h != null && h != tail) {
            // 獲取頭節點對應的線程的狀態
            int ws = h.waitStatus;
            // 判斷頭節點是否是SIGNAL狀態
            //如果是,下一個節點所對應的線程需要被喚醒。
            if (ws == Node.SIGNAL) {
                // 設置“頭節點對應的線程狀態”爲0。失敗的話,則繼續循環。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 喚醒“頭節點的下一個節點所對應的線程”。
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果頭節點發生變化,則繼續循環。否則,退出循環。
        if (h == head)                   // loop if head changed
            break;
    }
}

5、實例

class CachedData {
    Object data;
    volatile boolean cacheValid;
    // 讀寫鎖實例
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        // 獲取讀鎖
        rwl.readLock().lock();
        if (!cacheValid) { // 如果緩存過期了,或者爲 null
            // 釋放掉讀鎖,然後獲取寫鎖 (後面會看到,沒釋放掉讀鎖就獲取寫鎖,會發生死鎖情況)
            rwl.readLock().unlock();
            rwl.writeLock().lock();

            try {
                if (!cacheValid) { // 重新判斷,因爲在等待寫鎖的過程中,可能前面有其他寫線程執行過了
                    data = ...
                    cacheValid = true;
                }
                // 獲取讀鎖 (持有寫鎖的情況下,是允許獲取讀鎖的,稱爲 “鎖降級”,反之不行。)
                rwl.readLock().lock();
            } finally {
                // 釋放寫鎖,此時還剩一個讀鎖
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }

        try {
            use(data);
        } finally {
            // 釋放讀鎖
            rwl.readLock().unlock();
        }
    }
}

結束語

本篇到此ReentrantReadWriteLock的共享鎖獲取和釋放的源碼就分析完了,下一篇將對信號量Semaphore進行分析。

如果本篇對你有幫助,請點個贊再走,謝謝大家!

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