Java基礎(1)——ThreadLocal

来源:https://www.cnblogs.com/stepfortune/archive/2022/05/25/16311274.html
-Advertisement-
Play Games

1. Java基礎(1)——ThreadLocal 1.1. ThreadLocal ThreadLocal是一個泛型類,當我們在一個類中聲明一個欄位:private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();時,這時候,即使不同的線 ...


1. Java基礎(1)——ThreadLocal

1.1. ThreadLocal

ThreadLocal是一個泛型類,當我們在一個類中聲明一個欄位:private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();時,這時候,即使不同的線程持有了該類的同一個實例,那麼它們在訪問該實例的threadLocalFoo的時候訪問的是不同的Foo對象,這些Foo對象和這些線程是一一對應的關係,並被這些線程所私有,因此每個線程不需要對自己從threadLocalFoo獲得的Foo實例進行加鎖(加鎖也沒用啊),這種無鎖化的設計提高了並行能力,但註意ThreadLocal並不是萬能的,有些場景可以使用ThreadLocal(比如Spring中的事務),但有些場景它的語義就是必須對同一個對象實例進行加鎖後獨占地訪問,比如單例模式,這種ThreadLocal就起不了作用了。

當然ThreadLocal還提供了initialValue這個protected方法,用來創建聲明的泛型類型對象,因此我們還可以以下麵這種方式來聲明一個thread local:

        ThreadLocal<Foo> threadLocal = new ThreadLocal<Foo>(){

            @Override
            protected Foo initialValue() {
                return new Foo();
            }
        };

同時ThreadLocal還提供了一個withInitial靜態方法,該方法接收一個相同泛型類型的Supplier,返回ThreadLocal。

Java的每個Thread實例中,都有一個ThreadLocalMap類型的實例欄位,它存放了該線程所用到過的所有ThreadLocal式樣的實例對象,比如,有個類中聲明瞭這個欄位private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();,雖然它的一個實例被多個線程持有,但這些線程不一定都訪問過這個實例的threadLocalFoo欄位,只有訪問過這個欄位的Thread,它的thread local map中才會存Foo對象(以Entry的方式存,key為該ThreadLocal實例(共用),value為每個線程自己持有的Foo對象(私有))。

註意,我們使用ThreadLocal的是因為有些對象每個線程都可以持有一份,然後我們才使用ThreadLocal來避免同一個對象的實例方法的併發操作,但這樣的話我們要謹防ThreadLocal的退化:如果使用它的時候,用之前都是set,之後就remove,那麼相當於每訪問一次ThreadLocal都要創建出一個新的對象出來,這樣發揮不出ThreadLocal節省對象數量的作用。ThreadLocal一般被聲明為static欄位。

1.1.1. get方法

如果當前的Thread中的thread local map欄位不空,並且其中存的有對應的對象,那麼返回。

如果thread local map欄位不空,但是沒有存對應的對象,那麼使用initialValue創建對象,然後將它和該ThreadLocal實例,打包成Entry放入當前的thread local map中,返回創建的對象。

如果thread local map欄位為空,那麼首先創建對象,然後創建該線程的thread local map,然後再存Entry,再返回創建的對象。

總而言之呢,get方法就是說返回的對象都必須從當前線程的thread local map中取,thread local map沒創建,就創建thread local map,創建了但裡面沒有需要的對象,那麼就創建對象並將其塞進去,反正必須從thread local map中拿就對了。

setInitialValue方法:

createMap方法:

1.1.2. set方法

Set方法,將傳入的對象設置到當前的線程的thread local map中,註意,Entry的Key為set方法所在的ThreadLocal實例。

還是一樣,沒有thread local map就創建thread local map,反正必須塞入當前的thread local map中。

1.1.3. remove方法

remove方法,就是獲取當前線程的thread local map,如果它不空的話,就移除key為remove方法所在的ThreadLocal的Entry(不同的ThreadLocal實例對應著不同的Entry,而同一個ThreadLocal實例在一個thread local map中最多存一個,但是可以存在多個thread local map中)。

1.2. ThreadLocalMap(ThreadLocal內部類)

ThreadLocalMap是ThreadLocal機制的關鍵,它不被使用ThreadLocal的用戶所感知,它是ThreadLocal的靜態內部類,它的所有方法都是private方法,並且該類的可見性是包可見的,因此ThreadLocalMap類中的所有方法都只能被ThreadLocal的方法調用。

