探究HashMap線性不安全(二)——鏈表成環的詳細過程

来源:https://www.cnblogs.com/lonelyJay/archive/2018/09/29/9726187.html
-Advertisement-
Play Games

內容 ​ 網上很多資料都詳細地講解了HashMap底層的實現,但是講到HashMap的併發操作不是線性安全時,往往一筆帶過:在多個線程併發擴容時,會在執行transfer()方法轉移鍵值對時,造成鏈表成環,導致程式在執行get操作時形成死迴圈。 ​ 對於沒有研究過該過程的童鞋,很難費解這句話的含義。 ...


內容

​  網上很多資料都詳細地講解了HashMap底層的實現,但是講到HashMap的併發操作不是線性安全時,往往一筆帶過:在多個線程併發擴容時,會在執行transfer()方法轉移鍵值對時,造成鏈表成環,導致程式在執行get操作時形成死迴圈

​  對於沒有研究過該過程的童鞋,很難費解這句話的含義。下麵筆者分四個小節帶著大家共同研究一下JDK1.7和JDK1.8版本下HashMap的線性不安全是怎麼造成的,詳細探究鏈表成環的形成過程。如果對於HashMap底層的put、get操作不清楚,建議先學習參考1中的內容。

適合人群

​  Java進階

說明

  轉載請說明出處:探究HashMap線性不安全(二)——鏈表成環的詳細過程

 參考

​ 1、https://www.toutiao.com/i6544826418210013700/ HashMap底層數據結構原理

​ 2、https://www.toutiao.com/i6545790064104833539/ 為什麼HashMap非線程安全

​ 3、https://blog.csdn.net/qq_32182461/article/details/81152025 hashmap併發情況下的成環原因(筆者認為該文是一種誤解)

正文

​  本節將詳細探究HashMap擴容的鍵值對遷移過程,多線程併發執行transfer()方法是如何產生環形鏈表的。transfer()方法的代碼為:

 1 void transfer(Entry[] newTable, boolean rehash) {
 2     int newCapacity = newTable.length;
 3     //遍歷table數組中鍵值對鏈
 4     for (Entry<K,V> e : table) {
 5         //遍歷鍵值對e鏈上的所有鍵值對,當e指向null時結束
 6         while(null != e) {
 7             Entry<K,V> next = e.next;//斷點一
 8             //通常rehash為false,不會重新計算鍵值對key的hash值
 9             if (rehash) {
10                 e.hash = null == e.key ? 0 : hash(e.key);
11             }
12             //根據擴容後的table數組長度計算鍵值對的index
13             int i = indexFor(e.hash, newCapacity);
14             //頭插法,將後遍歷的鍵值對存到鏈條的頭部
15             e.next = newTable[i];
16             newTable[i] = e;
17             //鏈條中的下一個鍵值對繼續執行while迴圈。
18             e = next;
19         }
20     }
21 }

 情景:

​  兩個線程A、B同時向HashMap中寫入鍵值對,某個時刻HashMap已經到了Resize的臨界點。由於多線程的執行沒有必然的先後順序,存線上程A未完成擴容,而線程B又進行擴容的情況,即兩個線程都可能會執行擴容方法transfer()。此時兩個線程都會遍歷table數組中的鍵值對鏈,對於每個鏈執行while迴圈,遷移所有鍵值對。

1538206946728

  假定HashMap中table數組的初始長度為4

​  假如線程A和線程B都遍歷到index為2的鍵值鏈(即Entry3->Entry2->null這條鏈)。由於CPU時間片分配的不確定性,線程B執行到代碼中斷點一的位置後暫停,此時線程B中的e指向Entry3,e.next指向Entry2。而線程A繼續執行,完成了擴容操作。HashMap的數據結構為下圖所示。

1538212067957

​  重點!!:e和e.next為線程B中的引用變數,分別指向hashMap中的Entry3和Entry2。由於Entry3和Entry2是線程共用的,因此受線程A執行的影響,Entry3將指向null,Entry2指向Entry3

​  此時線程B中局部變數newTable的結構:

1538216168524

執行第一次迴圈:e為Entry3,e.next為Entry2(線程B暫停前e和e.next引用變數的指向)

 1 //遍歷e所在鏈上的所有鍵值對
 2 while(null != e) {
 3     //斷點1,此時線程B中的e為Entry3,e.next為Entry2
 4     Entry<K,V> next = e.next;
 5     //通常rehash為false,不會重新計算鍵值對key的hash值
 6     if (rehash) {
 7         e.hash = null == e.key ? 0 : hash(e.key);
 8     }
 9     //根據擴容後的table數組長度計算鍵值對的index
10     int i = indexFor(e.hash, newCapacity);
11     //頭插法
12     e.next = newTable[i];//將Entry3的next設置為null
13     newTable[i] = e;//將Entry3放置到線程B newTable下標為3的位置
14     //繼續處理Entry2
15     e = next;
16 }

 執行完第一次迴圈後線程B中局部變數newTable的結構:

