ReentrantLock獲取鎖釋放鎖的流程示例分析

目的

  • 瞭解ReentrantLock獲取鎖、釋放鎖的流程

代碼

package com.company.aqs;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * ReentrantLock使用案例——使用ReentrantLock加鎖
 * @Author: Alan
 * @Date: 2022/11/20 01:38
 */
public class ReentrantLockDemo {
    private static int sum=0;
    private static Lock lock=new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                // 獲取鎖
                lock.lock();
                try {
                    for (int j = 0; j < 1000; j++) {
                        sum++;
                    }
                }finally {
                    // 在finally代碼塊中釋放鎖
                    lock.unlock();
                }
            }).start();
        }
        // 保證所有線程執行完畢
        Thread.sleep(1000);
        System.out.println(sum);
    }
}

這是一個使用ReentrantLock實現多線程求和的案例。代碼邏輯比較簡單,外層循環開啟瞭3個線程,然後每個線程內多sum累加1000,最後輸出結果sum=1000。

獲取鎖流程

整個過程概括起來就做瞭兩件事兒

  • 獲取鎖成功,執行當前線程內的其他事情;
  • 獲取鎖失敗,當前線程加入同步隊列,同時阻塞當前線程。

當第一個線程(thead0)進來的時候,通過CAS去修改state屬性為1,如果成功,通過setExclusiveOwnerThread()方法設置exclusiveOwnerThread為當前線程

此時,第二個線程(thead1)進來,再去通過CAS修改state屬性為1時,便會失敗,此時進入acquire()方法。最終會走到如下方法,首先通過tryAcquire()方法再次嘗試去獲取鎖。

tryAcquire()方法內部還是會通過CAS去獲取鎖。此時鎖資源還被第二線程持有,因此會返回false。現在接著看acquire()方法中的if判斷。

此時,會進行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))判斷。這裡需要執行兩個方法addWaiter()和acquireQueued()。首先看addWaiter()方法。這個方法,我們需要關註以下四點。

  • 首先會先構造一個node節點(節點內部細節,可以看其構造方法)。
  • 如果tail(同步隊列尾節點指針)不為空,其實也就是同步隊列不為空,那麼就把第1步構建的節點通過尾插法加入隊列中,然後返回

如果同步隊列為空瞭,那麼執行enq()方法,這個方法為我們做瞭兩件事兒。

  • 如果同步隊列為空,那麼初始化隊列
  • 隊列初始化完成後,將node節點入隊。

通過addWaiter()方法和enq()方法,我們也可以看出來,AQS中的同步隊列是通過雙向鏈表來實現的,節點入隊和出隊,需要修改兩個指針才行(prev和next)。

addWaiter()方法執行執行完畢後,我們通過下面這張圖大致看下此時同步隊列中的節點指向情況。此處,不太理解可以再回頭看看enq()方法的執行流程。

addWaiter()方法執行執行完畢後,會返回入隊後的新節點。然後開始執行acquireQueued()方法。這個方法做瞭五件事兒。

  • 獲取當前節點的前驅節點p
  • 如果p是頭節點,那麼再次嘗試去獲取鎖,獲取鎖成功,就可以跳出循環
  • 獲取鎖失敗,通過shouldParkAfterFailedAcquire()去修改waitSatus為-1(為什麼修改為-1,這裡可以從AQS的源碼中找到原因,後續獲取鎖的流程也會遵從這個邏輯)

4. parkAndCheckInterrupt()方法會阻塞當前線程,同時,能夠返回當前線程的中斷狀態(Thread.interrupted()會清除中斷標記位)。

經過上述的一通操作,我們可以知道,線程1沒有獲取到鎖,被加入到瞭同步隊列,並且還對其進行瞭阻塞。概括下就是四個字“入隊”、“阻塞”。此時我們的同步隊列也變成如下所示。和上述執行完addWaiter()方法相比,隻是線程1節點的前驅節點,waitStaus被設置成瞭-1。

釋放鎖流程

整個過程(隻考慮釋放鎖成功)概括起來做瞭三件事兒

  • 釋放鎖資源;
  • 喚醒同步隊列中頭節點的後一個節點對應的線程
  • 步驟2被喚醒的該線程嘗試競爭鎖,競爭鎖成功,那麼更新同步隊列(即頭節點出隊)。

當第一個線程(thread0)執行完自己的業務流程後,就會釋放鎖。此時,我們來看看釋放鎖的流程又是什麼樣子的。調用unlock()方法可以對鎖進行釋放,需要註意的時,為瞭避免死鎖,需要將該方法的調用放在fiaally代碼塊中。

當thread0去釋放鎖時,會調用release()方法,該方法主要做瞭兩件事兒。

  • 調用tryRelease()方法釋放鎖

其方法內部,會將state設置為0,同時通過setExclusiveOwnerThread()方法設置exclusiveOwnerThread為當null,代表此時鎖資源被釋放,沒有任何線程持有鎖資源

如果第一步返回true,即釋放鎖成功,那麼開始第二步。第二步首先獲取同步隊列的頭節點,查看其waitStatus屬性。這裡我們把剛才獲取鎖過後,同步隊列中的節點情況再放一下。此時,我們的head節點的waitStatus為-1,因此會進入unparkSuccessor()方法

unparkSuccessor()方法,主要做瞭以下三件事兒。註意第三步,根據我們當前同步隊列的情況來看,LockSupport.unpark()方法會喚醒線程1。

當執行完unparkSuccessor()方法中的LockSupport.unpark()方法後,線程1(thread1)就會被喚醒瞭。線程1被喚醒過後,我們又來看看線程1的執行情況。此時線程1,會繼續執行acquireQueued()方法中的for循環(註意:這是一個死循環哦)。執行順序和獲取鎖時是一致的,和獲取鎖有所不同的時,此時執行第2步獲取鎖是會成功的(因為thread0已經釋放鎖瞭)。

獲取鎖成功後呢,會讓當前同步隊列中的頭節點出隊,此時,同步隊列中的節點情況如下所示。

此時,釋放鎖的邏輯就執行完成瞭。歸納起來其實也很簡單,首先釋放鎖資源,然後再喚醒同步隊列中頭節點的後一個節點對應的線程,最後,更新同步隊列(出隊)。

總結

以上便是ReentrantLock獲取鎖、釋放鎖的的大致流程。通過這篇文章,讀者對ReentrantLock的獲取鎖、釋放鎖過程有一個大致的瞭解瞭,細心的讀者可能會發現,獲取鎖時acquireQueued()方法中有一個隊cancelAcquire()方法的調用邏輯,這裡沒有詳細解釋,博主會在後面的文章中詳細來解釋這個方法的處理邏輯(flag先立下!!!)。

以上就是ReentrantLock獲取鎖釋放鎖的流程示例分析的詳細內容,更多關於ReentrantLock獲取鎖釋放鎖的資料請關註WalkonNet其它相關文章!

推薦閱讀: