CurrentHashMap的整體系統介紹及Java內存模型(JVM)介紹

當我們提到ConurrentHashMap時,先想到的就是HashMap不是線程安全的:

在多個線程共同操作HashMap時,會出現一個數據不一致的問題。

ConcurrentHashMap是HashMap的線程安全版本。

它通過在相應的方法上加鎖,來保證多線程情況下的數據一致性。

hashmap導致數據不一致的原因?

數據不一致問題的表象有兩種情況:

1.寫-讀沖突:一個線程修改后,另一個線程讀到的不是最新的數據。

2.寫-寫沖突:兩個線程同時修改數據,發生數據覆蓋的情況。

原因是Java內存模型(JVM)的一些相關規定。

Java內存模型(JVM)

Java內存模型將內存分為兩種,主內存工作內存。

并且規定,所有的變量都存儲在主內存中(不包括局部變量與方法參數)。

主內存中的變量是所有線程共享的。

每個線程都有自己的工作內存,存儲的是當前線程所使用到的變量值。主內存變量中的一個副本數據。

線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。

不同線程間無法直接訪問對方工作內存中的變量。

線程間變量值的傳遞需要通過主內存實現。

這樣規定的原因:

是為了屏蔽各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。

關于各種硬件間的內存訪問差異

CPU,內存,IO設備都在不斷迭代,不斷朝著更快的方向努力,但三者的速度是有差異的。

CPU最快,內存其次,IO設備(硬盤)最慢。

為了合理利用CPU的高性能,平衡三者間的速度差異,計算機體系結構,操作系統,編譯系統都做了貢獻,主要體現為:

  • CPU增加了緩存,以平衡與內存的速度差異,

這樣CPU運算時所需要的變量,優先會從緩存中讀取。

緩存沒有時,會從主內存中加載并緩存。如下圖所示:

image-20250509163155756

事物都是有兩面性的,緩存提高了CPU的運算速度,也帶來了相應的問題:

當多個線程在不同的CPU上運行并訪問同一個變量時,由于緩存的存在,可能讀取不到做最新的值,也就是可見性問題。

可見性指的是一個線程對共享變量的修改,另一個線程能夠立刻看到,被稱為可見性

  • 操作系統增加了進程線程,以時分復用CPU,進而均衡CPU與IO設備的速度差異

操作系統通過任務的一個切換來減少CPU的等待時間,從而提高效率。

任務切換的時間,可能是發生在任何一條CPU指令執行完之后。

但是我們平時使用的編程語言,如C,Java,Python等都是高級語言,高級語言轉換成CPU指令時,一條指令可能對應多條CPU指令。 相當于1=n,這是違背我們直覺的地方。

所以問題來了,著名的count+=1問題就是這個原因。也就是原子性問題。

我們把一個或多個操作在CPU執行的過程中不被中斷的特性為原子性。(這里的操作是指我們高級語言中相應的一些操作)

  • 編譯程序優化指令執行次序,使得緩存能夠得到更加合理的利用。

指令重排序可以提高了緩存的利用率,同樣也帶來了有序性問題

也就是單例模式問題

重排序提高緩存利用率的例子:

在平時寫代碼時,經常會在方法內部的開始位置,把這個方法用到的變量全部聲明了一遍。緩存的容量是有限的,聲明的變量多的時候 前面的變量可能就會在緩存中失效 。

接下來再寫業務時,用到了最先聲明的變量 然后發現在緩存中已經失效了,需要重新的去主內存進行加載。

所以指令重排序可以看成編譯器對我們寫的代碼進行的一個優化。就類似于讓變量都能用上,不至于等到失效在使用。

所以要想實現在各種平臺都能達到一直的內存訪問效果,就需要解決硬件和操作系統之間產生的問題:

1.CPU增加緩存最后導致的可見性問題

2.操作系統增加了線程,進程之后出現的原子性問題

3.指令重排序導致的有序性問題

Java內存模型如何解決三個問題?

原子性問題解決方案

  • JVM定義了8種操作來完成主內存與工作內存之間的數據交互,虛擬機在實現時需要保證每一種操作都是原子的,不可再分的。

Java中基本數據類型的訪問、讀寫都是具備原子性的(long和Double除外),更大的原子性保證:Java提供了synchronized關鍵字(synchronized的字節碼指令monitorenter和monitorexit來隱式的使用了lock和unlock操作),在synchronized塊之間的操作也具備原子性。

八種操作: lock,unlock,read,load,assign,use,store,write