ThreadLocalMap的底層存儲是ThreadLocalMap.Entry類型的數組,它的碰撞處理策略不是HashMap的開鏈法(開散列方法),而是線性探測法(linear probing,屬於閉散列方法,常見的其他閉散列方法還有:平方探測法、雙散列法)。這個線性探測法就是說:

  • 在put的時候,先根據key的hash值定位到在數組中的槽位,如果對應的位置沒有Entry,那麼就可以把當前的鍵值對放入這裡,反之,如果該位置已經被占用的話,那麼需要獲取該位置的下一個位置(如果當前位置為數組最後一個位置,那麼下一個位置為0),直到找到空位為止

  • 在get的時候,根據key找Entry,也是首先先根據key的hash值定位到在數組中的槽位,如果這個槽位空著,那麼說明當前map沒有存這個key,如果這個槽位不空,那麼還要檢查Entry中的key是否就是當前的key,如果不是的話還要繼續向後探測,直到遇到了空位或者遇到了key為當前key的Entry。

  • 在remove的時候,首先跟get一樣,找到key對應的Entry,然後將其移除,但是移除完之後,如果該槽位後面連續的槽位也都被占用了,那麼還要對這些槽位中的Entry再進行位置修正。

和Map介面中的Entry不一樣,ThreadLocalMap.Entry聲明為:

ThreadLocalMap.Entry是一個對ThreadLocal對象的弱引用,也就是說,雖然該Entry會持有ThreadLocal對象,但是並不會影響該ThreadLocal對象的GC,而這個弱引用對象Entry本身是個尋常的Java對象,它還持有了ThreadLocal的泛型類型的對象(比如上面例子中的Foo),這個持有關係是強引用,只有當ThreadLocalMap的底層數組不再持有這個Entry時,該Entry才會被GC。因此,也就是說,如果ThreadLocalMap如果不做特殊處理的話,那麼即使是ThreadLocal實例都被GC了,但是它們對應的Entry依舊無法被GC,導致實際使用的泛型類型對象也無法被GC,只是這些Entry引用的ThreadLocal變成null了,這個問題其實就是記憶體泄露

為瞭解決這個記憶體泄露問題,ThreadLocalMap線上性探測操作中,如果發現了持有的thread local已經被GC的Entry(Stale Entry),那麼就不再持有這個Entry,使得這個Entry可以被GC,但是即使這樣依然無法完全保證stale entry都能及時的被清理,這個殘留的問題就是偽記憶體泄露問題

這個偽記憶體泄露問題一般存在於線程池的場景下,因為如果線程本身被銷毀,那麼thread local map也會銷毀,也不存在什麼泄露問題。

為瞭解決這個偽記憶體泄露問題,我們作用應用程式的開發者,在使用到threadlocal時,如果我們不再需要它時,那麼就要手動進行remove操作,使得對應的Entry可以被GC。

這個Entry數組初始容量為16,threshold為當前數組長度的三分之二(hard code),每次向Thread local map放入entry之後,會檢查更新後的size(數組中的Entry數量)是否達到了threshold,如果達到了,那麼就需要進行擴容,擴容的邏輯是,先把所有stale entry清理後,判斷清理的數量是否達到了四分之一threshold,如果是,那麼說明當前thread local map只是因為stale entry太多的緣故導致的容量緊張,就只需執行清理動作,而不用將底層數組容量翻倍併進行entry的遷移,這個策略的目的:

  1. 數組容量翻倍本身占用空間,並且擴容時搬運entry的操作相對相不擴容清理stale entry的操作來說開銷更大。

  2. 更好的去抑制上面講的偽記憶體泄露問題。

註意,thread local map底層的Entry數組只會擴容,不會縮容。

1.2.1. 構造函數

1.2.2. getEntry方法

getEntryAfterMiss

getEntryAfterMiss就是get操作的線性探測步驟。

expungeStaleEntry:

這個expungeStaleEntry就是說呢,需要刪除那些Stale的Entry(已經被GC後的ThreadLocal實例對應的Entry)。

它不止刪除給定stale位置的entry,它還有線性探測該位置之後被連續占用的位置的entry。在這些entry中,對於不是stale的,我們需要把它們挪到更正後的位置上,對於是stale的,將其刪除。