1538210103080

執行完第一次迴圈後hashMap對象的結構:

1538212858020

執行第二次迴圈:e為Entry2,e.next為Entry3(受線程A影響,e和e.next引用變數的指向發生改變)

 1 //遍歷鍵值對e鏈上的所有鍵值對
 2 while(null != e) {
 3     //斷點1,e為Entry2,e.next為Entry3
 4     Entry<K,V> next = e.next;
 5     //通常rehash為false,不會重新計算鍵值對key的hash值
 6     if (rehash) {
 7         e.hash = null == e.key ? 0 : hash(e.key);
 8     }
 9     //由線程A的執行結果可知,Entry2的index也為3
10     int i = indexFor(e.hash, newCapacity);
11     //頭插法,
12     e.next = newTable[i];//將Entry2的next設置為Entry3
13     newTable[i] = e; //newTable[3]設置為Entry2
14     //另e等於Entry3,繼續執行while迴圈。
15     e = next;
16 }

執行完第二次迴圈後線程B中局部變數newTable的結構:

1538213198238

執行完第二次迴圈後hashMap對象的結構:

1538212858020[1]

執行第三次迴圈:e變為Entry3,e.next為null

 1 //遍歷鍵值對e鏈上的所有鍵值對
 2 while(null != e) {
 3     //斷點1,此時線程B中的e變為Entry3,e.next為null
 4     Entry<K,V> next = e.next;
 5     //通常rehash為false,不會重新計算鍵值對key的hash值
 6     if (rehash) {
 7         e.hash = null == e.key ? 0 : hash(e.key);
 8     }
 9     //由線程A的執行結果可知,Entry3的index為3
10     int i = indexFor(e.hash, newCapacity);
11     //頭插法
12     e.next = newTable[i];//將Entry3的next設置為當前鏈條的首個鍵值對Entry2
13     newTable[i] = e;//newTable[3]設置為Entry3
14     //另e=next=null,結束while迴圈。
15     e = next;
16 }

執行完第三次迴圈後線程B中局部變數newTable的結構:

1538213730420

執行完第三次迴圈後hashMap對象的結構:

1538216304142

​  至此,線程B因為改變了Entry3的next屬性,在hashMap對象中產生了環形鏈表。下一節,將探究環形鏈表是如何在hashMap查詢時產生死迴圈的。


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

-Advertisement-
Play Games
更多相關文章
  • Python環境搭建首先到官網(www.python.org)下載相應的安裝版本。主要分為windows和Linux兩種: 一、windows環境 下載地址:https://www.python.org/downloads/windows/ 1、下載好版本後按照正常提示安裝。 2、設置環境變數 py ...
  • django下載Excel,使用django-excel插件 由於目前的資料多是使用pandas或xlwt庫實現的。其實沒有那麼的麻煩,因為django有相對應的插件django-excel。 該插件是依賴於pyexcel庫寫的。不過,不用專門安裝pyexcel庫,因為在安裝django-excel ...
  • 環境 一、websocket協議 1. 先建立連接 wss://broadcastlv.chat.bilibili.com/sub 2. 發送登錄包 { "uid": 0表示未登錄,否則為用戶ID, "roomid": 房間ID, "protover": 1, "platform": "web", ...
  • 下麵的程式會發生崩潰: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 include <stdio.h> include <iostream> using namespace std; int main(void) { int p; int i = ...
  • 單變數:表達式、方程式、函數或者一元多項式等 數據:http://www.presidency.ucsb.edu/data/sourequests.php美國總統歷年在國情咨文中對國會提起的訴求數量 一、獲取數據 本次使用到的數據量並不多,不過還是按照常規思路,通過爬蟲獲取。 得到的數據: 二、繪製 ...
  • n1、下載windows版本的nginx安裝包 nginx官網,我使用的是穩定版的1.8.1 2、下載好的安裝包,找一個路徑進行解壓(註意:不要使用中文路徑);解壓之後nginx就安裝好了,嘻嘻window下安裝特別簡單,比linux簡單多了 3、然後就需要配置tomcat伺服器了,因為要模擬集群的 ...
  • 1. weak_ptr 介紹 std::weak_ptr 是一種智能指針,它對被 std::shared_ptr 管理的對象存在非擁有性("弱")引用。在訪問所引用的對象指針前必須先轉換為 std::shared_ptr。 主要用來表示臨時所有權,當某個對象存在時才需要被訪問。轉換為shared_p ...
  • jQuery runnoob網址: http://www.runoob.com/jquery/jquery-tutorial.html jQuery API手冊: http://www.runoob.com/manual/jquery/ jQuery筆記 筆記來源於: 傳智播客的黑馬程式員視頻筆記.... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...