CAS(樂觀鎖),比較并替換,(Compare And Swap),CAS是一條CPU的原子指令(即cmpxchg指令),Java中的Unsafe類提供了相應的CAS方法,如(compareAndSwapXXX)底層實現即為CPU指令cmpxchg,從而保證操作的原子性。

可見性問題與有序性問題解決方案

  • JVM定義了Happens-Before原則來解決內存的不可見性與重排序的問題。

Happens-Before規則約束了編譯器的優化行為,雖允許編譯器優化,但是要求編譯器優化后要遵守Happens-Before規則。

Happens-Before規則:

對于兩個操作A和B,這兩個操作可以在不同的線程中執行,如果A Happens-Before B,那么可以保證,當A操作執行完后,A操作的執行結果對B操作時可見的。

8種Happens-Before規則

程序次序規則、鎖定規則、volatile變量規則、線程啟動規則、線程終止規則、線程中斷規則、對象終結原則、傳遞性原則。

volatile變量規則(重點):對一個volatile變量的寫操作先行發生于后面的這個變量的讀操作。

hashmap導致數據不一致的解決方案

常規思路是加鎖,但是鎖的存在會大大影響性能,所以提升性能的關鍵就是減少鎖的粒度,以及找出哪些操作可以無鎖化

對于寫操作:涉及到對數據的改動,需要加鎖,這只能盡量減少鎖的粒度。

對于讀操作:確保數據改動不會出錯之后,讀操作就相對好辦;主要考慮的能不能讀到另外一個線程對數據的一個改動(一致性)(等待寫操作的完成)

這時就有三種情況:

  1. 強一致性 : 讀寫都加鎖,類似于串行化,這樣可以保證讀到最新的數據,但性能過低

  2. 順序一致性 : 變量使用volatile關鍵字修飾

  3. 弱一致性 : 讀不加鎖

對應方案:

  1. 強一致性 使用synchronized 修飾方法或者代碼塊,來保證代碼塊或方法的一致性,可見性(串行,即有序性),性能較低

  2. 順序一致性 : 使用volatile關鍵字修飾變量,volatile 可以保證一個共享變量的可見性以及禁止指令的重排序

  3. 弱一致性: 使用CAS,CAS操作可以保證一個共享變量的原子操作。

我們可以去讀一下ConcurrentHashMap的源碼,

可以發現代碼中一會使用CAS,一會使用synchronized,讓人摸不清,為什么呢?

這是因為在高級語言中一條語句往往需要多條CPU指令完成

而Java中基本數據類型的訪問、讀寫都具備原子性(long和Double除外),其他大部分不是原子性操作,

就比如在new一個對象時,就不是一個原子性操作,它需要三步才能完成,分配內存,初始化對象,將對象賦值給變量。

所以在創建數組的時候,除了使用synchronized外,CAS是不能保證原子性的,CAS只是CPU的一條指令,他不能保證多個指令的原子性,但是我們可以參考AQS,使用CAS鎖一個基本類型的變量,其他線程進行自旋。

其次,synchronized鎖需要一個對象,當數組的元素為null時,是無法使用synchronized鎖的,所以此時使用的就是CAS操作來保證賦值的原子性。

以及底層的數組table已經被volatile修飾,但是數組元素的修改卻不能保證可見性

明明volatile保證共享變量的可見性,為什么數組元素的修改卻不能保證可見性呢?

原因:

volatile保證共享變量的可見性,但是如果該變量是一個對象的引用,那么volatile此時指的就是對象引用的可見性。

而在Java中,數組也是一個對象,當使用volatile來修飾數組arr時,代表的是arr的引用具有可見性,即arr的引用地址修改了之后,其他線程是可見的,但是無法保證數組內的元素具有可見性。

HashTable與ConcurrentHashMap

Hashtable

前置知識:在JDK1.0時,加鎖只有synchronized一種方法,synchronized是重量級鎖(需要去CPU申請鎖)

底層結構:數組+鏈表 鏈表使用頭插法 定位數組下標使用取余操作

線程安全: 使用synchronized來保證線程安全,在所有的方法上都加了synchronized關鍵字,即使用一把全局鎖來同步不同線程間的并發訪問(鎖住整個table結構),性能較低。

相關操作: put,get,remove,size方法體上都添加synchronized關鍵字,擴容邏輯在put方法內發生,也是線程安全的

優點:實現簡單

缺點:一個線程在插入數據時,其他線程不能讀寫,并發效率低下

ConcurrentHashMap(JDK1.5)

在JDK1.5時引入,此時Java內存模型已經成熟完善,在此基礎上開發了java.util.concurrent包,ConcurrentHashMap隨著JUC包一起引入JDK,同時引入了AQS,實現了ReentrantLock

