深入淺出AQS之共用鎖模式

来源:http://www.cnblogs.com/lfls/archive/2017/09/27/7599863.html
-Advertisement-
Play Games

在瞭解了 "AQS獨占鎖模式" 以後,接下來再來看看共用鎖的實現原理。 原文地址:http://www.jianshu.com/p/1161d33fc1d0 搞清楚AQS獨占鎖的實現原理之後,再看共用鎖的實現原理就會輕鬆很多。兩種鎖模式之間很多通用的地方本文只會簡單說明一下,就不在贅述了,具體細節可 ...


在瞭解了AQS獨占鎖模式以後,接下來再來看看共用鎖的實現原理。

原文地址:http://www.jianshu.com/p/1161d33fc1d0

搞清楚AQS獨占鎖的實現原理之後,再看共用鎖的實現原理就會輕鬆很多。兩種鎖模式之間很多通用的地方本文只會簡單說明一下,就不在贅述了,具體細節可以參考我的上篇文章深入淺出AQS之獨占鎖模式

一、執行過程概述

獲取鎖的過程:

  1. 當線程調用acquireShared()申請獲取鎖資源時,如果成功,則進入臨界區。
  2. 當獲取鎖失敗時,則創建一個共用類型的節點併進入一個FIFO等待隊列,然後被掛起等待喚醒。
  3. 當隊列中的等待線程被喚醒以後就重新嘗試獲取鎖資源,如果成功則喚醒後面還在等待的共用節點並把該喚醒事件傳遞下去,即會依次喚醒在該節點後面的所有共用節點,然後進入臨界區,否則繼續掛起等待。

釋放鎖過程:

  1. 當線程調用releaseShared()進行鎖資源釋放時,如果釋放成功,則喚醒隊列中等待的節點,如果有的話。

二、源碼深入分析

基於上面所說的共用鎖執行流程,我們接下來看下源碼實現邏輯:
首先來看下獲取鎖的方法acquireShared(),如下

   public final void acquireShared(int arg) {
        //嘗試獲取共用鎖,返回值小於0表示獲取失敗
        if (tryAcquireShared(arg) < 0)
            //執行獲取鎖失敗以後的方法
            doAcquireShared(arg);
    }

這裡tryAcquireShared()方法是留給用戶去實現具體的獲取鎖邏輯的。關於該方法的實現有兩點需要特別說明:

一、該方法必須自己檢查當前上下文是否支持獲取共用鎖,如果支持再進行獲取。

二、該方法返回值是個重點。其一、由上面的源碼片段可以看出返回值小於0表示獲取鎖失敗,需要進入等待隊列。其二、如果返回值等於0表示當前線程獲取共用鎖成功,但它後續的線程是無法繼續獲取的,也就是不需要把它後面等待的節點喚醒。最後、如果返回值大於0,表示當前線程獲取共用鎖成功且它後續等待的節點也有可能繼續獲取共用鎖成功,也就是說此時需要把後續節點喚醒讓它們去嘗試獲取共用鎖。

有了上面的約定,我們再來看下doAcquireShared方法的實現:

    //參數不多說,就是傳給acquireShared()的參數
    private void doAcquireShared(int arg) {
        //添加等待節點的方法跟獨占鎖一樣,唯一區別就是節點類型變為了共用型,不再贅述
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //表示前面的節點已經獲取到鎖,自己會嘗試獲取鎖
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //註意上面說的, 等於0表示不用喚醒後繼節點,大於0需要
                    if (r >= 0) {
                        //這裡是重點,獲取到鎖以後的喚醒操作,後面詳細說
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //如果是因為中斷醒來則設置中斷標記位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //掛起邏輯跟獨占鎖一樣,不再贅述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //獲取失敗的取消邏輯跟獨占鎖一樣,不再贅述
            if (failed)
                cancelAcquire(node);
        }
    }

獨占鎖模式獲取成功以後設置頭結點然後返回中斷狀態,結束流程。而共用鎖模式獲取成功以後,調用了setHeadAndPropagate方法,從方法名就可以看出除了設置新的頭結點以外還有一個傳遞動作,一起看下代碼:

    //兩個入參,一個是當前成功獲取共用鎖的節點,一個就是tryAcquireShared方法的返回值,註意上面說的,它可能大於0也可能等於0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //記錄當前頭節點
        //設置新的頭節點,即把當前獲取到鎖的節點設置為頭節點
        //註:這裡是獲取到鎖之後的操作,不需要併發控制
        setHead(node);
        //這裡意思有兩種情況是需要執行喚醒操作
        //1.propagate > 0 表示調用方指明瞭後繼節點需要被喚醒
        //2.頭節點後面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果當前節點的後繼節點是共用類型獲取沒有後繼節點,則進行喚醒
            //這裡可以理解為除非明確指明不需要喚醒(後繼等待節點是獨占類型),否則都要喚醒
            if (s == null || s.isShared())
                //後面詳細說
                doReleaseShared();
        }
    }

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

