JAVA可重入鎖ReentrantLock及其公平性

重入鎖ReentrantLock

顧名思義,就是支持重複進入的鎖,它表示該所能夠支持一個線程對資源的重複加鎖。而除此之外,該所還支持了獲取鎖時的公平和非公平性的選擇。注意,ReentrantLock是一個獨佔鎖。

重入性分析及實現

分析

這裏對比一下synchronized:要知道,synchronized也是支持可重入的,只不過是隱式的,例如,一個synchronized關鍵字修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次的獲取該鎖。

ReentrantLock沒有像synchronized關鍵字一樣支持隱式的重進入,但是在調用lock()方法的時候,已經獲取該鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。

實現

如果你對java線程併發與鎖相關知識很熟悉的話,你會馬上想到AbstractQueueSynchronizer,也就是隊列同步器,以下簡稱AQS。我們知道,Lock()接口是面向使用者的,而AQS是面向實現者的。所以,ReentrantLock也不例外。他就是聚合了內部的一個自定義同步器,而這個自定義同步器又繼承了AQS。

爲了較好的理解,我們這樣思考,假如現在沒有可重入鎖,我們自己實現這樣一個鎖,應該怎麼實現這個可重入的邏輯呢?其實很簡單,可重入的本質就是已經拿到同步狀態(基於AQS的,如果你不瞭解AQS,需要先充電相關內容)的線程可以再次獲取同步狀態,想到這裏,其實就很簡單了,我們只需要在某一個線程獲取到同步狀態後,將該線程與同步狀態綁定,之後所有線程嘗試獲取同步狀態的時候,都要先判斷該線程是不是同步狀態綁定的線程。

ReentrantLock就是基於上述思想,只不過實現了更多的細節,我們來看一下JDK的源碼

(這裏以非公平性選擇爲例。)

仔細理解上述代碼,發現:先判斷同步狀態爲0時候,表示現在還沒有線程獲取,那就CAS原子性獲取同步狀態,成功後,綁定當前線程。else,也就是同步狀態不爲0,在此基礎上,要判斷當前請求線程是否爲同步狀態鎖綁定的線程,如果是,同步狀態數自增,設置新的同步狀態,仍舊返回true,表示成功,也就不會阻塞。除上述兩種情況外,一律返回false,交由上層的Acquire()方法去處理,也就是進入請求等待隊列。

公平性選擇的理解和實現

首先,一定要理解這樣一個問題,我們的ReentrantLock既然是使用了底層的AQS,那自然我們重寫tryAcquire(),然後使用來自AQS的模板方法acquire()去調用。我們也知道,我們重寫的tryAcquire()方法,只能按照自己的邏輯意願去控制每一次獲取鎖的邏輯,一旦獲取失敗,底層的AQS的acquire()方法會去處理。所以獲取失敗的處理邏輯永遠是一致的,只要你使用了AQS,因爲acquire()方法還是final的,不可重寫。

那麼,問題來了,AQS底層的請求等待隊列的管理就是一個雙向隊列,且其處理邏輯就是FIFO(先來先出),也就是說,只要線程請求同步狀態失敗,那麼進入到AQS的請求等待隊列後,無論是公平性鎖,還是非公平性鎖,或是其他任何的鎖,都是一樣的處理邏輯,當然了,這所有的前提都是你使用了AQS,除非,你重寫一個底層的處理框架。

所以,說這麼多,就是爲了理解一件事情,所謂的ReentrantLock的公平性選擇的差異性,是體現在進入到AQS底層請求等待隊列之前的,進入隊列之後,那就只能規規矩矩按照FIFO的順序了。

好了,基於上述概述,爲了更好的理解,我們還是來看JDK的源碼:

這裏先文字描述一下整個ReentrantLock內部的層次關係:

首先有一個核心自定義同步器Syn,繼承了AQS.

然後,基於公平性和非公平性的區別,又對應實現了兩個自定義同步器,繼承Syn。

然後,ReentrantLock有個構造函數,通過傳入boolean來指定該鎖的公平性選擇,以使用對應的同步器。

無參默認構造函數,默認採用的是非公平性的同步器。

傳參boolean構造函數,true公平性,false非公平性。

我們來看一下這兩個自定義同步器的差異:

1.非公平性鎖:

(1)

先記住此處的lock()方法先直接嘗試了一次CAS獲取同步狀態

(2)

2.公平性鎖

(3)

(4)

3.ReentrantLock的lock()方法:

(5)

分析總結:基於上述代碼,我們發現重入鎖的構造函數根據其傳入參數,決定了其使用的自定義同步器,而使用可重入鎖的時候,必然是調用其lock()方法,我們發現底層的lock()方法,其實是調用了所使用的自定義AQS的lock()方法。

而這時候就有區別了:我們知道lock()方法其實應該調用的是底層AQS的acquire()方法,所以既然鎖使用了自定義AQS的lock()方法,那麼在自定義AQS的lock()方法中,應該調用acquire()方法。回到代碼中,我們發現公平性的自定義AQS的lock()方法直接使用了AQS的acquire()方法。而非公平性的lock()方法,則先直接進行了一次CAS的嘗試獲取,獲取失敗才調用AQS的acquire方法。

繼續往下看,重寫的tryAcquire()方法,公平鎖和非公平鎖的自定義AQS的tryAcquire()並沒有什麼不同,當然了,非公平鎖的tryAcquire間接的又往下調用了一層nonfairTryAcquire()的真正實現。我們對比這兩段代碼,發現也就公平性鎖的實現中多出了一條代碼,其他的都一樣。

公平鎖多出的一條代碼。

這調代碼是幹嘛的呢?簡單的理解就是,當前是否有線程正在請求該同步狀態或者底層的請求等待隊列中是否有其他線程在等待。

哈哈,恍然大悟,所謂的公平性鎖,和非公平性鎖的區別:其實就在於這第一次請求鎖時候的處理邏輯,公平性鎖的線程在初次請求同步狀態的時候,如果有線程比起更早的請求同步狀態,他會直接返回false,進入請求等待隊列。而非公平性鎖,他在調用lock()方法的時候,就直接進行一次CAS嘗試獲取同步狀態(非公平性的本質),而不理會是否有比其更早的線程請求同步狀態,如果這一次CAS失敗了,才繼續執行對應的nonTryAcquire()。

公平性選擇的總結:

線程使用ReentrantLock獲取鎖可以抽象的理解爲兩個階段,第一個階段是第一次嘗試獲取,第二個階段是基於AQS底層所維護的請求等待隊列的競爭。所謂的公平性與否,其實就是表現在第一次嘗試獲取上的區別,因爲一旦進入了AQS的請求等待隊列後,無論你是什麼,後續的執行邏輯都是一樣的。

 

 

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