深入剖析ThreadLocal使用場景、實現原理、設計思想

来源:https://www.cnblogs.com/caicaiJava/archive/2023/09/28/17736472.html
-Advertisement-
Play Games

前言 ThreadLocal可以用來存儲線程的本地數據,做到線程數據的隔離 ThreadLocal的使用不當可能會導致記憶體泄漏,排查記憶體泄漏的問題,不僅需要熟悉JVM、利用好各種分析工具還耗費人工 如果能明白其原理並正確使用,就不會導致各種意外發生 本文將從使用場景、實現原理、記憶體泄漏、設計思想等層 ...


前言

ThreadLocal可以用來存儲線程的本地數據,做到線程數據的隔離

ThreadLocal的使用不當可能會導致記憶體泄漏,排查記憶體泄漏的問題,不僅需要熟悉JVM、利用好各種分析工具還耗費人工

如果能明白其原理並正確使用,就不會導致各種意外發生

本文將從使用場景、實現原理、記憶體泄漏、設計思想等層面分析ThreadLocal,並順帶聊聊InheritableThreadLocal

ThreadLocal使用場景

什麼是上下文?

比如線程處理一個請求,請求會經過MVC流程,由於流程很長,會經歷很多方法,這些方法就可以叫上下文

ThreadLocal作用在上下文中存儲常用的數據、存儲會話信息、存儲線程本地變數等

比如使用攔截器在請求處理前,通過請求中的token獲取登錄用戶信息,將用戶信息存儲在ThreadLocal中,方便後續處理請求時從ThreadLocal中直接獲取用戶信息

如果線程會重覆利用,為了避免數據錯亂,使用完(攔截器處理後)應該刪除該數據

ThreadLocal 常用的方法有:set()get()remove()分別對應存儲、獲取和刪除

可以將ThreadLocal放在工具類中方便使用

public class ContextUtils {
    public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}

攔截器偽代碼

//執行前 存儲
public boolean postHandle(HttpServletRequest request)  {
    //解析token獲取用戶信息
	String token = request.getHeader("token");
	UserInfo userInfo = parseToken(token);   
	//存入
	ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
    
    return true;
}


//執行後 刪除
public void postHandle(HttpServletRequest request)  {
    ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}

使用時

//提交訂單
public void orderSubmit(){
    //獲取用戶信息
    UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
    //下單
    submit(userInfo);
    //刪除購物車
    removeCard(userInfo);
}

為了更好的使用ThreadLocal,我們應該瞭解其實現原理,避免使用不當造成意外發生

ThreadLocalMap

Thread 線程中有兩個欄位存儲ThreadLocal的內部類ThreadLocalMap

public class Thread implements Runnable {    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

threadLocals用於實現ThreadLocal

inheritableThreadLocals 用於實現InheritableThreadLocal (可繼承的ThreadLocal 後文再聊)

image.png

ThreadLocalMap 的實現是哈希表,其內部類Entry是哈希表的節點,由Entry數組實現哈希表 ThreadLocalMap

public class ThreadLocal<T> {
    //,,,
	static class ThreadLocalMap {
        //...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
	}
}

節點構造中的Key是ThreadLocal,Value是需要存儲的值

同時節點繼承弱引用,通過泛型和構造可以知道它將ThreadLocal設置為弱引用

不理解弱引用的同學可以查看這篇文章:深入淺出JVM(十四)之記憶體溢出、泄漏與引用 )

image.png

set

在存儲數據的方法中

image.png

獲取ThreadLocalMap,如果沒有則初始化ThreadLocalMap(懶載入)

public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取當前線程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
   
    if (map != null) {
        //添加數據
        map.set(this, value);
    } else {
        //沒有就初始化
        createMap(t, value);
    }
}

createMap

創建ThreadLocalMap賦值給當前線程的threadLocals

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

創建ThreadLocalMap,初始化長度為16的數組

	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化數組 16
        table = new Entry[INITIAL_CAPACITY];
        //獲取下標
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //構建節點
        table[i] = new Entry(firstKey, firstValue);
        //設置大小
        size = 1;
        //設置負載因數
        setThreshold(INITIAL_CAPACITY);
   }

ThreadLocalMap.set

通過哈希獲取下標,當發生哈希衝突時,遍歷哈希表(不再使用鏈地址法)直到位置上沒有節點再進行構建