expungeStaleEntries:

expungeStaleEntries方法就是遍曆數組中的所有Entry,檢查是否stale,如果stale,那麼調用expungeStaleEntry來刪除並調整。

1.2.3. set方法

Set方法往thread local map中添加一個Entry。

如果該Entry未經線性探測時的位置未被占用,那麼直接占用,更新size計數,並且從該位置嘗試清理一些stale entry(見cleanSomeSlots方法)。如果清理成功,那麼此時size鐵定沒有超出threshold(因為此時至少清理了一個Entry,而set方法一次只set一個,並且初始情況下size小於threshold)。如果沒有清理到到,那麼就判斷更新後的size是否超過了threshold,如果超過了,那麼要擴容。

如果原始位置被占用了,那麼就需要通過線性探測,探測之後的位置,在探測過程中:

  • 如果發現已經有給定的Key的Entry了,那麼直接替換value就完事了。

  • 如果沒有發現stale entry,那麼就將遇到的第一個空位用來放置該Entry,然後完事,此時同樣需要像上面一樣嘗試清理stale entry,如果清理失敗看需不需要擴容等。

  • 如果在探測中發現了stale entry,那麼就進行替換操作,註意這個替換操作很複雜,見replaceStaleEntry方法。

replaceStaleEntry:

前兩個參數是需要放置的Entry的信息,最後一個參數是stale entry的位置。

首先是向前探測,因為給的stale entry的位置可能是處於一個連續被占用段的中間,因此來向前探測,來找到該連續占用段的第一個stale位置。

然後再從給定的stale位置向後探測,在這個向後探測的過程中:

  • 如果遇到了跟傳入key對應的Entry,那麼就將該Entry給挪到傳入的stale位置。如果上一步向前探測時沒有找到stale entry,那麼就從當前的位置向後回收連續占用段的stale entry;如果向前探測時找到了的話,就從這個找到的位置向後回收本連續占用段的stale entry。

  • 如果沒有遇到該key對應的Entry,並且之前向前探測的時候也沒有找到當前連續占用段的第一個stale位置,那麼就需要在這個向後探測從保存第一個stale entry的位置,探測結束後將傳入的stale位置放入entry,然後從這個向後探測過程中保存的stale位置開始向後回收所在連續占用段的stale entry。

上面兩種情況結束後,如果它們expungeStaleEntry的開始位置不是傳入的stale位置,那麼在這個expungeStaleEntry操作的結束位置(這個結束位置是一個空位)的下一位置開始向後嘗試回收一些stale entry,見cleanSomeSlots方法。

cleanSomeSlots:

這個方法的作用是說從給定position(不包含該position)開始向後找stale entry,如果連續找了 log(n) 個位置都不是stale entry,那麼就結束,反之如果找到一個stale entry的話,那麼需要再重新向後看 log(len) 個位置。

註意,這個方法在set方法、replaceStaleEntry方法中的末尾都有調用,區別在於,set方法中調用cleanSomeSlots時設置初始初始向後看的位置數目為log(size),而replaceStaleEntry設置的是log(len)

rehash:

先把所有stale entry清理後,判斷清理的數量是否達到了四分之一threshold,如果是,那麼說明當前thread local map只是因為stale entry太多的緣故導致的容量緊張,就只需執行清理動作,而不用將底層數組容量翻倍併進行entry的遷移。

1.2.4. remove方法

1.3. ThreadLocal記憶體泄露

記憶體泄露(Memory Leak)指由於對象永遠無法被垃圾回收導致其占用的Java虛擬機記憶體無法被釋放。持續的記憶體泄露會導致Java虛擬機可用記憶體主鍵減少,並最終可能導致Java記憶體溢出(OOM),直到Java虛擬機宕機。

偽記憶體泄露(Memory Psedo-leak)類似於記憶體泄露,偽記憶體泄露中對象占用的記憶體在其不再被使用的相當長時間內仍然無法回收,甚至永遠無法回收。就是說,偽記憶體泄露的對象,理論上將是可以被回收的,但是這個等待回收的時間太長了。

