天天看點

死磕 java同步系列之ReentrantLock源碼解析(一)——公平鎖、非公平鎖

重入鎖是什麼?

ReentrantLock如何實作重入鎖?

ReentrantLock為什麼預設是非公平模式?

ReentrantLock除了可重入還有哪些特性?

(1)重入鎖是什麼?

(2)ReentrantLock如何實作重入鎖?

(3)ReentrantLock為什麼預設是非公平模式?

(4)ReentrantLock除了可重入還有哪些特性?

Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或者形容詞形式,翻譯為進入者或者可進入的,是以Reentrant翻譯為可重複進入的、可再次進入的,是以ReentrantLock翻譯為重入鎖或者再入鎖。

重入鎖,是指一個線程擷取鎖之後再嘗試擷取鎖時會自動擷取鎖。

在Java中,除了ReentrantLock以外,synchronized也是重入鎖。

那麼,ReentrantLock的可重入性是怎麼實作的呢?

死磕 java同步系列之ReentrantLock源碼解析(一)——公平鎖、非公平鎖

ReentrantLock實作了Lock接口,Lock接口裡面定義了java中鎖應該實作的幾個方法:

Lock接口中主要定義了 擷取鎖、嘗試擷取鎖、釋放鎖、條件鎖等幾個方法。

ReentrantLock中主要定義了三個内部類:Sync、NonfairSync、FairSync。

(1)抽象類Sync實作了AQS的部分方法;

(2)NonfairSync實作了Sync,主要用于非公平鎖的擷取;

(3)FairSync實作了Sync,主要用于公平鎖的擷取。

在這裡我們先不急着看每個類具體的代碼,等下面學習具體的功能點的時候再把所有方法串起來。

主要屬性就一個sync,它在構造方法中初始化,決定使用公平鎖還是非公平鎖的方式擷取鎖。

(1)預設構造方法使用的是非公平鎖;

(2)第二個構造方法可以自己決定使用公平鎖還是非公平鎖;

上面我們分析了ReentrantLock的主要結構,下面我們跟着幾個主要方法來看源碼。

彤哥貼心地在每個方法的注釋都加上方法的來源。

這裡我們假設ReentrantLock的執行個體是通過以下方式獲得的:

下面的是加鎖的主要邏輯:

看過之前彤哥寫的【死磕 java同步系列之自己動手寫一個鎖Lock】的同學看今天這個加鎖過程應該思路會比較清晰。

下面我們看一下主要方法的調用關系,可以跟着我的 → 層級在腦海中大概過一遍每個方法的主要代碼:

擷取鎖的主要過程大緻如下:

(1)嘗試擷取鎖,如果擷取到了就直接傳回了;

(2)嘗試擷取鎖失敗,再調用addWaiter()建構新節點并把新節點入隊;

(3)然後調用acquireQueued()再次嘗試擷取鎖,如果成功了,直接傳回;

(4)如果再次失敗,再調用shouldParkAfterFailedAcquire()将節點的等待狀态置為等待喚醒(SIGNAL);

(5)調用parkAndCheckInterrupt()阻塞目前線程;

(6)如果被喚醒了,會繼續在acquireQueued()的for()循環再次嘗試擷取鎖,如果成功了就傳回;

(7)如果不成功,再次阻塞,重複(3)(4)(5)直到成功擷取到鎖。

以上就是整個公平鎖擷取鎖的過程,下面我們看看非公平鎖是怎麼擷取鎖的。

相對于公平鎖,非公平鎖加鎖的過程主要有兩點不同:

(1)一開始就嘗試CAS更新狀态變量state的值,如果成功了就擷取到鎖了;

(2)在tryAcquire()的時候沒有檢查是否前面有排隊的線程,直接上去擷取鎖才不管别人有沒有排隊呢;

總的來說,相對于公平鎖,非公平鎖在一開始就多了兩次直接嘗試擷取鎖的過程。

支援線程中斷,它與lock()方法的主要差別在于lockInterruptibly()擷取鎖的時候如果線程中斷了,會抛出一個異常,而lock()不會管線程是否中斷都會一直嘗試擷取鎖,擷取鎖之後把自己标記為已中斷,繼續執行自己的邏輯,後面也會正常釋放鎖。

題外話:

線程中斷,隻是線上程上打一個中斷标志,并不會對運作中的線程有什麼影響,具體需要根據這個中斷标志幹些什麼,使用者自己去決定。

比如,如果使用者在調用lock()擷取鎖後,發現線程中斷了,就直接傳回了,而導緻沒有釋放鎖,這也是允許的,但是會導緻這個鎖一直得不到釋放,就出現了死鎖。

當然,這裡隻是舉個例子,實際使用肯定是要把lock.lock()後面的代碼都放在try...finally...裡面的以保證鎖始終會釋放,這裡主要是為了說明線程中斷隻是一個标志,至于要做什麼完全由使用者自己決定。

嘗試擷取一次鎖,成功了就傳回true,沒成功就傳回false,不會繼續嘗試。

tryLock()方法比較簡單,直接以非公平的模式去嘗試擷取一次鎖,擷取到了或者鎖本來就是目前線程占有着就傳回true,否則傳回false。

嘗試擷取鎖,并等待一段時間,如果在這段時間内都沒有擷取到鎖,就傳回false。

tryLock(long time, TimeUnit unit)方法在阻塞的時候加上阻塞時間,并且會随時檢查是否到期,隻要到期了沒擷取到鎖就傳回false。

釋放鎖。

釋放鎖的過程大緻為:

(1)将state的值減1;

(2)如果state減到了0,說明已經完全釋放鎖了,喚醒下一個等待着的節點;

未完待續,下一章我們繼續學習ReentrantLock中關于條件鎖的部分

為什麼ReentrantLock預設采用的是非公平模式?

答:因為非公平模式效率比較高。

為什麼非公平模式效率比較高?

答:因為非公平模式會在一開始就嘗試兩次擷取鎖,如果當時正好state的值為0,它就會成功擷取到鎖,少了排隊導緻的阻塞/喚醒過程,并且減少了線程頻繁的切換帶來的性能損耗。

非公平模式有什麼弊端?

答:非公平模式有可能會導緻一開始排隊的線程一直擷取不到鎖,導緻線程餓死。