遍歷期間如果有節點,則根據節點取出key進行比較,如果是則是覆蓋;如果節點沒有key說明該節點的ThreadLocal被回收(已過期),為了防止記憶體泄漏會清理節點

最後會檢查其他位置有沒有已過期的節點進行清理,並檢查擴容

private void set(ThreadLocal<?> key, Object value) {

    //獲取哈希表
    Entry[] tab = table;
    int len = tab.length;
    //獲取下標
    int i = key.threadLocalHashCode & (len-1);

    //遍歷 直到下標上沒有節點
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //獲取key
        ThreadLocal<?> k = e.get();
		//key如果存在則覆蓋
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key不存在 說明該ThreadLocal以及不再使用(GC回收),需要清理防止記憶體泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //構建節點
    tab[i] = new Entry(key, value);
    //計數
    int sz = ++size;
    //清理其他過期的槽,如果滿足條件進行擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

獲取哈希值時,使用哈希值自增的原子類獲取,步長則是每次自增的數量(也許是經過研究、測試的,儘量減少哈希衝突)

	//獲取哈希值
    private final int threadLocalHashCode = nextHashCode();
    //哈希值自增器
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    //增長步長
    private static final int HASH_INCREMENT = 0x61c88647;

    //獲取哈希值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextIndex是獲取下一個下標,超出上限時回到0

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

get

在獲取數據時

image.png

獲取當前線程的ThreadLocalMap,如果為空則初始化,否則獲取節點

	public T get() {
        //獲取當前線程
        Thread t = Thread.currentThread();
        //獲取線程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //獲取節點
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化(懶載入)
        return setInitialValue();
    }

在獲取節點時,先根據哈希值獲取到下標,再查看節點,比較key;如果匹配不上則說明key過期可能發生記憶體泄漏要去清理哈希表

		private Entry getEntry(ThreadLocal<?> key) {
            //獲取下標
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果匹配 則返回
            if (e != null && e.get() == key)
                return e;
            else
                //匹配不到 去清理
                return getEntryAfterMiss(key, i, e);
        }

記憶體泄漏

在設置、獲取數據的過程中,都會去判斷key是否過期,如果過期就清理

實際上ThreadLocal使用不當是會造成記憶體泄漏的

設計者為了避免使用不當導致的記憶體泄漏,在常用方法中儘量清理這些過期的ThreadLocal

前文說過節點繼承弱引用,在構造中設置key為弱引用(也就是ThreadLocal)

當ThreadLocal在任何地方都不被使用時,下次GC會將節點的key設置為空

如果value也不再使用,但是由於節點Entry(null,value)存在,就無法回收value,導致出現記憶體泄漏

image.png

因此使用完數據後,儘量使用remove進行刪除

並且設計者在set、get、remove等常用方法中都會檢查key為空的節點並刪除,避免記憶體泄漏

設計思想

為什麼要把entry中的key,也就是ThreadLocal設置成弱引用?

我們先想象一個場景:線程在我們的服務中經常重覆利用,而在某些場景下ThreadLocal並不長期使用

如果節點entry 的key、value都是強引用,一但不再使用ThreadLocal,那麼這個ThreadLocal還作為強引用存儲在節點中,那麼就無法回收,相當於發生記憶體泄漏

把ThreadLocal設置為弱引用後,這種場景下如果value也不再使用依舊會發生記憶體泄漏,因此在set、get、remove方法中都會區檢查刪除key為空的節點,避免記憶體泄漏

既然value可能無法回收,為什麼不把value也設置成弱引用?

由於value存儲的是線程隔離的數據,如果將value設置成弱引用,當外層也不使用value對應的對象時,它就沒有強引用了,再下次gc被回收,導致數據丟失

InheritableThreadLocal

InheritableThreadLocal 繼承 ThreadLocal 用於父子線程間的線程變數傳遞

	public void testInheritableThreadLocal(){
        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

        new Thread(()->{
            //main
            System.out.println(itl.get());
        }).start();
    }

前文說過線程中另一個ThreadLocalMap就是用於InheritableThreadLocal 的

在創建線程時,如果父線程中inheritableThreadLocals 不為空 則傳遞

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //....
    
        //如果父線程中inheritableThreadLocals 不為空 則傳遞
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    }

總結

ThreadLocal 用於隔離線程間的數據,可以存儲數據作用在上下文中,由於線程可能重覆利用,使用後需要刪除,避免出現數據混亂

