【併發編程】 --- Reentrantlock源碼解析4:公平鎖加鎖過程中 [判斷當前線程是否要排隊的具體細節] 超詳細解析

源碼地址:https://github.com/nieandsun/concurrent-study.git


1 簡單回顧

【併發編程】 — Reentrantlock源碼解析1:同步方法交替執行的處理邏輯
【併發編程】 — Reentrantlock源碼解析2:公平鎖加鎖過程超詳細解析
【併發編程】 — Reentrantlock源碼解析3:公平鎖釋放鎖過程超詳細解析

上面三篇文章我自認爲 已經非常詳細的介紹了Reentrantlock公平鎖加鎖和解鎖的過程 —》 其實非公平鎖比公平鎖更簡單,其主要區別就在於在獲取鎖時:

  • 如使用公平鎖,當前線程即使發現鎖沒有被佔用,也不能直接去搶鎖,它要先去看一看有沒有線程在排着隊等着獲取鎖
  • 若使用非公平鎖 ,當前線程若發現鎖沒被佔用,它就會立刻去獲取鎖

有興趣的可以自己擼一下非公平鎖的源碼。

本篇文章將主要來講解一下,在使用公平鎖時,當前線程在發現鎖沒有被佔用的情況下,在高併發環境中判斷自己是否需要排隊的具體實現細節。

在看Doug Lea大神對這塊邏輯的具體實現之前,我們不妨先從宏觀角度去想一想,在鎖沒被佔用的情況下,如果是你,你會怎樣判斷當前線程是否需要排隊??? 我覺得仔細想一下的話,其實每個人都應該可以想明白,肯定就只有下面三種情況:

  • (1)根本就還沒有隊伍時 — 即Node鏈表爲空時肯定不用排隊
  • (2)有隊伍但隊伍中只有一個節點時肯定不用排隊 —> 因爲Node鏈表中第一個節點肯定是線程爲null的節點 —> 所以此時肯定沒有線程在排隊 —> 當然當前線程也就不用排隊了
  • (3)持有鎖的線程剛剛釋放鎖,第二個節點自旋嘗試獲取鎖時,肯定也不用排隊,因爲對於公平鎖來說它將肯定是最先獲取鎖的線程

想明白了這些之後,你再看Doug Lea大神的代碼,你就會發現 —》 其實根本不難!!!


2 源碼 + 整體邏輯流程梳理

上篇文章《【併發編程】 — Reentrantlock源碼解析3:公平鎖釋放鎖過程超詳細解析》其實已經介紹過,Doug Lea大神對這塊邏輯的具體實現代碼其實就寥寥數行,源碼如下:

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;
    //注意1:下面的邏輯並不是原子操作,這一點我想大家肯定不會有任何疑問
    //注意2:當下面的表達返回false時表示當前線程不用排隊
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

這段代碼的具體執行流程如下。 通過該圖我們可以捋出有且僅有的兩條不用排隊的線路:

  • h != t 不成立時
  • h != t 成立,但是s = h.next) == null 和 s.thread != Thread.currentThread()兩個條件都不成立時

在這裏插入圖片描述


3 高併發環境下當前線程使用公平鎖時判斷自己是否要排隊的具體實現細節

3.1 h != t


3.1.1 h != t 不成立時(即h等於t時) —> 不用排隊的原因

所謂h != t 不成立時不用排隊,也就是說,當 h == t 時,不用排隊!!! 而其實 h == t 對應瞭如下兩種情況。


3.1.1.1 情況1 —> h和t都等於null —> 不用排隊

這很容理解,當h和t都等於null,說明Node鏈表都還沒初始化呢,此時肯定沒有排隊要獲得鎖的線程,因此當前線程就肯定不用排隊了。

其實可以很容易的想到:同步方法交替執行時,就是這種情況。這時候有興趣的可以再來看一下我之前寫的文章 《【併發編程】 — Reentrantlock源碼解析1:同步方法交替執行的處理邏輯》 就更可以理解爲什麼Reentrantlock在同步方法交替執行時,效率會很高了:

  • (1) 既沒有park和unpark線程 —》 肯定也就不會調用內核函數
  • (2)甚至連Node鏈表都不用初始化

3.1.1.2 情況2 —> h和t不等於null,但h=t的情況 —> 不用排隊

其實這也很容易理解,h和t既然不等於null,而h和t又相等,那Node鏈表就只有如下一種情況了。也就是說此時雖然Node鏈表已經初始化了,但是還沒有線程入隊到第2 個節點 —> 那就肯定還沒有線程排隊,那當前線程肯定也就不用排隊了!
在這裏插入圖片描述


3.1.1.3 小結 — h = t 爲啥就直接可以說不用排隊了???

其實通過2.2.1和2.2.2已經可以知道,當h = t的時候:

  • 要麼根本就沒有Node鏈表
  • 要麼雖然有Node鏈表,但是還沒有一個線程在Node鏈表裏進行排隊

這裏我覺得一定要聯繫一下我在《【併發編程】 — Reentrantlock源碼解析2:公平鎖加鎖過程超詳細解析》這篇文章裏說的一句話:Reentrantlock公平鎖所謂的公平並不是你先嚐試獲取鎖,你就一定會最先獲取到鎖,而是你最先進入到了Node隊列,你最先獲取到鎖!!!

因此當h=t時,當前線程肯定就不用排隊了!!!


3.1.2 h != t 成立時 —> 是否需要排隊仍未可知原因分析

