ReetrantReadWriteLock底層原理

文章目錄

    • 一、讀寫鎖介紹
    • 二、ReentrantReadWriteLock底層原理
      • 1. 讀寫鎖的設計

一、讀寫鎖介紹

現實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁(讀多寫少)。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源(讀讀可以并發);但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫操作了(讀寫,寫讀,寫寫互斥)。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發性和吞吐量。

針對這種場景,JAVA的并發包提供了讀寫鎖ReentrantReadWriteLock,它內部,維護了 一對相關的鎖,一個用于只讀操作,稱為讀鎖;一個用于寫入操作,稱為寫鎖。

線程進入讀鎖的前提條件:

  • 沒有其它線程的寫鎖
  • 沒有寫請求或者有寫請求,但是調用線程和持有鎖的線程是同一個

線程進入寫鎖的前提條件:

  • 沒有其他線程的讀鎖
  • 沒有其他線程的寫鎖

讀寫鎖有以下三個重要的特性:

  • 公平選擇性:支持非公平和公平的鎖獲取方式,吞吐量還是非公平優于公平
  • 可重入:讀鎖和寫鎖都支持線程重入,以讀寫線程為例,讀鎖獲取讀鎖后,能夠再次獲取讀鎖,寫線程在獲取寫鎖之后能夠再次獲取寫鎖,同時也可以獲取讀鎖。
  • 鎖降級:遵循獲取寫鎖,再獲取讀鎖最后釋放寫鎖的次序,寫鎖能夠降級為讀鎖。

二、ReentrantReadWriteLock底層原理

看源碼需要了解三個核心問題

  1. 讀寫鎖是怎樣實現分別記錄讀寫狀態的?
  2. 寫鎖時怎么獲取和釋放的?
  3. 讀鎖時怎么獲取和釋放的?

1. 讀寫鎖的設計

在這里插入圖片描述
首先看它的類信息:

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {}

可以發現該類實現了ReadWriteLock這個接口

public interface ReadWriteLock {Lock readLock();Lock writeLock();
}

該接口就實現了讀鎖和寫鎖的規范,以下就是相關的類圖

在這里插入圖片描述
下面看看ReentrantReadWriteLock的讀寫鎖的實現邏輯,首先看寫鎖:

public static class ReadLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -5992448646407690164L;private final Sync sync;protected ReadLock(ReentrantReadWriteLock lock) {sync = lock.sync;}public void lock() {sync.acquireShared(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireSharedInterruptibly(1);}public boolean tryLock() {return sync.tryReadLock();}public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));}public void unlock() {sync.releaseShared(1);}public Condition newCondition() {throw new UnsupportedOperationException();}public String toString() {int r = sync.getReadLockCount();return super.toString() +"[Read locks = " + r + "]";}}
  • ReadLock:是一個ReetrantReadWriteLock的靜態內部類
  • Sync:和ReentrantLock一樣,Sync也是ReetrantReadWriteLock的一個靜態內部抽象類,它繼承了AbstractQueuedSynchronizer,實現了AQS的邏輯,然后它會有兩種實現,分別是FairSync公平鎖,和NonfairSync非公平鎖
  • lock:可以發現加讀鎖加的是AQS的共享鎖
  • tryLock:嘗試獲取讀鎖

然后看看寫鎖是怎么實現的

 public static class WriteLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -4992448646407690164L;private final Sync sync;protected WriteLock(ReentrantReadWriteLock lock) {sync = lock.sync;}public void lock() {sync.acquire(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}public boolean tryLock( ) {return sync.tryWriteLock();}public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}public void unlock() {sync.release(1);}public Condition newCondition() {return sync.newCondition();}public String toString() {Thread o = sync.getOwner();return super.toString() + ((o == null) ?"[Unlocked]" :"[Locked by thread " + o.getName() + "]");}public boolean isHeldByCurrentThread() {return sync.isHeldExclusively();}public int getHoldCount() {return sync.getWriteHoldCount();}}