底層結構:數組+鏈表 鏈表使用頭插法 定位下標使用&運算

線程安全:使用分段鎖的思想,其內部是一個Segment數組,Segment繼承了ReentrantLock(可重復鎖),即Segment自身就是一個鎖。

Segment內部有一個HashEntry數組(Segment有點類似HashTable),每個HashEntry是一個鏈表結構的元素,一把鎖只鎖住容器中的一部分數據,多線程訪問容器中里不同數據段的數據,就不會存在鎖競爭,提高并發訪問率

相關操作:調用put方法時,當前的segment會將自己鎖住,此時其他線程無法操作這個segment,但不會影響到其他segment的操作。

調用get方法時,使用unsafe.getObjectVolatile方法獲取節點;底層使用C++的volatile來實現Java中的volatile效果(保證共享變量的可見性(一個線程對共享變量的修改,另一個線程能夠立刻看到))

調用remove方法時,當前的segment會將自己鎖住。

put,get,remove操作都是在單個Segment上進行的,size操作是在多個segment進行的

size方法采用了一種比較巧妙的方式,來盡量避免對所有的Segment都加鎖。

每個Segment都有一個modCount 變量,代表的是對Segment中元素的數量造成影響的操作次數。這個值只增不減。

size 操作就是遍歷了兩次Segment,每次記錄Segment 的modCount值,然后將兩次的modCount進行比較,如果相同,則表示期間沒有發生過寫入操作,就將原先遍歷的結果返回。如果不相同,則把這個過程再重復做一次,如果再不同,則就需要將所有的Segment都鎖住,然后一個一個遍歷。

擴容操作,發生在put方法內部,跟put方法使用的是同一個鎖.

擴容不會增加Segment的數量,只會增加Segment中鏈表數組的容量大小

這樣的好處是擴容過程不需要對整個ConcurrentHashMaprehash,只需要對Segment里面的元素做一個rehash即可。這樣就不會去影響其他的segment里面的元素。

優點:每次只鎖住一部分數據,訪問不同數據段的數據,不會存在鎖競爭。提高了并發訪問率;

擴容只針segment內部的HashEntry數組進行擴容,不影響其他segment內部的HashEntry數組。

缺點:定位一個元素,需要經過兩次hash操作。 當某個segment很大時,類似Hashtable,性能會下降。

比較浪費內存空間(因為每個segment內部的HashEntry數組是不連續的)

拓展:

在JDK6中,針對synchronized做了大量的優化,引入了輕量級鎖偏向鎖。性能與ReentrantLock已相差無幾,甚至synchronized的自動釋放鎖會更好用。

Java官方表示,在多線程環境下不建議使用HashMap。

隨著互聯網的快速發展,業務場景隨之更加復雜,很多人在使用多線程的情況下使用HashMap的時候,結果導致cpu100%的情況。

主要原因:HashMap的鏈表使用的是頭插法,在多線程的情況下觸發擴容,鏈表可能會形成一個死循環。

在JDK8中也做了相應的優化,將頭插法改為尾插法,引入了紅黑樹,來優化鏈表過長導致的查詢速度變慢。

連帶著ConcurrentHashMap也做了相應的修復,使得ConcurrentHashMap與HashMap的結構更加統一。

ConcurrentHashMap(JDK8之后)

image-20250509190726003

由類圖可知,ConcurrentHashMap中有四種類型的節點,四種類型的節點的用途不同。

  • Node節點是ConcurrentHashMap中存儲數據的最基本結構,也是其他類型節點的父類,他可以用來構建鏈表。hash值>=0

  • TreeNode節點主要用來構造紅黑樹以及存儲數據hash值>=0

  • TreeBin節點是紅黑樹的代理節點,不存儲數據,他的Hash值是一個固定值-2

  • ForWardingNode節點,表示的是底層數組table正在擴容,當前節點的數據已經遷移完畢,不存儲數據,hash值也是固定值-1

注意事項:TreeBin為什么是紅黑樹的代理節點?

因為在向紅黑樹添加數據或刪除數據時可能會觸發紅黑樹的自平衡,根節點可能會被子節點替代,如果此時有線程來紅黑樹讀取數據,可能會出現讀取不到數據的情況。

而紅黑樹的查找是從根節點開始遍歷的,當根節點變成子節點時,作為根節點的左子樹或者右子樹可能是不被遍歷的。

ConcurrentHashMap的get方法是沒有使用鎖的,不可能通過加鎖來保證強一致性,而紅黑樹的并發操作需要加上一層鎖來保證在紅黑樹自平衡時的讀操作沒有問題。這就是TreeBin的工作。