這裏首先要明確一下,通過前面幾篇文章的知識可知:任何情況下都不可能出現h和t一個爲null,一個不爲null的情況

因此當判定h != t 成立時的那一刻Node鏈表裏肯定至少有一個線程正在排隊了!!!

那當前線程肯定就有可能也要排隊了。

當然在判斷h != t 成立的那一刻,當前線程也有可能不需要排隊 —> 原因請看3.3!!!

因此總得來說h != t這個判斷條件成立時對於當前線程來說,是否需要排隊,還未可知!!!


3.2 (s = h.next) == null 能被運行的情況


3.2.1 (s = h.next) == null 成立時爲什麼需要排隊

能讀懂這個邏輯,必須要有多線程併發的意識!!!

這裏首先說一下爲什麼 (s = h.next) == null有可能會成立??? 原因如下:

在這裏插入圖片描述


接下來說一下爲啥當 (s = h.next) == null成立時就必須要排隊

由上圖來說,該條件如果成立,那肯定說明此前排在第2的線程已經獲得 或者說或得過鎖了!!!

  • 當然如果該線程正在持有鎖,那當前線程肯定就得老老實實去排隊了 — 這一點肯定是毋庸置疑的!!!
  • 但是我想肯定有人會像我一樣去挑刺: 誒,有可能此前排在第2的線程就是這麼牛逼,它不僅獲得到了鎖,而且還咔咔咔咔執行完,釋放了鎖 —> 這時當前線程是不是就不用再去排隊了??? —> 對,非常對!!!但是呢?肯定沒必要再去爲此搞個邏輯啊
    • 首先來說這種情況出現的機率肯定本來就小
    • 其次此時當前線程就肯定排在Node鏈表中第2的位置了 — 那它就極有可能通過2次自旋獲取到鎖了,或許再爲此單獨實現一個邏輯,還不如自旋來的效率高呢!!!

3.2.1 (s = h.next) == null 不成立時 —> 是否需要排隊仍未可知原因分析

最後說一下爲啥當 (s = h.next) == null不成立時無法判斷是否需要排隊

  • 首先該條件不成立,就說明肯定在此刻至少有一個線程正在排隊 —》 因此當前線程也有可能需要排隊
  • 其次 當前線程也有可能不需要排隊 —> 原因請看3.3!!!

因此總得來說 (s = h.next) == null這個判斷條件不成立時對於當前線程來說,是否需要排隊,還未可知!!!



3.3 s.thread != Thread.currentThread() 能被運行的情況


3.3.1 s.thread != Thread.currentThread() 成立時需要排隊的原因

s.thread != Thread.currentThread() 成立其實也可以分爲兩種情況:

  • (1)排在第2的線程還在Node鏈表中,但不是當前線程 —》 那當前線程需要排隊肯定是毋庸置疑的
  • (2)進行該條件判斷時排在第2的線程已經獲得 或者說或得過鎖了!!! —》 這種情況我就不具體分析了,其實可以參考我在3.2.1 中的描述 —》 總而言之這種情況下肯定也是需要排隊的!!!

3.3.2 s.thread != Thread.currentThread() 不成立時不需要排隊的原因 ★★★

我覺得只有看明白了這裏,你纔算真真正正明白了Reentrantlock!!!

s.thread != Thread.currentThread() 不成立 —> 也就是說Node鏈表中排在第2的線程就是當前線程!!!

首先再看一次這整塊邏輯的流程圖:
在這裏插入圖片描述
然後我們仔細想一想,究竟怎樣纔有可能會出現s.thread != Thread.currentThread()可以被執行,但執行結果卻爲false的情況??? —》想破頭皮,你會發現,其實就有且僅有一種情況:

擁有鎖的線程剛剛釋放鎖,排在Node鏈表中第2的線程通過自旋,嘗試獲取鎖!!!

當然自旋的時機有可能會有如下兩種:

  • (1)擁有鎖的線程剛剛釋放鎖,而排在Node鏈表第2的線程剛剛完成了入隊,還處在自旋當中
  • (2)擁有鎖的線程剛剛釋放鎖,並喚醒排在Node鏈表第2的線程 —》 由此引發排在第2的線程嘗試獲取鎖!!!

4 總結

高併發環境下當前線程使用公平鎖時,當發現鎖沒被佔用時,判斷自己不需要排隊的情況有且僅有三種:

  • (1)根本就沒有隊伍 — Node鏈表根本就沒初始化
  • (2)有隊伍,但隊伍中只有一個Node — Node鏈表隊首肯定是一個線程爲null的Node,沒有第2個Node,說明還沒有線程在排隊
  • (3)擁有鎖的線程剛釋放鎖,第二個Node自旋時的情況

其實現在想想也挺簡單的(☆_☆)


5 本篇文章背後故事及啓發

一開始寫這篇文章時,我是按照如下三個條件由上往下逐個分析的,但是文章寫到最後一個條件時,發現自己的理解有些竟然是錯誤的!!!

  • (1)h != t
  • (2) (s = h.next) == null
  • (3) s.thread != Thread.currentThread()‘

很傷心!!!

究其原因,就是因爲我一開始就鑽進了細枝末節裏,而沒有從宏觀的角度去想一下,上面三個條件都被執行的情況下到底是個什麼情況!!! — 我當時竟然以爲與重入鎖有關,現在想想,真是可笑!!!

—》 由此得到如下啓發:

分析問題,一定要先從宏觀上認清這個東西究竟是要幹什麼!!!只有搞明白了其具體的方向,纔不會在細枝末節裏迷失自己!!!


end !!!

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