Thread線程中存儲ThreadLocalMap,ThreadLocalMap是一個使用開放定址法解決哈希衝突的哈希表,其中節點存儲Key是ThreadLocal,Value存儲的是線程要存儲數據

節點繼承弱引用,並設置ThreadLocal為弱引用,這就導致當ThreadLocal不再使用時,下次GC會將其回收,此時Key為空,如果Value也不再使用,但是節點未刪除就會導致value被使用,從而導致記憶體泄漏

在ThreadLocal的set、get、remove等常用方法中,遍曆數組的同時還回去將過期的節點(key為空)進行刪除,避免記憶體泄漏

如果將ThreadLocal設置成強引用,當ThreadLocal不再使用時會發生記憶體泄漏;將ThreadLocal設置成弱引用時,雖然也可能發生記憶體泄漏,但可以在常用方法中檢查並清理這些數據;如果將value設置成弱引用,當外層不使用value時會發生數據丟失

InheritableThreadLocal繼承ThreadLocal ,用於父子線程間的ThreadLocal數據傳遞

最後(不要白嫖,一鍵三連求求拉~)

本篇文章被收入專欄 由點到線,由線到面,深入淺出構建Java併發編程知識體系,感興趣的同學可以持續關註喔

本篇文章筆記以及案例被收入 gitee-StudyJavagithub-StudyJava 感興趣的同學可以stat下持續關註喔~

案例地址:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關註、收藏支持一下~

關註菜菜,分享更多乾貨,公眾號:菜菜的後端私房菜

本文由博客一文多發平臺 OpenWrite 發佈!


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

-Advertisement-
Play Games
更多相關文章
  • MySQL InnoDB的索引統計信息在什麼時候更新呢? 或者說什麼事件會觸發InnoDB索引的統計信息更新呢?下麵結合參考資料When Does InnoDB Update the Index Statistics? (Doc ID 1463718.1)[1]簡單總結梳理一下(文中大部分知識點來自 ...
  • 本文分享自華為雲社區 《實戰指南,SpringBoot + Mybatis 如何對接多數據源》,作者:戰斧。 在我們開發一些具有綜合功能的項目時,往往會碰到一種情況,需要同時連接多個資料庫,這個時候就需要用到多數據源的設計。而Spring與Myabtis其實做了多數據源的適配,只需少許改動即可對接多 ...
  • 近日,天翼雲與神州信息完成神州信息銀行分散式核心系統與天翼雲4.0雲平臺及TeleDB資料庫的適配認證工作,標志著雙方聯合推出的“銀行核心業務系統聯合解決方案”生產環境投產成功。 ...
  • 趁著國慶前夕整了一個vite4結合react18搭建後臺管理模板,搭配上位元組團隊react組件庫ArcoDesign,整體操作功能非常絲滑。目前功能支持多種模板佈局、暗黑/亮色模式、國際化、許可權驗證、多級路由菜單、tabview標簽欄快捷菜單、全屏控制等功能。極簡非凡的佈局界面、高定製化模塊,用心打 ...
  • 針對改動範圍大、影響面廣的需求,我通常會問上線了最壞情況是什麼?應急預案是什麼?你帶開關了嗎?。當然開關也是有成本的,接下來本篇跟大家一起交流下高頻發佈支撐下的功能開關技術理論與實踐結合的點點滴滴。 ...
  • RPC,Remote Procedure Call 即遠程過程調用,與之相對的是本地服務調用,即LPC(Local Procedure Call)。本地服務調用比較常用,像我們應用內部程式(註意此處是程式而不是方法,程式包含方法)互相調用即為本地過程調用,而遠程過程調用是指在本地調取遠程過程進行使用... ...
  • 使用雙指針進行原地移除元素 題目描述 給定一個數組 nums 和一個值 val,需要將數組中所有等於 val 的元素原地刪除,並返回刪除後數組的新長度。 要求: 不使用額外的數組空間 只能使用 O(1) 額外空間 數組中超過新長度後面的元素可以忽略 示例 1: 輸入:nums = [3,2,2,3] ...
  • 在Python中,字元串可以用單引號或雙引號括起來。'hello' 與 "hello" 是相同的。您可以使用print()函數顯示字元串文字: 示例: print("Hello") print('Hello') 將字元串分配給變數是通過變數名後跟等號和字元串完成的: 示例 a = "Hello" p ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...