Java學習筆記(多線程):ReentrantLock 源碼分析

本文是自己的學習筆記,主要參考資料如下
JavaSE文檔


  • 1、AQS 概述
    • 1.1、鎖的原理
    • 1.2、任務隊列
      • 1.2.1、結點的狀態變化
    • 1.3、加鎖和解鎖的簡單流程
  • 2、ReentrantLock
    • 2.1、加鎖源碼分析
      • 2.1.1、tryAcquire()的具體實現
      • 2.1.2、acquirQueued()的具體實現
      • 2.1.3、tryLock的具體實現
      • 2.1.5、總結

1、AQS 概述

1.1、鎖的原理

AQS是指抽象類AbstractQueuedSynchronizer。這個抽象類代表著一種實現并發的方式。

具體實現方式是使用volitile修飾state變量,保證了state的可見性和有序性。最后使用CAS改變state的值,保證原子性。

那么AbstractQueuedSynchronizer通過更新state的值來實現的加鎖和解鎖。

下面是關鍵源代碼的截圖。
請添加圖片描述
請添加圖片描述


1.2、任務隊列

AQS中維護了一個任務隊列,是一個雙向隊列。隊列節點是內部類Node

Node中記錄者節點的狀態waitStatus,比如CANCELSIGNAL等分別表示該任務節點已經取消和任務節點正在沉睡需要被喚醒。

當然,因為是雙向列表所以也有指向前后節點的指針。下面是Node源碼的部分截圖。
請添加圖片描述

這個隊列會初始化一個頭結點和一個尾結點作為虛擬節點。頭結點的狀態在整個加鎖和釋放鎖的過程中都會變化。

1.2.1、結點的狀態變化

當頭結點指向的Node才擁有鎖。

這里主要介紹三個狀態

  • 0, 表示當前Node后續無節點在排隊。不表明是否擁有鎖。
  • -1,表示除了當前Node在排隊以外,還有其他Node排在當前Node后面。不表明是否擁有鎖。
  • 1,表示當前Node可能因為等待時間太長而放棄獲取鎖。

下面是三個Node在隊列中的狀態。這里從左到右解釋他們的狀態。
請添加圖片描述
head指向第一個Node,所以當前Node擁有鎖。

第一個NodewaitStatus=-1表示后續有節點等待獲取鎖。當該節點釋放鎖時會喚醒后續的節點。

第二個NodewaitStatus = -1,后續有節點等待獲取鎖。

第三個NodewaitStatus = 0,后續無節點等待獲取鎖。

1.3、加鎖和解鎖的簡單流程

假設有兩個線程A和B,他們需要爭奪基于AQS實現的鎖,下面是爭奪的簡單流程。

  1. 線程A先執行CAS,將state從0修改為1,線程A就獲取到了鎖資源,去執行業務代碼即可。
  2. 線程B再執行CAS,發現state已經是1了,無法獲取到鎖資源。
  3. 線程B需要去排隊,將自己封裝為Node對象。
  4. 需要將當前B線程的Node放到雙向隊列保存,排隊。

2、ReentrantLock

2.1、加鎖源碼分析

ReentrantLock分為公平鎖和非公平鎖。在加鎖的時候因這兩種鎖的不同會有不同的加鎖方式。

ReentrantLock默認是非公平鎖,構造方法中傳入false則是公平鎖。

非公平鎖的lock()方法會直接基于CAS嘗試獲取鎖,如果成功的話則執行setExclusiveOwnerThread()方法表示當前線程持有該鎖;如果失敗則執行acquire()方法。

公平鎖則是直接執行acquire()方法。下面是源碼對比。
請添加圖片描述
接下來的重點則是看acquire()的具體操作。

tryAcquire()方法會再次嘗試獲取鎖,如果成功返回true,否則返回false

可以看到如果失敗的話則將請求放到等待隊列中同時發送中斷信號。
在這里插入圖片描述

2.1.1、tryAcquire()的具體實現

  • 非公平鎖
    非公平鎖會嘗試再次直接通過CAS獲取鎖資源。因為是可重入鎖,所以當鎖的持有者是當前線程時也可直接獲取鎖,然后計數器加一。
    請添加圖片描述

  • 公平鎖
    公平鎖的邏輯與非公平鎖類似,只不過再獲取鎖之前會先判斷AQS中自己是不是排在第一位,之后才會獲取鎖。
    請添加圖片描述

