《Java後端知識體系》系列之獨佔鎖 Reentrant Lock 的原理

週末兩天跟女朋友出去high了一下,所以自己的知識整理也擱置了兩天,手動狗頭!

ReentrantLock概覽

ReentrantLock是可重入的獨佔鎖,所以只能有一個線程獲得該鎖,,其它獲取該鎖的線程會被阻塞而被放入該鎖的AQS阻塞隊列中,以下是ReentrantLock的類圖。
在這裏插入圖片描述
從以上類圖中可以看到,ReentrantLock最終還是使用AQS來實現的,並且根據參數來決定其內部是一個公平鎖還是非公平鎖,默認是非公平鎖。

public ReentrantLock(){
	sync = new NofairSync();
}
public ReentrantLock(boolean fair){
	sync = fair ? new FairSync() : new NofairSync();
}

其中Sync類直接繼承自AQS,它的子類NofairSync和FairSync分別實現了獲取鎖的非公平與公平策略。
在這裏AQS的state狀態值表示線程獲取鎖的可重入次數,在默認情況下state的值爲0表示當前鎖沒有任何線程持有。當一個線程第一次獲取該鎖時會嘗試使用CAS來設置state的值爲1,如果CAS成功則當前線程獲取了該鎖,然後記錄該鎖的持有者爲當前線程。在線程沒有釋放鎖的情況下第二次獲取該鎖後,state被設置爲2,這就是可重入次數。在該線程釋放的時候,會通過CAS操作讓狀態值state減1,如果減1後狀態值爲0,則釋放該鎖,否則繼續釋放鎖直到state爲0才最終放棄鎖。

獲取鎖

1、void lock()方法

當一個線程調用該方法時,說明該線程希望獲取該鎖。

  • 如果鎖當前沒有被其它線程佔用並且當前線程沒有獲取過該鎖,則當前線程會獲取到該鎖,然後設置當前鎖的持有者爲當前線程,並且設置state的值爲1,然後直接返回。
  • 如果當前線程已經持有了該鎖,則只是通過CAS操作將state加1,然後返回。
  • 如果該鎖已經被其它線程持有,那麼調用該方法的線程會被放入AQS隊列後阻塞掛起。
public void lock(){
	sync.lock();
}

在如上代碼中。ReentrantLock的lock()委託給了sync類,根據創建ReentrantLock構造函數選擇sync的實現是NofairSync還是FairSync,這個鎖是一個非公平鎖還是公平鎖。這裏先看sync的子類NofairSync的情況,也就是非公平鎖時。