TreeBin重要屬性:

  • root:指向的是紅黑樹的根節點

  • first:指向的是雙向鏈表,也就是所有的TreeNode節點構成的一個雙向鏈表

  • lockState:用于實現基于CAS的讀寫鎖。

總結:對紅黑樹添加或刪除數據的整體操作:

首先在最外層加上synchronized同步鎖,然后再紅黑樹自平衡時加上lockState的寫鎖。

當由線程來讀紅黑樹的時候,會先判斷此時是否有線程持有寫鎖或者是否有線程在等待獲取寫鎖,如果有的話,讀線程直接讀取雙向鏈表,否則會加上lockState的讀鎖。然后讀取紅黑樹的數據,從而來保證讀操作不被阻塞以及它的正確性。

雙向鏈表的作用:

  • 讀操作會來讀取鏈表上的數據。

  • 在擴容時,會遍歷雙向鏈表,根據hash值判斷是放在新數組的高位還是低位。

底層結構:數組+鏈表+紅黑樹 鏈表使用尾插法 定位下標使用 & 運算

線程安全:消了分段鎖的設計,1取而代之的是通過 cas 操作和 synchronized 關鍵字來保證并發更新的安全。

Synchronized只是用于鎖住鏈表或者紅黑樹的第一個節點,只要沒有Hash沖突,就不存在并發問題,效率也就大大的提升。

相關操作:

put方法,使用cas + synchronized 來保證線程安全.

get方法,沒有使用加鎖,使用的是Unsafe.getObjectVolatile方法獲取數據。保證數據的可見性。

remove方法、使用synchronized 來保證線程安全。

size方法(難點):主要是LongAdder的思想進行的累加計算。

擴容操作(難點):擴容操作發生在數據添加成功之后,并且支持多個線程。

優點:鎖粒度更精細,性能更強

缺點:實現更加復雜。

希望對大家有所幫助!

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

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

相關文章

Android開發-設計規范

在Android應用開發中,遵循良好的設計規范不僅能夠提升用戶體驗,還能確保代碼的可維護性和擴展性。本文將從用戶界面(UI)、用戶體驗(UX)、性能優化以及代碼結構等多個維度探討Android開發中的設計規范&#…

泛型加持的策略模式:打造高擴展的通用策略工具類

一、傳統策略模式的痛點與突破 1.1 傳統策略實現回顧 // 傳統支付策略接口 public interface PaymentStrategy {void pay(BigDecimal amount); }// 具體策略實現 public class AlipayStrategy implements PaymentStrategy {public void pay(BigDecimal amount) { /* 支付寶支…

物聯網從HomeAssistant開始

文章目錄 一、什么是home-assistant?1.核心架構2.集成架構 二、在樹梅派5上安裝home-assistant三、接入米家1.對比下趨勢2.手動安裝插件3.配置方式 四、接入公牛1.手動安裝插件2.配置方式 五、接入海爾1.手動安裝插件2.配置方式 六、接入國家電網 一、什么是home-assistant? …

系統架構-嵌入式系統架構

原理與特征 嵌入式系統的典型架構可概括為兩種模式,即層次化模式架構和遞歸模式架構 層次化模式架構,位于高層的抽象概念與低層的更加具體的概念之間存在著依賴關系,封閉型層次架構指的是,高層的對象只能調用同一層或下一層對象…

計算機圖形學編程(使用OpenGL和C++)(第2版)學習筆記 09.天空和背景

