ReentrantLock 公平鎖源碼 第1篇

来源:https://www.cnblogs.com/sunankang/archive/2022/07/08/16458795.html
-Advertisement-
Play Games

ReentrantLock 1 這篇還是接著ReentrantLock的公平鎖,沒看過第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342.html 這篇就以問題為導向,先提出問題,然後根據問題去看代碼 確保能喚醒排隊的線程? A,B兩線程 ...


ReentrantLock 1

這篇還是接著ReentrantLock的公平鎖,沒看過第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342.html

這篇就以問題為導向,先提出問題,然後根據問題去看代碼

確保能喚醒排隊的線程?

A,B兩線程,A線程執行完業務釋放鎖過程中B線程添加進了鏈表,如何保證B線程能正常醒來

現在假設A線程走完tryAcuqire後獲取到鎖,執行業務代碼,最後unlock() tryAcquire代碼就不進去看了,上篇講過了 現在只需關註兩個點

lock方法中的acquireQueued 用來park

unlock方法中的release用來unpark

首先來看park的條件是啥

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

進入acquireQueued方法

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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) //在這裡進行的park
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

也就是shouldParkAfterFailedAcquire 如果這個方法返回true,才會去park

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

現在假設第一種情況,首次進入這個shouldParkAfterFailedAcquire方法的時候,A線程就進入unlock方法了 那麼此時節點狀態如下圖

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        //主要看這段代碼
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

那麼h!=null進入,但是頭節點的waitStatus還是0,所以不走unpark,A線程結束

A線程結束了誰來喚醒B線程呢? 回到acquireQueued方法

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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

因為第一次進入shouldParkAfterFailedAcquire方法中,最後走到else代碼塊,我們假設沒有發生衝突,修改成功

A線程執行完了unlock,而此時鎖的狀態值為0,沒有被持有的狀態,最外層的for(;;)讓代碼又重新跑了一遍

第二次的時候if (p == head && tryAcquire(arg)) 這個if就會進入,因為現在已經沒有其他線程在持有鎖了,所以tryAcquire嘗試獲取鎖成功,返回ture

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

在setHead方法中將當前節點,咱們這個例子中也就是B節點,設置為head,之後清空上個引用和當前引用的線程

最後清除上個節點對B節點的引用,此時節點關係如下

而原來的頭節點沒有任何引用,等待GC即可,也可以看到在代碼p.next = null; // help GC 這段旁邊寫的註釋 幫助GC

之後將失敗狀態設置為false,返回是否被打斷的變數,lock方法結束,

現在來假設在shouldParkAfterFailedAcquire方法中修改成功,但此時的A線程還沒有走到unlock,當B線程馬上要開始走parkAndCheckInterrupt方法開始park的時候,時間片用完的情況

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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                //====假設此時B線程在這裡=====
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

此時節點關係如下

A線程的unlock就可以進入unparkSuccessor

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    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);
}

第一個if判斷為true,嘗試修改狀態為0 (這裡沒看懂為什麼是嘗試修改)

if (s == null || s.waitStatus > 0) 這個判斷我們是不進入的,註意unparkSuccessor這個方法的node參數是head節點,而不是我們的B節點,所以繼續執行下麵的if判斷

s就是B節點,在B線程park前喚醒,B線程再走到park的時候是不會再進行park的,直接返回,方法結束

真的公平嗎?

A線程在運行,B線程初始化鏈表中的過程中,A線程運行完成,釋放鎖,C線程進入

我們只需要看線程B初始化鏈表的情況即可

addWaiterenq方法

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                //假設線程B走到這裡時間片用完,還沒來得及設置tail
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

那麼此時線程A解鎖了,線程C調用lock方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

tryAcquire方法的hasQueuedPredecessors方法中

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

此時tail還是null,而head已經被線程B設置了一個空Node,h!=t為true,h也只是一個空Node,所以(s = h.next) == null為true,整體返回true,外層取反為false,退出tryAcquire方法去入隊列

