ReentrantLock 源碼解讀

ReentrantLock 源碼解讀

傻瓜源碼-內容簡介

傻瓜源碼-內容簡介
🤪【職場經驗】(持續更新)
精編短文:如何成爲值錢的Java開發-指南

如何平常學習、如何書寫簡歷、引導面試官、系統準備面試、選擇offer、提升績效、晉升TeamLeader.....
🧐【源碼解讀】(持續更新) <br/>1. 源碼選材:Java架構師必須掌握的全部框架和類庫源碼<br/>2. 內容大綱:按照「企業應用Demo」講解執行源碼:總綱「閱讀指南」、第一章「源碼基礎」、第二章「相關Java基礎」、第三章「白話講源碼」、第四章「代碼解讀」、第五章「設計模式」、第六章「附錄-面試習題、相關JDK方法、中文註釋可運行源碼項目」
3. 讀後問題:粉絲羣答疑解惑
已收錄:HashMapReentrantLockThreadPoolExecutor《Spring源碼解讀》《Dubbo源碼解讀》.....
🤩【面試題集】(持續更新)<br/>1. 面試題選材:Java面試常問的全部面試題和必會知識點<br/>2. 內容大綱:第一部分」注意事項「、第二部分「面試題解讀」(包括:」面試題「、」答案「、」答案詳解「、「實際開發解說」)
3. 深度/廣度:面試題集中的答案和答案詳解,都是對齊通常面試要求的深度和廣度
4. 讀後問題:粉絲羣答疑解惑
已收錄:Java基礎面試題集Java併發面試題集JVM面試題集數據庫(Mysql)面試題集緩存(Redis)面試題集 .....
🤤【粉絲羣】(持續更新) <br/>收錄:阿里、字節跳動、京東、小米、美團、嗶哩嗶哩等大廠內推
😛 做者介紹:Spring系源碼貢獻者、世界五百強互聯網公司、TeamLeader、Github開源產品做者
😛 做者微信:wowangle03 (企業內推聯繫我)

  加入個人粉絲社羣,閱讀更多內容。從學習到面試,從面試到工做,從 coder 到 TeamLeader,天天給你答疑解惑,還能有第二份收入!html

圖片

第 1 章 閱讀指南

  • 本文基於 open-jdk 1.8 版本。
  • 本文根據「 Demo 」解讀源碼。
  • 本文建議分爲兩個學習階段,掌握了第一階段,再進行第二階段;java

    • 第一階段,理解章節「源碼解讀」前的全部內容。即掌握 IT 技能:熟悉 ReentrantLock 原理。
    • 第二階段,理解章節「源碼解讀」(包括源碼解讀)以後的內容。即掌握 IT 技能:精讀 ReentrantLock 源碼。
  • 建議按照本文內容順序閱讀(內容先後順序存在依賴關係)。
  • 閱讀過程當中,若是遇到問題,記下來,後面不遠的地方確定有解答。
  • 閱讀章節「源碼解讀」時,建議得到中文註釋源碼項目配合本文,Debug 進行閱讀學習。
  • 源碼項目中的註釋含義:node

    • 」 Demo 「在源碼中,會標註「 // ReentrantLock Demo 」。

第 2 章 簡介

  ReentrantLock 是 java.util.concurrent 併發包中的可重入鎖(可重入鎖就是指持有鎖的線程能夠重複進入有該鎖的代碼塊);基於 AQS(AbstractQueuedSynchronized)實現。須要手動釋放鎖,可是支持更多方法,好比:上公平/非公平鎖、可被中斷鎖、可被中判定時鎖等。面試

第 3 章 Demo

3.1 代碼示例

@Test
    public void testReentrantLock1() {
        // 多個線程使用同一個 ReentrantLock 對象,上同一把鎖,默認上非公平鎖
        Lock lockNoFair = new ReentrantLock();
        try {
            // 本線程嘗試獲取鎖;若是鎖已經被其它線程持有,則會進入阻塞狀態,直到獲取到鎖
            lockNoFair.lock();
            System.out.println("處理中...");
        } finally {
            // 釋放鎖
            lockNoFair.unlock();
        }
    }

    @Test
    public void testReentrantLock2() {
        // 多個線程使用同一個 ReentrantLock 對象,上同一把鎖,構造函數傳 true,上公平鎖
        Lock lockFair = new ReentrantLock(true);
        try {
            // 本線程嘗試獲取鎖;若是鎖已經被其它線程持有,則會進入阻塞狀態,直到獲取到鎖
            lockFair.lock();
            System.out.println("處理中...");
        } finally {
            // 釋放鎖
            lockFair.unlock();
        }
    }

3.2 方法介紹

  1. lock()(公平鎖)sql

  線程獲取鎖的順序徹底基於調用 lock() 方法的前後順序。數據庫