最終的喚醒操作也很複雜,專門拿出來分析一下:
註:這個喚醒操作在releaseShare()方法里也會調用。

private void doReleaseShared() {
        for (;;) {
            //喚醒操作由頭結點開始,註意這裡的頭節點已經是上面新設置的頭結點了
            //其實就是喚醒上面新獲取到共用鎖的節點的後繼節點
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示後繼節點需要被喚醒
                if (ws == Node.SIGNAL) {
                    //這裡需要控制併發,因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //執行喚醒操作      
                    unparkSuccessor(h);
                }
                //如果後繼節點暫時不需要喚醒,則把當前節點狀態設置為PROPAGATE確保以後可以傳遞下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果頭結點沒有發生變化,表示設置完成,退出迴圈
            //如果頭結點發生變化,比如說其他線程獲取到了鎖,為了使自己的喚醒動作可以傳遞,必須進行重試
            if (h == head)                   
                break;
        }
    }

接下來看下釋放共用鎖的過程:

public final boolean releaseShared(int arg) {
        //嘗試釋放共用鎖
        if (tryReleaseShared(arg)) {
            //喚醒過程,詳情見上面分析
            doReleaseShared();
            return true;
        }
        return false;
    }

註:上面的setHeadAndPropagate()方法表示等待隊列中的線程成功獲取到共用鎖,這時候它需要喚醒它後面的共用節點(如果有),但是當通過releaseShared()方法去釋放一個共用鎖的時候,接下來等待獨占鎖跟共用鎖的線程都可以被喚醒進行嘗試獲取。

三、總結

跟獨占鎖相比,共用鎖的主要特征在於當一個在等待隊列中的共用節點成功獲取到鎖以後(它獲取到的是共用鎖),既然是共用,那它必須要依次喚醒後面所有可以跟它一起共用當前鎖資源的節點,毫無疑問,這些節點必須也是在等待共用鎖(這是大前提,如果等待的是獨占鎖,那前面已經有一個共用節點獲取鎖了,它肯定是獲取不到的)。當共用鎖被釋放的時候,可以用讀寫鎖為例進行思考,當一個讀鎖被釋放,此時不論是讀鎖還是寫鎖都是可以競爭資源的。


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

-Advertisement-
Play Games
更多相關文章
  • 基於上篇文章 "《HiBlogs》重寫筆記[1] 從DbContext到依賴註入再到自動註入" 園友 @Flaming丶淡藍@ 吳瑞祥 提出了討論和質疑,嚇得我連夜查詢資料(玩笑~)。 本來重點是想分析“自動註入”和對“註入”有更深的理解。不過既然有疑問和討論那也是很好的。總比時不時來篇“這個不行” ...
  • 要求:1.列印市、區、街道三級菜單 2.按b可隨時返回上一級 3.按q可隨時退出程式 1 dict={'北京':{'海澱區':['中關村','北太平莊','西三旗'], '昌平區':['回龍觀','霍營','沙河'],'朝陽區':['酒仙橋','望京','將台']}, 2 '上海':{'浦東新區': ...
  • test ...
  • 1.當我們使用IE內核的瀏覽器下在PHPExcel報表時(谷歌、火狐瀏覽器正常, IE瀏覽器,360瀏覽器的相容模式報錯),會出現如下錯誤: 2.解決辦法: 在下載文件時,對當前的瀏覽器進行判斷, 如果是IE內核的瀏覽器的話,進行文件名的轉碼, 若不是IE內核的瀏覽器,則不用。 關鍵代碼如下: EN ...
  • 我是一名c#老鳥,雖然編程多年,但只會使用c#通過Visual Studio工具開發Windows環境下的桌面應用和網站。這是我自學.net core的經歷,如果你也和我一樣,也是剛剛接觸.net core,並對此有新區,或許能對你有所幫助。眾所周知,.net也是跨平臺的,但是,都是Windows平 ...
  • 前一段時間做過一個 "郵件發送的服務" ,以前大體都測試過,文本、圖片、附件都是沒有問題的,可有同事反應發送的附件名稱有中文亂碼,類似如下截圖展示: 咋一看不像亂碼,抱著試試看的態度,為MimeMessageHelper硬性加了編碼: 並且對文件名稱加了轉碼: 但是,如果你跟進源碼會發現spring ...
  • 在以前的博文中我們介紹了Slick,它是一種FRM(Functional Relation Mapper)。有別於ORM,FRM的特點是函數式的語法可以支持靈活的對象組合(Query Composition)實現大規模的代碼重覆利用,但同時這些特點又影響了編程人員群體對FRM的接受程度,阻礙了FRM ...
  • jps:JVM Process StatusTool,顯示指定系統內所有的HotSpot虛擬機進程 jstat:JVM Statistics Monitoring Tool,用於手機HotSpot虛擬機各方面的運行數據 jinfo: Configuration Info for Java 顯示虛擬機 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...