下面我們需要思考一個核心問題,讀寫鎖的狀態是怎么用AQS底層的state狀態來維護的。其實這里的核心問題就是,如何用一個變量維護多種狀態。在 ReentrantLock 中,使用 Sync ( 實際是 AQS )的 int 類型的 state 來表示同步狀態,表示鎖被一個線程重復獲取的次數。但是,讀寫鎖 ReentrantReadWriteLock 內部維護著一對讀寫鎖,如果要用一個變量維護多種狀態,需要采用“按位切割使用”的方式來維護這個變量,將其切分為兩部分:高16為表示讀,低16為表示寫。分割之后,讀寫鎖是如何迅速確定讀鎖和寫鎖的狀態呢? 其實底層是通過位運算來實現的。假如當前同步狀態為S, 那么寫狀態,等于 S & 0x0000FFFF(將高 16 位全部抹去)。 當寫狀態加1,等于S+1。讀狀態,等于 S >>> 16 (無符號補 0 右移 16 位)。當讀狀態加1,等于 S+(1<<16),也就是S+0x00010000。根據狀態的劃分能得出一個推論:S不等于0時,當寫狀態(S&0x0000FFFF)等于0時,則讀狀態(S>>>16)大于0,即讀鎖已被獲取。
在這里插入圖片描述
源碼如下:

//該部分在Sync類中static final int SHARED_SHIFT   = 16;static final int SHARED_UNIT    = (1 << SHARED_SHIFT);static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** Returns the number of shared holds represented in count  */static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count  */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • exclusiveCount:獲得持有寫狀態的鎖的次數
  • sharedCount:獲得持有讀狀態的鎖的線程數量,不同于寫鎖,讀鎖利用同時被多個線程持有,而每個線程持有的讀鎖支持重入的特性,所以需要每個線程持有的讀鎖數量單獨計數,這就需要HoldCounter計數器。

HoldCounter計數器
讀鎖的內在機制其實就是一個共享鎖。一次共享鎖的操作就相當于對HoldCounter 計數器的操作。獲取共享鎖,則該計數器 + 1,釋放共享鎖,該計數器 - 1。只有當線程獲取共享鎖后才能對共享鎖進行釋放、重入操作。

static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}

寫鎖的獲取
寫鎖是一個支持重進入的排它鎖。如果當前線程已經獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該線程不是已經獲取寫鎖的線程, 則當前線程進入等待狀態。

