一、引言
話不多說,扶我起來,我還可以繼續擼。
在學習ReentrantLock源碼之前,先來回顧一下鏈表、隊列數據結構的基本概念~~
二、數據結構
2.1 鏈表(Linked List)
小學一、二年級的時候,學校組織戶外活動,老師們一般都要求同學之間小手牽着小手。這個場景就很類似一個單鏈表。每個小朋友可以看作一個節點信息,然後通過牽手的方式,形成整個鏈表結構。
1、鏈表是以節點的形式來存儲數據,可以稱之爲:鏈式存儲
2、每個節點都包含所需要存放對應的數據(data 域),以及指向下一個節點的元素(next 域)。
3、鏈表可以帶頭節點也可以不帶頭節點,根據實際需求來確定,頭節點一般不會存放具體數據,只會指向下一個節點。
4、鏈表總的來說可以分之爲幾種類型:單鏈表、雙向鏈表、環形鏈表(循環鏈表)
單鏈表(帶頭節點) 結構示意圖:
單鏈表總的來看,理解比較簡單,但是缺點也是顯而易見的,查找的方向只能是一個方向,並且在某些操作下,單鏈表會比較費勁。比如說在刪除某個單鏈表節點時,我們需要找到刪除節點的,前一個節點才能夠進行刪除。
這個時候,就有了我們雙向鏈表:
雙向鏈表(帶頭節點) 結構示意圖:
相對應單鏈表來說,雙向鏈表多了一個pre屬性,這個屬性會指向當前節點的上一個節點,所以稱之爲雙向鏈表。
換句話來說雙向鏈表就是你中有我,我中有你哈哈哈哈~~~~
環形鏈表(循環鏈表)結構示意圖:
環形鏈表也就是,鏈表最後一個節點,指向了頭節點,整體構成一個環形。其實理解了單鏈表結構,後面兩種結構都比較好理解。
2.2 隊列(Queue)
隊列其實只要記住最重要特點:遵循先入先出的原則,先存入的數據,先取出,後存儲的數據後取出。
在換到生活場景來說,最簡單的就是排隊,最先排隊的人,弄完事最先走了,也就是出隊列了。
隊列也是線性表的一種,它只允許在表的前端進行進行刪除操作,在表的後端進行插入操作。進行刪除操作端叫做隊頭,進行插入的一端叫做隊尾。
這裏小編多的就不講了,相信作爲一名碼農來說,這兩種都是很基本、很基本、很基本的數據結構了。
三、AQS隊列同步器
3.1 基本介紹
AQS是什麼呢? 全稱是AbstractQueuedSynchronizer,中文就是隊列同步器,簡單暴力來說,它對應我們Java中的一個抽象類,AQS是ReentrantLock很重要的實現部分。
首先我們需要了解到,在AQS中包含了哪些重要內容,小編這裏給列舉部分出來了。
這裏代碼小編省略很多了,展示了我們所需要關心的內容。(省的擔心你們說小編亂畫圖)
// @author Doug Lea
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
static final class Node {
// 指向上一個節點
volatile Node prev;
// 指向下一個節點
volatile Node next;
// 存放具體的數據
volatile Thread thread;
// 線程的等待狀態
volatile int waitStatus;
}
// 頭節點
private transient volatile Node head;
// 尾節點
private transient volatile Node tail;
// 鎖狀態
private volatile int state;
}
3.2 AQS在ReentrantLock中起什麼作用呢?
假設現在要求小夥伴們自己實現一把鎖,你們會怎麼去設計一把鎖呢?
最容易想到的方案就是,首先肯定要有個鎖狀態(假設就是個int 變量 0 自由狀態、1 被鎖狀態),如果一個線程獲取到了鎖,就把這個鎖狀態改成 1,線程釋放鎖就改成0。 那又假設現在我們線程一獲取到了鎖,線程二來了怎麼辦? 線程二又要去哪裏等着呢? 這個時候AQS就給你提供了一系列基本的操作,讓開發者更加專注鎖的實現。
AQS這種設計屬於模板方法模式(行爲型設計模式),使用者需要繼承這個AQS並重寫指定的方法,最後調用AQS提供的模板方法,而這些模板方法會調用使用者重寫的方法。
這麼說把,AQS是用來構建鎖的基礎框架,主要的使用方式是繼承,子類通過繼承AQS並實現它的一系列方法來管理同步狀態。還有我們實現一把鎖肯定避免不了對鎖狀態的更改,AQS還提供了以下三個方法:
getState(): 獲取當前鎖狀態
setState(int newState): 設置當前鎖狀態
compareAndSetState(int expect, int update):CAS設置鎖狀態,CAS能夠保證原子性操作,小編上一篇文章講sync有具體講到。
看到這裏,希望小夥伴能夠對AQS這個抽象類有個大概的認識。
四、ReentrantLock 加鎖過程源碼分析
本文主要注重ReentrantLock 加鎖、解鎖過程源碼分析!!!
本文都是以公平鎖爲主,如果弄懂了公平鎖的過程,再回頭過看看非公平鎖,就很輕鬆了,這個就交給小夥伴你們自己了~
4.1 ReentrantLock結構圖
整體看下ReentrantLock結構:先來個IDEA裏面展示的結構圖,然後小編再結合畫一個更簡單明瞭的結構圖。
4.2 ReentrantLock 重入鎖
重入鎖簡單來說一個線程可以重複獲取鎖資源,雖然ReentrantLock不像synchronized關鍵字一樣支持隱式的重入鎖,但是在調用lock方法時,它會判斷當前嘗試獲取鎖的線程,是否等於已經擁有鎖的線程,如果成立則不會被阻塞(下面講源碼的時候會講到)。
還有ReentrantLock在創建的時候,可以通構造方法指定創建公平鎖還是非公平鎖。這裏是個細節部分,如果知道有公平鎖和非公平鎖,但是不知道怎麼創建,這樣還敢說看過源碼?
// ReentrantLock 構造方法
// 默認非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
// 傳入true,創建公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
怎麼理解公平鎖和非公平鎖呢? 先對鎖進行獲取的線程一定先拿到鎖,那麼這個鎖是公平的,反之就是不公平的。
比如:排隊買包子,大家都一一排隊進行購買那麼就是公平的,但是如果有人插隊,那就變成不公平了。憑啥你這個後來的還先買包子,就這個意思拉~~
4.3 lock方法
以下就是一個簡單鎖的演示了,簡單的加鎖解鎖。
public class ReentrantLockTest {
public static void main(String[] args) {
// 創建公平鎖
ReentrantLock lock = new ReentrantLock(true);
// 加鎖
lock.lock();
hello();
// 解鎖
lock.unlock();
}
public static void hello() {
System.out.println("Say Hello");
}
}
既然我們是看加鎖的過程,就從lock方法開始下手唄,前方高能,請注意準備~~~
點進去之後看到了調用了sync對象的lock方法,sync是我們ReentrantLock中的一個內部類,並且這個sync繼承了AQS這個類。
public void lock() {
sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 抽象方法,由公平鎖和非公平鎖具體實現
abstract void lock();
// ..... 代碼省略
}
通過快捷鍵查看,有兩個類對Sync中的lock方法進行了實現,我們先看公平鎖:FairSync
看代碼得知,lock方法最後調用了acquire方法,並且傳入了一個參數,值爲:1,那我們再繼續跟下去~
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// ...... 代碼省略
}
這個時候我們就來到AQS爲我們提供的方法了,接下來小編一一講解~~
public final void acquire(int arg) {
// 第一個調用了tryAcquire方法,這方法判斷能不能拿到鎖
// 強調,這裏的tryAcquire的結果,最後是取反,最前面加了 !運算
if (!tryAcquire(arg) &&
// 後面的方法,慢慢道來,先保持神祕感
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.3 tryAcquire方法
從方法名上看,字面意思就是嘗試獲取,獲取什麼呢? 那當然是獲取鎖呀。
從acquire()點擊tryAcquire方法進去看,AQS爲我們提供了默認實現,默認如果沒重寫該方法,則拋出一個異常,這裏就很突出模板方法模式這種設計模式的概念,提供了一個默認實現。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
同樣,我們查看公平鎖的實現 ~
最後來到了FairSync對象中的tyrAcquire方法了,重點來啦~~
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// 嘗試獲取鎖 拿到鎖了返回:true,沒拿到鎖返回:false
protected final boolean tryAcquire(int acquires) {
// 獲取當前線程
final Thread current = Thread.currentThread();
// 獲取鎖狀態 , 自由狀態 = 0,被上鎖 = 1 ,> 1 表示重入
int c = getState();
// 判斷當前狀態是否等於自由狀態
if (c == 0) {
// hasQueuedPredecessors 判斷自己需不需要排隊,這個方法比較複雜,在下面補充部分詳細解釋,返回值,不需要排隊返回false,然後取反,需要排隊返回true
if (!hasQueuedPredecessors() &&
// compareAndSetState 如果不需要排隊則直接進行CAS嘗試加鎖,成功則直接方法true
compareAndSetState(0, acquires)) {
// 成功獲取鎖,把當前線程設置成鎖的擁有者,爲了後續方便判斷是不是可重入鎖
setExclusiveOwnerThread(current);
return true;
}
}
// 判斷當前線程是否等於鎖的持有線程,這裏也證明了ReentrantLock是可重入鎖
else if (current == getExclusiveOwnerThread()) {
// 如果是重複鎖,計數器 + 1
int nextc = c + acquires;
// 正常來說nextc不可能會小於0,於是判斷如果小於0則直接拋出異常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 賦值計數器+1的結果
setState(nextc);
// 如果重入成功返回true
return true;
}
// 如果c不等於0,並且當前線程不等於持有鎖的線程,直接返回false,因爲就代表着有其他線程拿到鎖了
return false;
}
}
tryAcquire方法執行完成,又回到這裏: tryAcquire方法拿到鎖返回結果:true,沒拿到鎖返回:false。
一共分兩種情況:
第一種情況,拿到鎖了,結果爲true,通過取反,最後結果爲false,由於這裏是 && 運算,後面的方法則不會進行,直接返回,代碼正常執行,線程也不會進入阻塞狀態。第二種情況,沒有拿到鎖,結果爲false,通過取反,最後結果爲true,這個時候,if判斷會接着往下執行,執行這句代碼:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先執行addWaiter方法。
public final void acquire(int arg) {
// tryAcquire執行完,回到這裏
if (!tryAcquire(arg) &&
// Node.EXCLUSIVE 這裏傳進去的參數是爲null,在Node類裏面
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.4 addWaiter方法
看到這裏,還記得我們AQS裏面有個head、tail,以及Node吧,如果印象模糊了,趕緊翻上去看看。
AQS在初始化的時候,數據大概是這個樣子的,這個時候隊列還沒初始化的狀態,所以head、tail都是爲空。
addWaiiter這個方法總的來說做了什麼事呢?
核心作用:把沒有獲取到的線程包裝成Node節點,並且添加到隊列中
具體邏輯分兩步,判斷隊列尾部節點是不是爲空,爲空就去初始化隊列,不爲空就維護隊列關係。
這裏需要小夥伴掌握雙向鏈表數據結構,才能更容易的明白怎麼去維護一個隊列的關係。
private Node addWaiter(Node mode) {
// 因爲在AQS隊列裏面,節點元素是Node,所以需要把當前類包裝成一個node節點
Node node = new Node(Thread.currentThread(), mode);
// 把尾節點,賦值給pred,這裏一共分兩種兩種情況
Node pred = tail;
// 判斷尾部節點等不等於null,如果隊列沒有被初始化,tail肯定是個空
// 反而言之,如果隊列被初始化了,head和tail都不會爲空
if (pred != null) {
// 整個就是維護鏈表關係
// 把當前需要加入隊列元素的上一個節點,指向隊列尾部
node.prev = pred;
// CAS操作,如果隊列的尾部節點是等於pred的話,就把tail 設置成 node,這個時候node就是最後一個節點了
if (compareAndSetTail(pred, node)) {
// 把之前尾部節點的next指向最後的的node節點
pred.next = node;
return node;
}
}
// 初始化隊列
enq(node);
return node;
}
到這裏小夥伴要記住:AQS隊列默認是沒有被初始化的,只有當發生競爭的時候,並且有線程沒有拿到鎖纔會初始化隊列,否則隊列不會被初始化~
什麼情況下不會被初始化呢?
1、線程沒有發生競爭的情況下,隊列不會被初始化,由tryAcquire方法就可以體現出,如果拿到鎖了,就直接返回了。
2、線程交替執行的情況下,隊列不會被初始化,交替執行的意思是,線程執行完代碼後,釋放鎖,線程二來了,可以直接獲取鎖。這種就是交替執行,你用完了,正好就輪到我用了。
4.5 enq方法
這個方式就是爲了初始化隊列,參數是由addWaiter方法把當前線程包裝成的Node節點。
// 整個方法就是初始化隊列,並且把node節點追加到隊列尾部
private Node enq(final Node node) {
// 進來就是個死循環,這裏看代碼得知,一共循環兩次
for (;;) {
Node t = tail;
// 第一次進來tail等於null
// 第二次進來由於下面代碼已經把tail賦值成一個爲空的node節點,所以t現在不等於null了
if (t == null) {
// CAS把head設置成一個空的Node節點
if (compareAndSetHead(new Node()))
// 把空的頭節點賦值給tail節點
tail = head;
} else {
// 第二次循環就走到這裏,先把需要加入隊列的上一個節點指向隊列尾部
node.prev = t;
// CAS操作判斷尾部是不是t如果是,則把node設置成隊列尾部
if (compareAndSetTail(t, node)) {
// 再把之前鏈表尾部的next屬性,連接剛剛更換的node尾部節點
t.next = node;
return t;
}
}
}
}
通過enq代碼我們可以得知一個很重要、很重要、很重要的知識點,在隊列被初始化的時候,知道隊列第一個元素是什麼麼? 如果你認爲是要等待線程的node節點,那麼你就錯了。
通過這兩句代碼得知,在隊列初始化的時候,是new了一個空Node節點,賦值給了head,緊接着,又把head 賦值給tail。
if (compareAndSetHead(new Node()))
// 把空的頭節點賦值給tail節點
tail = head;
初始化完成後,隊列結構應該是這樣子的。
隊列初始化後,緊接着第二次循環對不對,t就是我們的尾部節點,node就是要被加入隊列的node節點,也就是我們所謂要等待的線程的node節點,這裏代碼執行完後,直接return了,循環終止了。
// 第二次循環就走到這裏,先把需要加入隊列的上一個節點指向隊列尾部
node.prev = t;
// CAS操作判斷尾部是不是t如果是,則把node設置成隊列尾部
if (compareAndSetTail(t, node)) {
// 再把之前鏈表尾部的next屬性,連接剛剛更換的node尾部節點
t.next = node;
return t;
}
看了這幅圖,哪怕對雙向鏈表不熟悉,應該也可以看懂了吧, skr skr skr ~~~~
記住,這裏隊列初始化的時候,第一個元素是空,隊列裏面存在兩個元素,切記切記切記,這也是面試需要注意的細節,把這個點勇敢的、大聲的、自信的出說來,肯定能夠證明你是看過源碼的。
好了,最終addWaiter方法會返回一個初始化並且已經維護好,隊列關係的Node節點出來。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// addWaiter返回Node,緊接着調用acquireQueued 方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.5 acquireQueued方法
看到這裏,也就是我們lock方法的接近尾聲了,我們拿到了隊列中的數據,猜猜接下來需要做什麼?
既然沒拿到鎖,就讓線程進入阻塞狀態,但是肯定不是直接就阻塞了,還需要經過一系列的操作,看源碼:
// node == 需要進隊列的節點、arg = 1
final boolean acquireQueued(final Node node, int arg) {
// 一個標記
boolean failed = true;
try {
// 一個標記
boolean interrupted = false;
// 又來一個死循環
for (;;) {
// 獲取當前節點的上一個元素,要麼就是爲頭部節點,要麼就是其他排隊等待的節點
final Node p = node.predecessor();
// 判斷是不是頭部節點,如果是頭部節點則代表當前節點是排隊在第一個,有資格去獲取鎖,也就是自旋獲取鎖
// 如果不是排隊在第一個,前面的人還在排隊後面的就更別說了去獲取鎖了
// 就比如食堂打飯,有人正在打飯,而排隊在第一個人的可以人,可以去看看前面那個人打完了沒
// 因爲他前面沒有沒人排隊,而後面的人就不同,前面的人還在排隊,那麼自己就只能老老實實排隊了
if (p == head && tryAcquire(arg)) {
// 如果能進入到這裏,則代表前面那個打飯的人已經搞完了,可以輪第一個排隊的人打飯了
// 既然前面那個人打完飯了,就可以出隊列了,會把thread、prev、next置空,等待GC回收
setHead(node);
p.next = null; // help GC
failed = false;
// 返回false,整個acquire方法返回false,就出去了
return interrupted;
}
// 如果不是頭部節點,就要過來等待排隊了
// shouldParkAfterFailedAcquire 這方法會使當前循環再循環一次,相當於自旋一次獲取鎖
if (shouldParkAfterFailedAcquire(p, node) &&
// 隊列阻塞,整個線程就等待被喚醒了
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
看代碼得知,如果當前傳進來的節點的上一個節點,是等於head,那麼又會調用tryAcquire方法,這裏體現的就是自旋獲取鎖,爲什麼要這麼做呢? 是爲了避免進入阻塞的狀態,假設線程一已經獲取到鎖了,然後線程二需要進入阻塞,但是由於線程二還在進入阻塞狀態的路上,線程一就已經釋放鎖了。爲了避免這種情況,第一個排隊的線程,有必要在阻塞之前再次去嘗試獲取鎖。
假設一:假設我們線程二在進入阻塞狀態之前,嘗試去獲取鎖,哎,竟然成功了,則會執行一下代碼:
// 調用方法,代碼在下面
setHead(node);
p.next = null; // help GC
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果拿到鎖了,隊列的內容,當然會發送變化,由圖可見,我們會發現一個問題,隊列的第一個節點,又是一個空節點。
因爲當拿到鎖之後,會把當前節點的內容,指針全部賦值爲null,這也是個小細節喲。
假設二:如果當前節點的上一個節點,不是head,那麼很遺憾,沒有資格去嘗試獲取鎖,那就走下面的代碼。
在進入阻塞之前,會調用shouldParkAfterFailedAcquire方法,這個方法小編先告訴你,由於我們這裏是死循環對吧,這個方法第一次調用會放回false,返回false則不會執行執行後續代碼,再一次進入循環,經過一些列操作,還是沒有資格獲取鎖,或者獲取鎖失敗,則又會來到這裏。當第二次調用shouldParkAfterFailedAcquire方法,會放回ture,這個時候,線程纔會調用parkAndCheckInterrupt方法,將線程進入阻塞狀態,等待鎖釋放,然後被喚醒!!
// 如果不是頭部節點,就要過來等待排隊了
// shouldParkAfterFailedAcquire 這方法會使當前循環再循環一次,相當於自旋一次獲取鎖
if (shouldParkAfterFailedAcquire(p, node) &&
// 隊列阻塞,整個線程就等待被喚醒了
parkAndCheckInterrupt())
interrupted = true;
4.5 parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
// 在這裏被park,等待unpark,如果該線程被unpark,則繼續從這裏執行
LockSupport.park(this);
// 這個是獲取該線程是否被中斷過,這句代碼需要結合lockInterruptibly方法來講,小編就不詳細說了,不然一篇文章講太多了~~~~
return Thread.interrupted();
}
到這裏我們ReentrantLock整個加鎖的過程,就相當於講完啦,但是這纔是最最最簡單的一部分,因爲還有很多場景沒考慮到。
4.6 補充說明:shouldParkAfterFailedAcquire方法
上面說爲什麼這個方法第一次調用返回false,第二次調用返回ture,我們來看源碼吧~~
這個方法主要做了一件事:把當前節點的,上一個節點的waitStatus狀態,改爲 - 1。
當線程進入阻塞之後,自己不會把自己的狀態改爲等待狀態,而是由後一個節點進行修改。 細節、細節、細節
舉個例子:你躺在牀上睡覺,然後睡着了,這個時候,你能告訴別人你睡着了嗎? 當然不行,因爲你已經睡着了,呼嚕聲和打雷一樣,怎麼告訴別人。 只有當後一個人來了,看到你在呼呼大睡,它纔可以告訴別人你在睡覺。
//pred 當前上一個節點,node 當前節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 第一次循環進來:獲取上一個節點的線程狀態,默認爲0
// 第二次循環進來,這個狀態就變成-1了,
int ws = pred.waitStatus;
// 判斷是否等於-1,第一進來是0,並且會吧waitStatus狀態改成-1,代碼在else
// 第二次進來就是-1了,直接返回true,是當前線程進行阻塞
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 判斷是否大於0,waitStatus分幾種狀態,這裏其他幾種狀態的源碼就不一一講了。
// = 1:由於在同步隊列中等待的線程,等待超時或者被中斷,需要從同步隊列中取消等待,該節點進入該狀態不會再變化
// = -1:後續節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後續節點,使後續節點繼續運行
// = -2:節點在等待隊列中,節點線程在Condition上,當其他線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態中獲取
// = -3:表示下一次共享式同步狀態獲取將會無條件地被傳播下去
// = 0 :初始狀態
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 因爲默認0,所以第一次會走到else方法裏面
} else {
// CAS吧waitStatus修改成-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false,外層方法接着循環操作
return false;
}
五、ReentrantLock 解鎖過程源碼分析
5.1 unlock方法
講完加鎖過程,就來解鎖過程吧,說實話,看源碼這種經歷,必須要自己花時間去看,去做筆記,去理解,大腦最好有個整體的思路,這樣纔會印象深刻。
public class ReentrantLockTest {
public static void main(String[] args) {
// 創建公平鎖
ReentrantLock lock = new ReentrantLock(true);
// 加鎖
lock.lock();
hello();
// 解鎖
lock.unlock();
}
public static void hello() {
System.out.println("Say Hello");
}
}
點擊unlock解鎖的方法,會調用到release方法,這個是AQS提供的模板方法,再來看tryRelease方法。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// tryRelease 釋放鎖,如果真正釋放會把當前持有鎖的線程賦值爲空,否則只是計數器-1
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
5.1 tryRelease方法
發現又是個抽象類,我們選擇ReentrantLock類實現的
這裏要注意:
1、當前解鎖的線程,必須是持有鎖的線程
2、state狀態,必須是等於0,纔算是真正的解鎖,否則只是代表重入次數-1.
protected final boolean tryRelease(int releases) {
// 獲取鎖計數器 - 1
int c = getState() - releases;
// 判斷當前線程 是否等於 持有鎖的線程,如果不是則拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 返回標誌
boolean free = false;
// 如果計算器等於0,則代表需要真正釋放鎖,否則是代表重入次數-1
if (c == 0) {
free = true;
// 將持有鎖的線程賦值空
setExclusiveOwnerThread(null);
}
// 重新設置state狀態
setState(c);
return free;
}
執行完tryRelease方法,返回到release,進行if判斷,如果返回false,就直接返回了,否則進行解鎖操作。
public final boolean release(int arg) {
// tryRelease方法返回true,則表示真的需要釋放鎖
if (tryRelease(arg)) {
// 如果是需要真正釋放鎖,先獲取head節點
Node h = head;
// 第一種情況,假設隊列沒有被初始化,這個時候head是爲空的,則不需要進行鎖喚醒
// 第二種情況,隊列被初始化了head不爲空,並且只要有線程在隊列中排隊,waitStatus在被加入隊列之前,會把當前節點的上一個節點的waitStatus改爲-1
// 所以只有滿足h != null && h.waitStatus != 0 這個條件表達式,才能真正代表有線程正在排隊
if (h != null && h.waitStatus != 0)
// 解鎖操作,傳入頭節點信息
unparkSuccessor(h);
return true;
}
return false;
}
5.1 unparkSuccessor方法
這裏的參數傳進來的是head的node節點信息,真正解鎖的線程是head.next節點,然後調用unpark進行解鎖。
private void unparkSuccessor(Node node) {
// 先獲取head節點的狀態,應該是等於-1,原因在shouldParkAfterFailedAcquire方法中有體現
int ws = node.waitStatus;
// 由於-1會小於0,所以重新改爲0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取第一個正常排隊的隊列
Node s = node.next;
// 這裏涉及到其他場景,小編就不詳細講了,正常的解鎖不會執行這裏
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 正常來說第一個排隊的節點不應該爲空,所以直接把第一個排隊的線程喚醒
if (s != null)
LockSupport.unpark(s.thread);
}
如果這裏調用unpark,線程被喚醒,會接着這個方法接着執行。到這裏整個解鎖的過程小編就講完了。
private final boolean parkAndCheckInterrupt() {
// 在這裏被park,等待unpark,如果該線程被unpark,則繼續從這裏執行
LockSupport.park(this);
// 這個是獲取該線程是否被中斷過,這句代碼需要結合lockInterruptibly方法來講,小編就不詳細說了,不然一篇文章講太多了~~~~
return Thread.interrupted();
}
六、最後難點:補充說明:hasQueuedPredecessors方法
看到這裏,小編希望是小夥伴真的理解了ReentrantLock加鎖和解鎖的過程,並且在心裏有整體流程,不然你看這個方法,會很蒙,這個方法雖然代碼幾行,但是要完全理解,比較困難。
這個方法估計是ReentrantLock加鎖過程中,最爲複雜的一個方法了,所以放到了最後來講~~~
// 不要小看以下幾行代碼,涉及的場景比較複雜
public final boolean hasQueuedPredecessors() {
// 分別把尾節點、頭節點賦值給 t、h
Node t = tail;
Node h = head;
Node s;
// AQS隊列如果沒有發生競爭,剛開始都是未初始化的,所以一開始tail、head都是爲null
// 第一種情況: AQS隊列沒有初始化的情況
// 假設線程一,第一個進來,這個時候t、h都是爲null,所以在h != t,這個判斷返回false,由於用的是&&所以整個判斷返回fase
// 返回flase表示不需要排隊。
// 但是也不排除可能會有兩個線程同時進來判斷的情況,假設兩個線程發現自己都不需要排隊,就跑去CAS進行修改計數器,這個時候肯定會有一個失敗的
// CAS 是可以保證原子性操作的,假設線程一它CAS成功了,那麼線程二就會去初始化隊列,老老實實排隊去了
// 第二種情況: AQS隊列被初始化了
// 場景一:隊列元素大於1個點情況,假設有一個線程在排隊,在隊列中應該有而個元素,一個是頭節點、線程2
// 現在線程2之前的線程已經執行完,並且釋放鎖喚醒線程2.線程2又會繼續醒來循環。並且線程二是第一個排隊的,所以有資格獲取鎖
// 只要是獲取鎖就會來排隊需不需要排隊,代碼又回到這裏
// 現在 h = 等於頭節點,而 tail = 線程2的node節點,所以 h != t 結果爲true
// h表示頭節點,並且h.next是線程2的節點,所以 (s = h.next) == null 返回 flase
// s 等於 h.next,也就是線程2的節點信息,並且當前執行的線程也是線程2,所以 s.thread != Thread.currentThread(),返回false
// 最後return的結果是 true && false,結果爲false,代表不需要排隊
// 場景二:隊列元素等於1個,什麼情況下隊列被初始化了並且只有一個元素呢?
// 當有線程競爭初始化隊列,之後隊列又全部都被消費完了。最後剩下一個爲空的node,並且head和tail都指向它
// 這個時候有新的線程進來,其實h != t,直接返回false,因爲head 和tail都指向最後一個節點了
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
小編按照自己的思路,儘自己有限的能力,表示了出來。
至於要理解,真的需要小夥伴仔細去思考,不斷的理解,纔會形成自己的思路, 奧利給~~~~~
這裏整個ReentrantLock源碼加鎖解鎖的流程圖,小編也沒貼出來,靠小夥伴們去完成吧~~~~~
七、程序人生,你怎麼看?
這篇文章說了這麼多,看了這麼多源碼,但實際上纔是ReentrantLock很基本的東西。
這就引起一個思考,一個小小的ReentrantLock想要完完全全的去精通每行代碼,需要我們花大量的時間、精力去研究,去探討。 更何況作爲一名程序員,所需要掌握的技術,你們懂得,好像看不到盡頭。 真的就是,你知道的越多,就會發現你不知道得就越多。
對於正在走向程序員道路上的你們,或者已經碼代碼幾年的程序員們,你們是選擇繼續堅持,和代碼死磕到底,一直到年齡的瓶頸、還是中途就選擇轉行呢? 可以文章尾部評論喲。
點贊是你們給博主最大的動力喲 👇👇👇👇👇👇