2.1.2、acquirQueued()的具體實現

在這里插入圖片描述
tryAcquire()返回false,即獲取鎖失敗,就開始嘗試將當前線程封裝成Node節點插入到AQS的結尾。

在插入時我們會看到if(p == head && tryAcquire(arg))這樣的語句。

這是因為AQS有偽頭結點,所以當這個線程插入到AQS中時發現自己的上一個節點是頭結點,即自己排在第一位,那無論是公平鎖還是非公平鎖自己都可以再次測試獲取鎖。所以會再次執行tryAcquire()

final boolean acquireQueued(final Node node, int arg) {// 不考慮中斷// failed:獲取鎖資源是否失敗(這里簡單掌握落地,真正觸發的,還是tryLock和lockInterruptibly)boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到當前節點的前繼節點final Node p = node.predecessor();// 前繼節點是否是head,如果是head,再次執行tryAcquire嘗試獲取鎖資源。if (p == head && tryAcquire(arg)) {// 獲取鎖資源成功setHead(node);p.next = null; // 獲取鎖失敗標識為falsefailed = false;return interrupted;}// 沒拿到鎖資源……// shouldParkAfterFailedAcquire:基于上一個節點轉改來判斷當前節點是否能夠掛起線程,如果可以返回true,// 如果不能,就返回false,繼續下次循環if (shouldParkAfterFailedAcquire(p, node) &&// 這里基于Unsafe類的park方法,將當前線程掛起parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)// 在lock方法中,基本不會執行。cancelAcquire(node);}
}

2.1.3、tryLock的具體實現

無參的tryLock()比較簡單,和tryAcquire()基本沒區別。

這里主要講解有參的tryAcquireNanos(int arg, long nanosTimeout)

它的作用在一個時間內嘗試獲得鎖。在這個時間內沒有獲得鎖會掛起park線程。如果成功則返回true,時間結束還沒有獲得則返回false

public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

該方法需要處理中斷異常,和lock()方法不一樣。

我們繼續深入。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}

可以看到,它直接通過線程的中斷標志位決定是否拋出異常。

之后進行tryAcquire(),這個方法細節上面分析過,它有公平和非公平兩種實現,簡而言之就是非公平直接嘗試CAS加鎖,公平則是進入隊列排隊。

也就是說,最后它會正常加鎖,只有失敗時才會執行doAcquireNanos()。所以有參的tryLock()方法park線程的細節就在其中。

那下面就看看這個方法的內部。

核心就是線程會被封裝Node放到隊列中,之后查看時間,如果時間比較長,就park線程直到時間結束后再嘗試獲取鎖;如果時間比較短,就在死循環中等到時間結束然后再次獲得鎖。

因為park的線程主要會因兩個動作結束park,即時間到,或者線程發出中斷狀態,所以最后會查看park是因為什么結束的。如果是中斷則拋出異常,否則嘗試獲取鎖。

private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {// 如果等待時間是0秒,直接告辭,拿鎖失敗  if (nanosTimeout <= 0L)return false;// 設置結束時間。final long deadline = System.nanoTime() + nanosTimeout;// 先扔到AQS隊列final Node node = addWaiter(Node.EXCLUSIVE);// 拿鎖失敗,默認trueboolean failed = true;try {for (;;) {// 如果在AQS中,當前node是head的next,直接搶鎖final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 結算剩余的可用時間nanosTimeout = deadline - System.nanoTime();// 判斷是否是否用盡的位置if (nanosTimeout <= 0L)return false;// shouldParkAfterFailedAcquire:根據上一個節點來確定現在是否可以掛起線程if (shouldParkAfterFailedAcquire(p, node) &&// 避免剩余時間太少,如果剩余時間少就不用掛起線程nanosTimeout > spinForTimeoutThreshold)// 如果剩余時間足夠,將線程掛起剩余時間LockSupport.parkNanos(this, nanosTimeout);// 如果線程醒了,查看是中斷喚醒的,還是時間到了喚醒的。if (Thread.interrupted())// 是中斷喚醒的!throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}

2.1.5、總結

ReentrantLock的加鎖有公平鎖和非公平鎖兩種方式。

對于非公平鎖,任務一開始會直接嘗試通過CAS獲取鎖,失敗后才會進入任務隊列。并且進入的時候會再次嘗試獲取鎖。整個過程并不考慮其他節點等了多久,所以才是非公平鎖。

對于公平鎖,任務會按序先進入任務隊列,直到有人喚醒他們才會開始獲取鎖。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/900628.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/900628.shtml
英文地址,請注明出處:http://en.pswp.cn/news/900628.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在C++11及后續標準中,auto和decltype是用于類型推導的關鍵特性,它們的作用和用法。

在C11及后續標準中&#xff0c;auto和decltype是用于類型推導的關鍵特性&#xff0c;它們的作用和用法有所不同。以下是詳細說明&#xff1a; 1. auto 關鍵字 基本作用 自動推導變量的類型&#xff08;根據初始化表達式&#xff09;主要用于簡化代碼&#xff0c;避免顯式書寫…

Linux:進程程序替換execl

目錄 引言 1.單進程版程序替換 2.程序替換原理 3.6種替換函數介紹 3.1 函數返回值 3.2 命名理解 3.3 環境變量參數 引言 用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支)&#xff0c;我們所創建的所有的子進程&#xff0c;執行的代碼&#x…