時間線 線程 1 線程 2 線程 3
1 線程 1 調用 lockFair.lock() 方法,獲取到鎖
2 線程 2 調用 lockFair.lock() 方法後,沒有獲取到鎖,將線程 2 順序放入到鏈表裏排隊,進入阻塞狀態
3 線程 1 調用 lockFair.unLock() 方法,釋放鎖,喚醒線程 2
4 線程 3 調用 lockFair.lock() 方法,發現線程 2 已經在鏈表裏等待得到鎖;線程 3 就追加到線程 2 以後,進行排隊
5 線程 2 被線程 1 喚醒,從新嘗試獲取鎖,獲取鎖成功

  2. lock()(非公平鎖)設計模式

時間線 線程 1 線程 2 線程 3
1 調用 lockNoFair.lock() 方法,獲取到鎖
2 調用 lockNoFair.lock() 方法後,沒有獲取到鎖,線程 2 順序追加到鏈表後排隊,進入阻塞狀態
3 調用 lockNoFair.unLock() 方法,釋放鎖,喚醒線程 2
4 調用 lockNoFair.lock() 方法,成功獲取到鎖
5 線程 2 被線程 1 喚醒,呆在鏈表裏位置不動,從新嘗試獲取鎖,獲取鎖失敗,已經被線程 3 搶佔到了鎖,再次進入阻塞狀態

  3. lockInterruptibly()(可被中斷鎖)緩存

  lockInterruptibly() 也分爲公平鎖和非公平鎖,與 lock() 方法的區別就在於:當線程調用 lockInterruptibly() 方法沒有獲取到鎖,進入阻塞後;若是其它線程對該線程標記爲中斷狀態, lockInterruptibly() 方法則會從阻塞中喚醒,拋出中斷異常。安全

  若是線程一開始就被標記爲中斷狀態,再調用 lockInterruptibly() 方法,lockInterruptibly() 方法則會直接拋出中斷異常。微信

  4. tryLock(long timeout, TimeUnit unit) 方法(可被中判定時鎖)

  tryLock(long timeout, TimeUnit unit) 也區分公平鎖和非公平鎖;與 lockInterruptibly() 方法的區別就在於:線程調用 tryLock(long timeout, TimeUnit unit) 方法,獲取不到鎖,進入阻塞後;若是在指定的時間裏,仍然沒有被其它釋放鎖的線程喚醒,則會自動喚醒,直接返回失敗。

第 4 章 相關 Java 基礎

4.1 原子性

  知足如下幾個特色,咱們就說這個操做支持原子性,線程安全:

  1. 原子操做中的全部子操做,要不全成功、要不全失敗;
  2. 線程執行原子操做過程當中,不會受到其它線程的任何影響;
  3. 其它線程只能感知到原子操做開始前和結束後的變化。

解釋:

  包含多個操做單元,但仍支持原子性,一般都是由鎖實現的。

代碼示例:

class Test {
    int x = 0;
    int y = 0;

    public void test() {
        // 原子操做
        x = 10; 

        // 大體分爲兩步:1)獲取 x 的值到緩存裏;2)取出緩存裏的值,賦值給 y
        // 不支持原子性;獲取 x 的值到緩存裏以後,其它線程可能修改 x 的值,致使 y 值錯誤
        y = x; 

        // 大體分爲三步:1)獲取 x 的值到緩存裏;2)取出緩存裏的值加一;3)賦值給 x
        // 不支持原子性;原理相似 y = x;
        x++; 

    }

}

4.2 Cas

  Cas 是 Compare-and-swap(比較並替換)的縮寫,是支持原子性的操做;在 Java 中,底層是 native 方法實現,經過 CPU 提供的 lock 信號保證的原子性。

  想要將數據 V 的原值 O 替換爲新值 N,執行 Cas 操做,會有如下操做:

  1. 預先讀取數據 V 的值 O 做爲預期值;
  2. 執行 Cas 操做:

    1. 比較當前數據 V 的值是不是 O;

      1. 若是是,則替換爲 N,返回執行成功;
      2. 若是不是,則不替換,返回執行失敗;

例子:

  以 java.util.concurrent.atomic 包中的 AtomicInteger 爲例;

public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(100);
        // 將 AtomicInteger 的值,從 100 替換爲 200
        Boolean b = atomicInteger.compareAndSet(100, 200);
        // 返回 true,替換成功
        System.out.println(b);
    }

4.3 volatile

  用於修飾變量,能夠保證被修飾變量的操做支持可見性和有序性,但不支持原子性。詳見」Java 併發面試題集「