那麼入隊列會破壞隊列的初始化或者C線程變成第一個排隊的節點嗎?,註意咱們現在假設的線程B還沒有獲取到cpu的調用,還是停在 tail = head;代碼執行前

線程C執行addWaiter方法

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

這個時候tail還是空,進入enq方法

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

首先第一個判斷是會進入的,這個時候tail還是空,但是if (compareAndSetHead(new Node()))方法不會成功,來看看代碼

private final boolean compareAndSetHead(Node update) {
    //註意第三個參數 null
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

判斷的是head為null的時候才會進行修改,所以線程C沒有修改成功,那麼會一直在for(;;)中迴圈,直到線程B初始化完空的頭節點,也就是執行tail = head;這段代碼

如果線程B走完了 tail = head;沒來得及進行第二次迴圈添加B節點的時候,線程A解鎖了,線程C進來了呢

還是在tryAcquire方法的hasQueuedPredecessors

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

這個時候第一個h!=t就是false,因為B線程已經將head和tail的引用指向同一個空節點了,返回false

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //因為返回false,取反則進行獲取鎖的操作
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

C線程直接獲取鎖去運行代碼了,所以ReentrantLock的公平鎖其實並不是絕對的公平


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 日期時間的處理,是軟體開發中極其常見的場景,JAVA中與日期、時間相關的一些類與API方法也很多,這裡結合平時的編碼實踐全面的整理了下,希望可以幫助大家釐清其中的門道,更加游刃有餘的面對此方面的處理~ ...
  • 1.許可權管理-用戶管理-高級搜索-手機號搜索不可用 1.1現象 1.2解決思路 1.2.1 定位介面 介面名:system/user/list 請求方式:GET請求 1.2.3 確定bug所在位置 bug定位:在執行查詢的sql處,沒有添加手機號搜索的條件 此處沒有根據phone進行搜索 1.2.4 ...
  • python可視化案例,包含:條形圖、環形圖、折線圖、堆疊柱形圖、詞雲圖等。 ...
  • 由於網上搜索 PowerJob MapReduce 都是設計原理,demo也展示個空殼子,沒有演示Map到Reduce結果怎麼傳遞,對於沒有MR開發經驗的人來說並沒有什麼幫助,所以這裡寫了一個有完整計算意義的demo供參考。 代碼功能: 實現一個sum累加。 任務輸入參數: batchSize=10 ...
  • 因為webman是常駐記憶體框架 當前進程初始化一次後就不會再初始化了 所以構造函數里傳遞request是不好用的。 這裡使用中間件來代替 瞭解中間件: 中間件一般用於攔截請求或者響應。例如執行控制器前統一驗證用戶身份,如用戶未登錄時跳轉到登錄頁面。例如響應中增加某個header頭。例如統計某個uri ...
  • 數組 java數組是一個容器,保存著一組值,當數組創建之後,數組的的長度就固定了。 1.數組的定義 1.聲明數組 int array=null; 聲明瞭數組之後,數組是空的,沒什麼實際意義 2.創建數組 ​ array=new[10]; 3.給元素中數組賦值 array[0]=0; 註:數組的下標是 ...
  • lab1 要求按照論文實現一個mapReduce 框架 lab1 :https://pdos.csail.mit.edu/6.824/labs/lab-mr.html 論文:https://zhuanlan.zhihu.com/p/122571315 在mrsequential.go文件中有個單機版 ...
  • 作為一項古老的智力游戲,千百年來迷宮都散髮著迷人的魅力。但是,手工設計迷宮費時又耗(腦)力,於是,我們有必要製作一個程式:迷宮生成器…… 好吧,我編不下去了。但是,從上面的文字中,我們可以看出,我們此次的主題是:用Python實現一個迷宮生成器。 首先展示一下效果圖: 我們先分析一下所需的庫: 既然 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...