LeetCode.02.04.分割鏈表

分割鏈表 給你一個鏈表的頭節點 head 和一個特定值 x &#xff0c;請你對鏈表進行分隔&#xff0c;使得所有 小于 x 的節點都出現在 大于或等于 x 的節點之前。 你不需要 保留 每個分區中各節點的初始相對位置。 示例 1&#xff1a; 輸入&#xff1a;head [1,4,3,2,5,2], x …

Johnson算法 流水線問題 java實現

某印刷廠有 6項加工任務J1&#xff0c;J2&#xff0c;J3&#xff0c;J4&#xff0c;J5&#xff0c;J6&#xff0c;需要在兩臺機器Mi和M2上完 成。 在機器Mi上各任務所需時間為5,1,8,5,3,4單位; 在機器M2上各任務所需時間為7,2,2,4,7,4單位。 即時間矩陣為&#xff1a; T1 {5, …

按鍵++,--在操作uint8_t類型(一個取值為1~10的數)中,在LCD中顯示兩位數字問題

問題概況 在執行按鍵&#xff0c;--過程中&#xff0c;本來數值為1~10.但是在執行過程中&#xff0c;發現數值在經過10數值后&#xff0c;后面的“0”會一直在LCD顯示屏中顯示。 就是執行操作中&#xff0c;從1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xf…

【QT】QTreeWidgetItem的checkState/setCheckState函數和isSelected/setSelected函數

目錄 1、函數原型1.1 checkState/setCheckState1.2 isSelected/setSelected2、功能用途3、示例QTreeWidget的checkState/setCheckState函數和isSelected/setSelected這兩組函數有著不同的用途,下面具體說明: 1、函數原型 1.1 checkState/setCheckState Qt::CheckState QTr…

005 vue項目結構 vue請求頁面執行流程(vue2)

文章目錄 vue項目結構vue請求頁面執行流程main.jsrouterHelloWorld.vueApp.vueindex.html vue項目結構 config目錄存放的是配置文件&#xff0c;比如index.js可以配置端口 node_modules存放的是該項目依賴的模塊&#xff0c;這些依賴的模塊在package.json中指定 src目錄分析 1…

匯豐xxx

1. Spring Boot 的了解&#xff0c;解決什么問題&#xff1f; 我的理解&#xff1a; Spring Boot 是一個基于 Spring 框架的快速開發腳手架&#xff0c;它簡化了 Spring 應用的初始搭建和開發過程。解決的問題&#xff1a; 簡化配置&#xff1a; 傳統的 Spring 應用需要大量的…

基于 Spring Boot 瑞吉外賣系統開發(一)

基于 Spring Boot 瑞吉外賣系統開發&#xff08;一&#xff09; 系統概述 系統功能 技術選型 初始項目和數據準備 初始項目和SQL文件下載 創建數據庫并導入數據 打開reggie項目 運行效果 主函數啟動項目&#xff0c;訪問URL&#xff1a; http://127.0.0.1:8080/backend/pag…

大型語言模型智能應用Coze、Dify、FastGPT、MaxKB 對比,選擇合適自己的LLM工具

大型語言模型智能應用Coze、Dify、FastGPT、MaxKB 對比&#xff0c;選擇合適自己的LLM工具 Coze、Dify、FastGPT 和 MaxKB 都是旨在幫助用戶構建基于大型語言模型 (LLM) 的智能應用的平臺。它們各自擁有獨特的功能和側重點&#xff0c;以下是對它們的簡要對比&#xff1a; Coz…