final void lock(){
	//(1)CAS設置狀態值state
	if(compareAndSetState(0,1))
		//設置鎖的持有者爲當前線程
		setExclusiveOwnerThread(Thread.currentThread());
	else
	//(2)調用AQS的acquire方法
		acquire(1);

在以上代碼中,因爲默認state的值爲0,所以第一個調用Lock的線程會通過CAS設置狀態值爲1,CAS成功則表示當前線程獲取到了鎖,然後通過setExclusiveOwnerThread設置該鎖的持有者爲當前線程。
如果這時候有其它線程調用lock方法企圖獲取該鎖,CAS會失敗,然後調用AQS的acquire方法。注意,傳遞參數爲1

public final acquire(int arg){
	//(3)調用ReentrantLock重寫的tryAcquire方法
	//tryAcquire返回false會把當前線程放入AQS的阻塞隊列中,通過addWaiter方法將當前線程放入AQS的阻塞隊列中
	if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
		selfInterrupt();

之前說過AQS並沒有提供可用的tryAcquire方法,tryAcquire方法需要子類自己定製化,所以這裏代碼(3)會調用ReentrantLock重寫的tryAcquire方法。接下來看非公平鎖的代碼:

protect final boolean tryAcquire(int acquires){
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
   int c = getState();
   //(4)當前AQS的狀態值
   if (c == 0) {
       if (compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   //(5)當前線程是該鎖的持有者
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0) // overflow
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}
  • 首先代碼4中會查看當前鎖的狀態值是否爲0,爲0則表示該鎖空閒,那麼就通過CAS嘗試獲取鎖,將state的值設置爲1,並設置鎖的佔有者爲當前線程,然後返回true。如果當前狀態值不爲0,則說明該鎖已經被其它線程佔用,所以代碼5會判斷當前鎖的佔有者是否爲當前線程,如果鎖的持有者爲當前線程則state加1,然後返回true,這裏需要注意nextc<0說明可重入次數溢出了。如果當前線程不是鎖的持有者則返回false,然後將線程放入AQS阻塞隊列。
  • 介紹完非公平鎖的實現代碼,現在看看非公平鎖是怎麼體現的,首先非公平是說先嚐試獲取鎖的線程並不一定比後嘗試獲取鎖的線程優先獲取鎖。
  • 假設線程A先調用lock時執行nonfairTryAcquire的代碼4發現當前狀態值不爲0,所以執行代碼5發現鎖的持有者不是自己,則返回false,然後被放入阻塞隊列。
  • 然後線程B調用lock方法,執行nonfairTryAcquire,發現當前state的值爲0了(其它線程此時剛好已經釋放了該鎖),所以通過CAS獲取到了該鎖。
  • 雖然線程A先嚐試獲取鎖,但是後來的線程B卻獲得了鎖,這就是非公平的體現。這裏線程B在獲取鎖前並沒有查看當前AQS阻塞隊列中是否有比自己更早請求該鎖的線程,而是使用了搶奪策略。那麼接下來看看公平鎖是如何實現的。
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //(7)判斷當前狀態值是否爲0
            if (c == 0) {
            	//(8)公平策略
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //(9)當前線程是該鎖的持有者
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
  • 如上代碼中,公平的tryAcquire策略與非公平的類似,不同之處在於,代碼8在設置CAS前添加了hasQueuedPredecessors方法,該方法是實現公平鎖的核心,代碼如下
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • 在如上代碼中如果當前線程節點有前驅節點則返回true,否則設置當前AQS隊列爲空或者當前線程是AQS的第一個節點則返回false。如果h==t則說明當前隊列爲空,直接返回false,如果h!=t並且s=null則說明有一個元素將要作爲AQS的第一個節點入隊,那麼返回true,如果h!=t並且s!=null和s.thread!=Thread.currentThread()則說明隊列裏面的第一個線程不是該線程則返回true。

2、void locklnterruptibly()方法

該方法與lock方法類似,它的不同在於它是對中斷進行響應,就是當前線程在調用該方法時,如果其它線程調用了當前線程的interrupt方法,則當前線程會拋出InterruptedException異常,然後返回

public void lockinterruptibly() throws InterruptedException{
	sync.acquirelnterruptibly(1);
}
public final void acquireinterruptibly(int arg)throws InterruptedException{
	//如果當前線程被中斷,則直接拋出異常
	if(Thread.interrupted())
		throws new terruptedException();
	//嘗試獲取資源
	if(!tryAcquire(arg)){
		//調用AQS可被中斷的方法
		doAcquireinterruptibly(arg);
	}

3、boolean trylock()方法

嘗試獲取鎖,如果當前該鎖沒有被其它線程佔用,則當前線程獲取該鎖並返回true,否則返回false。注意,該方法不會引起當前線程阻塞。

 public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

如上代碼中與非公平鎖的tryAcquire()方法類似,所以tryLock()使用的是非公平策略。

4、boolean tryLock(long timeout,TimeUnit unit)方法

嘗試獲取鎖,與tryLock不同之處在於,它設置了超時時間。如果超時時間到了卻還沒獲取到鎖則返回false。

  public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

釋放鎖

1、void unlock()方法

嘗試釋放鎖,如果當前線程持有該鎖,則調用該方法會讓該線程對線程持有的AQS狀態值減1,如果減1之後狀態值爲0,則當前線程會釋放該鎖,否則僅僅減1(重入鎖),需要繼續釋放鎖,直到狀態值爲0。如果當前線程沒有持有該鎖,而調用了該方法則會拋出IllegalMonitorStateException異常,代碼如下:

    public void unlock() {
        sync.release(1);
    }
    protected final boolean tryRelease(int releases) {
    	//(11)如果不是鎖的持有者調用unlock則拋出異常
       int c = getState() - releases;
       if (Thread.currentThread() != getExclusiveOwnerThread())
           throw new IllegalMonitorStateException();
       boolean free = false;
       //(12)如果當前可重入次數爲0,則清空持有線程
       if (c == 0) {
           free = true;
           setExclusiveOwnerThread(null);
       }
       //(13)設置可重入次數原始值減1
       setState(c);
       return free;
   }

如代碼11中如果當前線程不是該鎖的持有者,則直接拋出異常,否則查看狀態值是否爲0,爲0則說明當前線程要放棄該鎖的持有全,則執行代碼12把當前鎖的持有者設爲null。如果狀態值不爲0,則僅僅讓當前線程對鎖的可重入次數減1。

以上就是整理的ReentrantLock的知識點,自己也梳理了整個流程。

自己依然是個會敲代碼的湯姆貓!

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