protected final boolean tryAcquire(int acquires) {
//獲取當前線程Thread current = Thread.currentThread();//獲取當前state的值int c = getState();//獲取寫鎖的重入次數int w = exclusiveCount(c);//state!=0則表示當前有寫鎖或讀鎖if (c != 0) {//判斷是否是重入if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//獲取重入鎖setState(c + acquires);return true;}// writerShouldBlock有公平與非公平的實現, 非公平返回false,會嘗試通過cas加鎖,c==0 寫鎖未被任何線程獲取,當前線程是否阻塞或者cas嘗試獲取鎖if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//設置鎖由當前線程獨占setExclusiveOwnerThread(current);return true;}

上面簡單的代碼就實現了下面的邏輯:

  • 讀寫互斥
  • 寫寫互斥
  • 寫鎖支持同一個線程重入
  • writeShouldBlock寫鎖是否阻塞實現取決公平與非公平的策略
    在這里插入圖片描述

寫鎖的釋放

     protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();//設置狀態int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;//如果state為0,表示釋放寫鎖if (free)setExclusiveOwnerThread(null);setState(nextc);return free;}

在這里插入圖片描述
讀鎖的獲取

protected final int tryAcquireShared(int unused) {//獲取當前線程Thread current = Thread.currentThread();//獲取stateint c = getState();//exclusiveCount(c) != 0 判斷是否有寫鎖//getExclusiveOwnerThread() != current),判斷當前線程是否是寫鎖的持有者if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&//cas加讀鎖compareAndSetState(c, c + SHARED_UNIT)) {//r==0表示第一次獲取讀鎖if (r == 0) {//設置第一個讀為當前線程firstReader = current;//設置當前讀鎖的重入次數firstReaderHoldCount = 1;} else if (firstReader == current) {          //第一個讀的重入firstReaderHoldCount++;} else {//若不是第一個讀,則用HoldCounter記錄HoldCounter rh = cachedHoldCounter;//第一次讀鎖獲取失敗,再次嘗試if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);//第一次讀鎖獲取失敗,再次嘗試(fullTryAcquireShared)}

上面代碼的邏輯就實現了:

  • 讀鎖共享,讀讀不互斥
  • 讀鎖可重入,每個獲取讀鎖的線程都會記錄對應的重入數
  • 讀寫互斥,鎖降級場景除外
  • 支持鎖降級,持有寫鎖的線程,可以獲取讀鎖,但是后續要記得把讀鎖和寫鎖讀釋放
  • readerShouldBlock讀鎖是否阻塞實現取決公平與非公平的策略(FairSync和NonfairSync)

在這里插入圖片描述
讀鎖的釋放

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}

在這里插入圖片描述

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

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

相關文章

jQuery-操作DOM

使用jQuery操作DOM dom : 文檔對象模型 就是HTML元素 $() 函數的2個用法: 用法1:放入一個字符串(選擇器)表示獲取元素 例如 $("p") $("#abc") $(".del") 用法2:放入一個函數&#xff0c;表示文檔就緒函數 例如 $(function(){代…

TikTok挑戰榜單:全球用戶如何共襄盛舉

TikTok作為全球最受歡迎的短視頻應用之一&#xff0c;在這個平臺上&#xff0c;用戶們通過參與各種挑戰&#xff0c;創造了無數令人驚嘆的短視頻。 本文將深入探討TikTok挑戰榜單的現象&#xff0c;探究全球用戶如何共襄盛舉&#xff0c;以及這種創意激發和社交互動如何成為Ti…

go-zero開發入門-API網關鑒權開發示例

本文是go-zero開發入門-API網關開發示例一文的延伸&#xff0c;繼續之前請先閱讀此文。 在項目根目錄下創建子目錄 middleware&#xff0c;在此目錄下創建文件 auth.go&#xff0c;內容如下&#xff1a; // 鑒權中間件 package middlewareimport ("context""e…

前端開發常用的Vscode插件整理(持續更新)

本文記錄用vscode進行前端開發時&#xff0c;常用到的有用的vscode插件&#xff0c;將不定時更新&#xff5e; 1、Chinese (Simplified) 將編輯器變成簡體中文 2、vscode-icon 讓 vscode 資源樹目錄加上圖標&#xff0c;官方出品的圖標庫 3、Import Cost 引入包大小計算,對于…

Springboot Redis Lua 分布式限流器

pom文件中添加如下依賴包&#xff0c;比較關鍵的就是 spring-boot-starter-data-redis 和 spring-boot-starter-aop。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></d…

基于ssm實驗室開放管理系統論文

摘 要 現代經濟快節奏發展以及不斷完善升級的信息化技術&#xff0c;讓傳統數據信息的管理升級為軟件存儲&#xff0c;歸納&#xff0c;集中處理數據信息的管理方式。本實驗室開放管理系統就是在這樣的大環境下誕生&#xff0c;其可以幫助管理者在短時間內處理完畢龐大的數據信…

高效純化樹脂A-2313 CPR

在化工、制藥等行業中&#xff0c;對colorful chemicals的純化一直是挑戰。本文將為您介紹一款具有卓越性能的強堿性陰離子交換樹脂——Tulsion A-2313 CPR。通過分析其特性和應用&#xff0c;展示其在colorful chemicals純化領域的優勢。 一、Tulsion A-2313 CPR離子交換樹脂的…

代碼隨想錄二刷 |二叉樹 |94.二叉樹的中序遍歷

代碼隨想錄二刷 &#xff5c;二叉樹 &#xff5c;二叉樹的中序遍歷 題目描述解題思路代碼實現迭代法遞歸法 題目描述 94.二叉樹的中序遍歷 給定一個二叉樹的根節點 root &#xff0c;返回 它的 中序 遍歷 。 示例 1&#xff1a; 輸入&#xff1a;root [1,null,2,3] 輸出&a…

漏洞復現-浙江宇視 isc LogReport.php 遠程命令執行漏洞(附漏洞檢測腳本)

免責聲明 文章中涉及的漏洞均已修復&#xff0c;敏感信息均已做打碼處理&#xff0c;文章僅做經驗分享用途&#xff0c;切勿當真&#xff0c;未授權的攻擊屬于非法行為&#xff01;文章中敏感信息均已做多層打馬處理。傳播、利用本文章所提供的信息而造成的任何直接或者間接的…

C++臨時對象生命周期

引言 朋友問了我一段代碼&#xff1a; const string & foo(const string & a, const string & b) {return a.empty() ? b : a; } int main () {auto & s foo("", "foo"); // auto is const stringcout << s << \n;return …

第二百回 如何獲取App自身的信息

文章目錄 1. 概念介紹2. 使用方法2.1 ClipOval2.2 ClipRRect 3. 示例代碼 我們在上一章回中介紹了AspectRatio Widget相關的內容&#xff0c;本章回中將介紹剪裁類組件(Clip).閑話休提&#xff0c;讓我們一起Talk Flutter吧。 1. 概念介紹 我們在這里說的剪裁類組件主要是指對…

dockerfile---創建鏡像

dockerfile創建鏡像&#xff1a;創建自定義鏡像。 包擴配置文件的創建&#xff0c;掛載點&#xff0c;對外暴露的端口。設置環境變量。 docker鏡像的方式: 1、基于官方源進行創建 根據官方提供的鏡像源&#xff0c;創建鏡像&#xff0c;然后拉起容器。是一個白板&#xff0c…

初識人工智能,一文讀懂強化學習的知識文集(5)

&#x1f3c6;作者簡介&#xff0c;普修羅雙戰士&#xff0c;一直追求不斷學習和成長&#xff0c;在技術的道路上持續探索和實踐。 &#x1f3c6;多年互聯網行業從業經驗&#xff0c;歷任核心研發工程師&#xff0c;項目技術負責人。 &#x1f389;歡迎 &#x1f44d;點贊?評論…

2023年運營級網賺網盤平臺搭建指南(包含源碼和教程)

源碼介紹 為什么要考慮自己搭建網盤呢&#xff1f;現如今&#xff0c;許多大型網盤平臺都對文件添加了各種限制&#xff0c;導致很多文件容易被刪除。而且&#xff0c;大部分網盤還會限制下載速度&#xff0c;如果沒有開通VIP會員&#xff0c;使用起來非常不便。 本指南提供了…

免費節假日api接口使用教程-聚合數據

免費節假日api接口使用教程-聚合數據 文章目錄 &#x1f4d6;訪問官網&#x1f330;例子完整代碼&#x1f58a;?最后總結 &#x1f4d6;訪問官網 聚合數據 官網地址 https://dashboard.juhe.cn/home 點擊api 接口文檔 &#x1f330;例子 get方式 curl -k -i -d “key您申請…

解決Git提交錯誤分支

如果 Git 提交到錯誤的分支&#xff0c;可以通過以下步驟將其轉移到正確的分支上&#xff1a; 1.檢查當前所在的分支&#xff0c;可以通過 git branch 命令查看。 git branch2.切換到正確的分支&#xff0c;可以通過 git checkout <正確的分支名> 命令進行切換。 git …

vue使用echarts顯示中國地圖

項目引入echarts以后&#xff0c;在頁面創建canvas標簽 引入一個公共js文件&#xff08;下面這段代碼就是china.js文件&#xff09; (function (root, factory) {if (typeof define function && define.amd) {// AMD. Register as an anonymous module.define([ex…

【EXCEL】折線圖添加垂直x軸的豎線|畫圖

相關鏈接&#xff1a;excel 添加垂直豎向直線 如何在Excel中添加水平和垂直線&#xff1f; 因為加輔助列有點不習慣&#xff0c;已經有分位數橫坐標了&#xff0c;想著試下用散點圖的誤差線畫 效果圖&#xff1a; 步驟&#xff1a; s1&#xff1a;隨便框選兩列數據–>插入(…

大創項目推薦 卷積神經網絡手寫字符識別 - 深度學習

文章目錄 0 前言1 簡介2 LeNet-5 模型的介紹2.1 結構解析2.2 C1層2.3 S2層S2層和C3層連接 2.4 F6與C5層 3 寫數字識別算法模型的構建3.1 輸入層設計3.2 激活函數的選取3.3 卷積層設計3.4 降采樣層3.5 輸出層設計 4 網絡模型的總體結構5 部分實現代碼6 在線手寫識別7 最后 0 前言…

深入理解JavaScript異步編程與Promise

異步編程的背景 在Web開發中&#xff0c;異步編程是為了解決JavaScript的單線程執行模型導致的阻塞問題。異步編程允許程序在等待某些操作完成的同時&#xff0c;繼續執行其他任務&#xff0c;提高了程序的效率和響應速度。 回調地獄與Promise的誕生 回調地獄是異步編程中一…