談及ThreadLocal map的時候,我們談到了,當使用threadlocal任務不進行remove操作,並且任務又線上程池中運行時,有偽記憶體泄露的風險,這個風險被thread local map本身的實現抑制了,但是仍然存在,解決的辦法就是即使使用remove操作。

此外還有一種更加嚴重的記憶體泄露:每個線程實例持有thread local map,然後間接持有了線程特有對象(thread local的泛型類型),在Tomcat環境下,Web應用(打包成WAR)自身定義的類由類載入器WebAppClassLoader負責載入, JDK的標準類由類載入器StandardClassLoader負責載入。不管類每個類被哪個載入器載入,它都持有了載入它的載入器的引用,除了最特殊的那個。對於WebAppClassLoader來說,它還會持有它載入過的所有class的引用,這樣就導致,如果如果某個由WebAppClassLoader載入的類型(假設為ThreadLocalMemoryLeak)有個靜態的ThreadLocal欄位(threadLocalFoo),那麼該線程特有對象(foo對象)會持有該對象的Class對象(Foo.class),Foo類型會持有WebAppClassLoader,WebAppClassLoader又會持有ThreadLocalMemoryLeak的Class對象,這個Class對象又持有了threadLocalFoo這個靜態欄位,也就是說,foo對象這個線程特有對象,最終又反過來持有ThreadLocal實例了,這就導致,如果不及時remove的話,那麼thread local map中的Entry永遠不會stale,即使這個Web app不運行了,但是Tomcat容器還在運行的話,由於底層的這些線程不會被銷毀,因此thread local就產生了記憶體泄露,更進一步講Foo類的Class對象、ThreadLocalMemoryLeak的Class對象,以及它們的靜態變數所引用的所有對象,都無法被回收。當然Tomcat提供了一套記憶體泄露的檢查機制以及一定程度的自動規避,但我們不要依賴這個機制。為瞭解決這個問題,我們要及時remove。

作者: 邁吉

出處: https://www.cnblogs.com/stepfortune/

關於作者:邁吉

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接 如有問題, 可郵件([email protected])咨詢.


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

-Advertisement-
Play Games
更多相關文章
  • 2022年4月28日,在華為摺疊旗艦及全場景新品發佈上,華為Mate Xs 2摺疊屏手機搭載由HMS Core定位服務(Location Kit)提供的3D精準室內定位技術,為用戶提供了“店鋪級”定位能力。目前,該能力已經覆蓋國內160+城市、1300+核心商圈、20+大型交通樞紐等大中型公共建築, ...
  • 大家好,我是半夏👴,一個剛剛開始寫文的沙雕程式員.如果喜歡我的文章,可以關註➕ 點贊 👍 加我微信:frontendpicker,一起學習交流前端,成為更優秀的工程師~關註公眾號:搞前端的半夏,瞭解更多前端知識! 點我探索新世界! 原文鏈接 ==>http://sylblog.xin/archi ...
  • 手動搭建一個webpack+Vue項目,掌握相關loader的安裝與使用,包括css-loader、style-loader、vue-loader、url-loader、sass-loader等,熟悉webpack的配置、文件的打包,以及路由的配置及使用。 ...
  • 主題 Theme | Vitesse Theme Font | Input Mono、Fira Code、方正聚珍新仿簡體(下載方正官方正版字體,使用 FontCreator 加粗) File Icons | Gruvbox Material Icon Theme Product Icons | C ...
  • 1. 內置指令 v-show : 說明:根據條件展示元素,true展示元素,false隱藏元素 版本:vue2,vue3 <template> <div> <button v-show="isShow"></button> </div> </template> <script> export def ...
  • 本文記錄如何在 Vue2 環境下儘量使用 Vue3 的 Composition-api 並配合 Vuetify2 使用 ...
  • 切麵:公共的,通用的,重覆的功能稱為切麵,面向切麵編程就是將切麵提取出來,單獨開發,在需要調用的方法中通過動態代理的方式進行織入 ...
  • 1.微服務架構是什麼? 由很多個微服務來組成,並且每個服務與服務有自己獨立的資料庫,服務與服務之間是通過輕量級的協議(比如:rpc,http等)來調用,每個服務是獨立的,自治的能夠獨立的發佈和部署。如下圖: 本質上來講,微服務只是一種架構風格。架構風格如何採取,那麼就需要涉及到架構特征這個概念。而架 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...