4.4 Thread

  void interrupt():在一個線程中調用另外一個線程的 interrupt() 方法,會將那個線程設置成線程中斷狀態,而不會真的中斷線程。

  boolean isInterrupted(boolean ClearInterrupted):返回當前線程是否處於中斷狀態,ClearInterrupted 爲 true,則返回的同時清除中斷狀態,反之則保留中斷狀態。

interrupt() 代碼示例

class ThreadTest {
    public static void main(String[] args) throws Exception {

        MyThread thread = new MyThread();
        thread.start();
        
        // 主線程經過調用 thread 對象的 interrupt() 方法,將 thread 線程設置成線程中斷狀態,但不會真的中斷線程。
        thread.interrupt();

        System.out.println("主線程執行結束!");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("test thread!");
    }
}
// 打印結果:
// 主線程執行結束!
// test thread!

第 5 章 源碼基礎

5.1 Sync

  在 ReentrantLock 中,定義了一個內部抽象類 Sync;有兩個實現類,分別爲 FairSync(公平鎖) 和 NonfairSync (非公平鎖),是 ReentrantLock 裏的真正實現鎖邏輯的類。

代碼示例 1 ReentrantLock 成員變量

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {

    // state 表示鎖的狀態。int 類型未指定值,默認初始化爲0;
    // 爲 0 則表明當前沒有線程持有鎖;爲 1 則表明有線程持有鎖;若是大於1,則表明鎖被當前線程重入的次數
    // 繼承自 AbstractQueuedSynchronizer
    private volatile int state;
    
    // exclusiveOwnerThread 表示當前持有鎖的線程對象
    // 繼承自 AbstractOwnableSynchronizer
    private transient Thread exclusiveOwnerThread;

    // tail 記錄尾線程節點對象,默認爲 null
    // 繼承自 AbstractQueuedSynchronizer
    private transient volatile Node tail;

    // head 記錄頭線程節點對象,默認爲 null
    // 繼承自 AbstractQueuedSynchronizer
    private transient volatile Node head;
    
}

5.2 ReentrantLock 中的鏈

  ReentrantLock 使用 FIFO (先進先出)隊列來管理競爭鎖的線程關係。

5.2.1 Node

  在 ReentrantLock 中, Sync 經過繼承了 AQS 抽象類(AbstractQueuedSynchronizer),進而繼承 AbstractQueuedSynchronizer 中的內部類 Node,用來封裝線程,同時也是鏈表的組成元素。

代碼示例 Node 重要成員變量

static final class Node {

    // prev 指向前一個節點(表明前一個進入鏈表的線程)
    volatile Node prev; 

    // next 指向後一個節點(表明後一個進入鏈表的線程)
    volatile Node next; 

    // thread 當前節點表示的線程    
    volatile Thread thread; 

    // 0:表示[初始默認值]或者[表示已解鎖]
    // -1(SIGNAL):表示當前節點表明的線程在釋放鎖後須要喚醒下一個節點的線程
    // 1(CANCELLED):表示當前節點表明的線程在隊列中發生異常(發生中斷異常或者其它不可預知的異常),標記爲取消狀態
    volatile int waitStatus;

5.2.2 隊列生命週期

  1. 初始狀態:head 和 tail 變量爲空,不存在鏈表。

    1. 第一個線程 0 獲取到鎖( state 成功由 0 修改成 1),不須要鏈表。
  2. 初始化不表明線程的頭節點,再在頭節點後插入線程 1 節點(線程 1 獲取鎖失敗,放入鏈表,等待獲取鎖):

    1. 首先會先初始化一個不表明任何線程的 Node 節點 node;
    2. 而後將 tail 變量和 head 變量都指向這個 node,做爲頭節點和尾節點;
    3. 而後再爲線程 1 新建一個 Node 節點對象 node1,追加到頭節點後面:

      1. 將 node1 的 prev 屬性指向鏈表的尾節點;
      2. 將鏈表的尾節點的 next 屬性指向爲 node1 節點;
      3. 最後,將 tail 變量指向了 node1 節點。
  3. 線程 1 節點替換成頭節點(線程 1 在鏈表中被線程 0 喚醒,成功獲取到鎖):

    1. 線程 0 釋放鎖後,喚醒頭節點後的線程 1 節點;
    2. 當線程 1 成功獲取到鎖( state 成功由 0 修改成 1)後,會把 head 變量指向爲當前節點;
    3. 由於當前節點表明的線程已經獲取到鎖,因此當前節點再也不須要表明線程,就會把 thread 屬性修改成空,prev 屬性修改成空;
    4. 將老頭節點的 next 屬性置爲空(加快 GC)。

<br/>

加入個人粉絲社羣,閱讀所有內容

  從學習到面試,從面試到工做,從 coder 到 TeamLeader,天天給你答疑解惑,還能有第二份收入,這樣的知識星球,難道你還要猶豫!

圖片

相關文章
相關標籤/搜索