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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...