天空和背景 對于 3D 場景,通常可以通過在遠處的地平線附近創造一些逼真的效果,來增強其真實感。我們可以采用天空盒、天空柱(Skydome)或天空穹(Skydome)等技術來模擬天空。 天空盒 天空盒(Sk…

【Leetcode 每日一題】1550. 存在連續三個奇數的數組

問題背景 給你一個整數數組 a r r arr arr,請你判斷數組中是否存在連續三個元素都是奇數的情況:如果存在,請返回 t r u e true true;否則,返回 f a l s e false false。 數據約束 1 ≤ a r r . l e n g t h ≤ 10…

面試題解析 | C++空類的默認成員函數(附生成條件與底層原理)

在C面試中,“空類默認生成哪些成員函數”是考察對象模型和編譯器行為的高頻題目。許多資料僅提及前4個函數,但完整的答案應包含6個核心函數,并結合C標準深入解析其生成規則與使用場景。 一、空類默認生成的6大成員函數 1. ?缺省構造函數? …

視頻編解碼學習7之視頻編碼簡介

視頻編碼技術發展歷程與主流編碼標準詳解 視頻編碼技術是現代數字媒體領域的核心技術之一,它通過高效的壓縮算法大幅減少了視頻數據的體積,使得視頻的存儲、傳輸和播放變得更加高效和經濟。從早期的H.261標準到最新的AV1和H.266/VVC,視頻編碼…

使用Stable Diffusion(SD)中,步數(Steps)指的是什么?該如何使用?

Ⅰ定義: 在Stable Diffusion(SD)中,步數(Steps) 指的是采樣過程中的迭代次數,也就是模型從純噪聲一步步“清晰化”圖像的次數。你可以理解為模型在畫這張圖時“潤色”的輪數。 Ⅱ步數的具體作…

消息隊列如何保證消息可靠性(kafka以及RabbitMQ)

目錄 RabbitMQ保證消息可靠性 生產者丟失消息 MQ丟失消息 消費端丟失了數據 Kakfa的消息可靠性 生產者的消息可靠性 Kakfa的消息可靠性 消費者的消息可靠性 RabbitMQ保證消息可靠性 生產者丟失消息 1.事務消息保證 生產者在發送消息之前,開啟事務消息隨后生…

如何查看項目是否支持最新 Android 16K Page Size 一文匯總

前幾天剛聊過 《Google 開始正式強制 Android 適配 16 K Page Size》 之后,被問到最多的問題是「怎么查看項目是否支持 16K Page Size」 ?其實有很多直接的方式,但是最難的是當你的項目有很多依賴時,怎么知道這個「不支持的動態庫…

HttpServletResponse的理解

HttpServletResponse 是 Java Servlet API 提供的一個接口 常用方法 方法用途setContentType(String type)設置響應內容類型(如 "application/json"、"text/html")setStatus(int sc)設置響應狀態碼(如 200、404&#x…

可靈 AI:開啟 AI 視頻創作新時代

在當今數字化浪潮中,人工智能(AI)技術正以前所未有的速度滲透到各個領域,尤其是在內容創作領域,AI 的應用正引發一場革命性的變革。可靈 AI 作為快手團隊精心打造的一款前沿 AI 視頻生成工具,宛如一顆璀璨的…

用 AltSnap 解鎖 Windows 窗口管理的“魔法”

你有沒有遇到過這樣的場景:電腦屏幕上堆滿了窗口,想快速調整它們的大小和位置,卻只能拖來拖去,費時又費力?或者你是個多任務狂魔,喜歡一邊寫代碼、一邊看文檔、一邊刷視頻,卻發現 Windows 自帶的…

深度策略梯度算法PPO

一、策略梯度核心思想和原理 從時序差分算法Q學習到深度Q網絡,這些算法都側重于學習和優化價值函數,屬于基于價值的強化學習算法(Value-based)。 1. 基于策略方法的主要思想(Policy-based) 基于價值類方…

【LaTeX】Word插入LaTeX行間公式如何編號和對齊

在 Word 文檔中插入公式,需要用到 LaTeX \LaTeX LATE?X 。但遺憾的是,Word 只支持部分 LaTeX \LaTeX LATE?X 語法,這就導致很多在 Markdown 能正常渲染的公式在 Word 中無法正常顯示。 “內嵌”和“顯示” 首先介紹一下 Word 的“內嵌”…

互聯網大廠Java面試實戰:Spring Boot到微服務的技術問答解析

💪🏻 1. Python基礎專欄,基礎知識一網打盡,9.9元買不了吃虧,買不了上當。 Python從入門到精通 😁 2. 畢業設計專欄,畢業季咱們不慌忙,幾百款畢業設計等你選。 ?? 3. Python爬蟲專欄…

spring boot3.0自定義校驗注解:文章狀態校驗示例

文章目錄 Spring Boot 自定義校驗注解:狀態校驗示例一、創建 State 注解步驟:1. 創建自定義注解:2. 實現校驗邏輯: 二、 實現自定義校驗步驟:1. 在實體類中使用自定義校驗注解 State:2. 添加 State 注解: 總…

無侵入式彈窗體驗_探索 Chrome 的 Close Watcher API

1. 引言 在網頁開發中,彈窗(Popup)是一種常見的交互方式,用于提示用戶進行操作、確認信息或展示關鍵內容。然而,傳統的 JavaScript 彈窗方法如 alert()、confirm() 和 prompt() 存在諸多問題,包括阻塞主線程、樣式不可定制等。 為了解決這些問題,Chrome 瀏覽器引入了 …

調出事件查看器界面的4種方法

方法1. 方法2. 方法3. 方法4.