【項目管理】第6章 信息管理概論 --知識點整理

項目管理 相關文檔&#xff0c;希望互相學習&#xff0c;共同進步 風123456789&#xff5e;-CSDN博客 &#xff08;一&#xff09;知識總覽 項目管理知識域 知識點&#xff1a; &#xff08;項目管理概論、立項管理、十大知識域、配置與變更管理、績效域&#xff09; 對應&…

Zapier MCP:重塑跨應用自動化協作的技術實踐

引言&#xff1a;數字化協作的痛點與突破 在當今多工具協同的工作環境中&#xff0c;開發者與辦公人員常常面臨數據孤島、重復操作等效率瓶頸。Zapier推出的MCP&#xff08;Model Context Protocol&#xff09;協議通過標準化數據交互框架&#xff0c;為跨應用自動化提供了新的…

echart實現動態折線圖(vue3+ts)

最近接到個任務&#xff0c;需要用vue3實現動態折線圖。之前沒有用過&#xff0c;所以一路坎坷&#xff0c;現在記錄一下&#xff0c;以后也好回憶一下。 之前不清楚echart的繪制方式&#xff0c;以為是在第一秒的基礎上繪制第二秒&#xff0c;后面實驗過后&#xff0c;發現并…

Java學習——day24(反射進階:注解與動態代理)

文章目錄 1. 反射與注解2. 動態代理3. 實踐&#xff1a;編寫動態代理示例4. 注解定義與使用5. 動態代理6. 小結與思考 1. 反射與注解 注解&#xff1a;注解是 Java 提供的用于在代碼中添加元數據的機制。它不會影響程序的執行&#xff0c;但可以在運行時通過反射獲取和處理。反…

C++之nullptr

文章目錄 前言 一、NULL 1、代碼 2、結果 二、nullptr 1、代碼 2、結果 總結 前言 當我們談論空指針時,很難避免談及nullptr。nullptr是C++11引入的一個關鍵字,用來表示空指針。在C++中,空指針一直是一個容易引起混淆的問題,因為在早期版本的C++中,通常使用NULL來…

JavaScript惰性加載優化實例

這是之前的一位朋友的酒桌之談&#xff0c;他之前負責的一個電商項目&#xff0c;剛剛開發萬&#xff0c;首頁加載時間特別長&#xff0c;體驗很差&#xff0c;所以就開始排查&#xff0c;發現是在首頁一次性加載所有js導致的問題&#xff0c;這個問題在自己學習的時候并不明顯…

蘋果內購支付 Java 接口

支付流程&#xff0c;APP支付成功后 前端調用后端接口&#xff0c;后端接口將前端支付成功后拿到的憑據傳給蘋果服務器檢查&#xff0c;如果接口返回成功了&#xff0c;就視為支付。 代碼&#xff0c;productId就是蘋果開發者后臺提前設置好的 產品id public CommonResult<S…

數據庫中的數組: MySQL與StarRocks的數組操作解析

在現代數據處理中, 數組 (Array) 作為一種高效存儲和操作結構化數據的方式, 被廣泛應用于日志分析, 用戶行為統計, 標簽系統等場景. 然而, 不同數據庫對數組的支持差異顯著. 本文將以MySQL和StarRocks為例, 深入解析它們的數組操作能力, 并對比其適用場景. 文章目錄 一 為什么需…

LeetCode零錢兌換(動態規劃)

題目描述 給你一個整數數組 coins &#xff0c;表示不同面額的硬幣&#xff1b;以及一個整數 amount &#xff0c;表示總金額。 計算并返回可以湊成總金額所需的 最少的硬幣個數 。如果沒有任何一種硬幣組合能組成總金額&#xff0c;返回 -1 。 你可以認為每種硬幣的數量是無…

/sys/fs/cgroup/memory/memory.stat 關鍵指標說明

目錄 1. **total_rss**2. **total_inactive_file**3. **total_active_file**4. **shmem**5. **其他相關指標**總結 以下是/sys/fs/cgroup/memory/memory.stat文件中一些關鍵指標的詳細介紹&#xff0c;特別是與PostgreSQL相關的指標&#xff1a; 1. total_rss 定義&#xff1…