Java ThreadLocal 線程本地存儲(chǔ)工具思路詳解
ThreadLocal 詳解:Java 線程本地存儲(chǔ)工具
一、介紹
ThreadLocal 是 Java 提供的線程本地存儲(chǔ)(Thread-Local Storage, TLS)工具類,核心作用是為每個(gè)線程創(chuàng)建獨(dú)立的變量副本,讓線程操作自己獨(dú)有的數(shù)據(jù),實(shí)現(xiàn)線程間狀態(tài)隔離,避免多線程共享變量的競(jìng)爭問題。
二、定位
1. 思路
多線程環(huán)境中,共享變量(如靜態(tài)變量、成員變量)會(huì)引發(fā)線程安全問題(需加鎖 synchronized 或用并發(fā)容器),但加鎖會(huì)導(dǎo)致性能損耗。而 ThreadLocal 換了一種思路:不共享變量,給每個(gè)線程分配獨(dú)立副本,從根源避免競(jìng)爭。
2. 特性
- 線程隔離:每個(gè)線程的
ThreadLocal變量副本完全獨(dú)立,線程 A 修改自己的副本不會(huì)影響線程 B 的副本; - 懶初始化:變量副本默認(rèn)不會(huì)主動(dòng)創(chuàng)建,僅在線程首次訪問時(shí)初始化;
- 全局訪問:通過
ThreadLocal實(shí)例可在線程的任意方法、任意層級(jí)中訪問當(dāng)前線程的副本(無需參數(shù)傳遞)。
3.原理
ThreadLocal 的線程隔離特性,依賴 Java 中 Thread 類的內(nèi)部結(jié)構(gòu),核心是 Thread 與 ThreadLocalMap 的關(guān)聯(lián):
Thread類:每個(gè)Thread實(shí)例內(nèi)部都持有一個(gè)ThreadLocalMap成員變量(哈希表),專門存儲(chǔ)當(dāng)前線程的「ThreadLocal- 變量副本」映射;ThreadLocalMap:ThreadLocal的內(nèi)部靜態(tài)類,本質(zhì)是哈希表(解決哈希沖突用「開放地址法」,而非HashMap的鏈表 / 紅黑樹),鍵是ThreadLocal實(shí)例(弱引用),值是線程的變量副本(強(qiáng)引用);- 弱引用設(shè)計(jì):
ThreadLocalMap的鍵(ThreadLocal)是弱引用(WeakReference),目的是:當(dāng)ThreadLocal實(shí)例本身被回收時(shí)(如不再有強(qiáng)引用指向它),避免因哈希表持有強(qiáng)引用導(dǎo)致ThreadLocal無法 GC。
簡單舉出一個(gè)例子:
當(dāng)線程調(diào)用 threadLocal.get() 時(shí),底層執(zhí)行步驟:
- 獲取當(dāng)前線程:
Thread currentThread = Thread.currentThread(); - 從當(dāng)前線程中獲取
ThreadLocalMap:ThreadLocalMap map = currentThread.threadLocals; - 若
map存在且包含當(dāng)前ThreadLocal對(duì)應(yīng)的鍵,則直接返回對(duì)應(yīng)的變量副本(值); - 若map不存在,或無對(duì)應(yīng)鍵(線程首次訪問):
- 執(zhí)行初始化邏輯(
withInitial的 lambda 或initialValue()方法)創(chuàng)建變量副本; - 初始化當(dāng)前線程的
ThreadLocalMap,并將「ThreadLocal- 副本」映射存入 map;
- 執(zhí)行初始化邏輯(
- 返回變量副本。
線程1 → Thread.threadLocals(ThreadLocalMap)→ { ThreadLocal實(shí)例: 線程1的變量副本 }
線程2 → Thread.threadLocals(ThreadLocalMap)→ { ThreadLocal實(shí)例: 線程2的變量副本 }
線程3 → Thread.threadLocals(ThreadLocalMap)→ { ThreadLocal實(shí)例: 線程3的變量副本 }多個(gè)線程共享同一個(gè) ThreadLocal 實(shí)例,但各自的 ThreadLocalMap 獨(dú)立,副本互不干擾。
4.補(bǔ)充一下變量副本的概念:
變量副本(也叫「副本變量」「拷貝實(shí)例」),本質(zhì)是 原變量(或?qū)ο螅┑囊环莳?dú)立拷貝—— 它和原變量的「數(shù)據(jù)內(nèi)容初始一致」,但擁有獨(dú)立的內(nèi)存空間,后續(xù)對(duì)副本的修改不會(huì)影響原變量,反之亦然。
假設(shè)你有一份「原始合同」(對(duì)應(yīng)「原變量」):
- 你給同事復(fù)印了一份(對(duì)應(yīng)「創(chuàng)建副本」):兩份文件內(nèi)容完全一樣;
- 同事在自己的復(fù)印件上修改了條款(對(duì)應(yīng)「修改副本」):你的原始合同不受任何影響;
- 你在原始合同上補(bǔ)充了內(nèi)容(對(duì)應(yīng)「修改原變量」):同事的復(fù)印件也不會(huì)同步變化;
- 同事弄丟了自己的復(fù)印件(對(duì)應(yīng)「銷毀副本」):你的原始合同依然存在。
副本和原變量相互獨(dú)立,修改、銷毀互不干擾。
- 引用傳遞:多個(gè)線程持有同一個(gè)對(duì)象的引用(指向同一塊內(nèi)存),修改會(huì)相互影響(線程不安全);
- 副本傳遞:多個(gè)線程持有不同對(duì)象的引用(指向不同內(nèi)存),修改互不影響(線程安全)。
ThreadLocal 存儲(chǔ)的是「對(duì)象級(jí)別的副本」—— 每個(gè)線程拿到的是「同一個(gè)類的新實(shí)例」(而非同一個(gè)對(duì)象的引用),本質(zhì)是「對(duì)象的深拷貝 / 新實(shí)例化」,擁有獨(dú)立的內(nèi)存空間,還有另一種就是普通的值傳遞。
三、用法
ThreadLocal 的 API 極簡,核心只有 4 個(gè)方法,結(jié)合 Java 8+ 的簡化用法:
1. 初始化:創(chuàng)建ThreadLocal實(shí)例
有兩種初始化方式,推薦 Java 8+ 的 withInitial(函數(shù)式接口,代碼更簡潔):
// 方式1:Java 8+ 推薦(懶初始化,線程首次get()時(shí)執(zhí)行)
ThreadLocal<GlobalContext> threadLocal = ThreadLocal.withInitial(() -> {
GlobalContext context = new GlobalContext();
context.user = new SessionUser(); // 初始化副本數(shù)據(jù)
return context;
});
// 方式2:Java 8 前(重寫 initialValue() 方法)
ThreadLocal<GlobalContext> threadLocal = new ThreadLocal<GlobalContext>() {
@Override
protected GlobalContext initialValue() {
GlobalContext context = new GlobalContext();
context.user = new SessionUser();
return context;
}
};2. 獲取副本:get()
獲取當(dāng)前線程的變量副本,首次調(diào)用會(huì)觸發(fā)初始化:
GlobalContext context = threadLocal.get(); // 線程獨(dú)有的副本
3. 設(shè)置副本:set(T value)
主動(dòng)給當(dāng)前線程設(shè)置變量副本(覆蓋默認(rèn)初始化的副本):
GlobalContext customContext = new GlobalContext();
customContext.user = new SessionUser("admin", "管理員");
threadLocal.set(customContext); // 替換當(dāng)前線程的副本
4. 清除副本:remove()
刪除當(dāng)前線程的變量副本(解決內(nèi)存泄漏的關(guān)鍵):
threadLocal.remove(); // 線程使用完畢后必須調(diào)用!
補(bǔ)充一下內(nèi)存泄漏:
內(nèi)存泄漏(Memory Leak):程序中不再使用的對(duì)象,因?yàn)楸诲e(cuò)誤地持有了 “無法釋放的引用”,導(dǎo)致垃圾回收器(GC)不能回收它,最終這些對(duì)象占滿內(nèi)存,引發(fā)程序卡頓、OOM(內(nèi)存溢出)崩潰。
簡單舉個(gè)例子:
你買了一箱水果(對(duì)應(yīng) “對(duì)象”),吃完后箱子沒用了(對(duì)應(yīng) “對(duì)象不再被使用”),但你一直把箱子鎖在柜子里(對(duì)應(yīng) “被無效引用持有”),柜子空間被占著,后續(xù)再買東西就沒地方放,最后柜子徹底堆滿(對(duì)應(yīng) “內(nèi)存耗盡”)。
5. 實(shí)用封裝(實(shí)際開發(fā)常用)
通常會(huì)將 ThreadLocal 封裝為靜態(tài)工具類,方便全局訪問和統(tǒng)一清理:
public class ContextHolder {
// 私有靜態(tài) ThreadLocal 實(shí)例(全局唯一)
private static final ThreadLocal<GlobalContext> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
GlobalContext context = new GlobalContext();
context.user = new SessionUser();
return context;
});
// 靜態(tài)方法:獲取當(dāng)前線程上下文
public static GlobalContext getContext() {
return THREAD_LOCAL.get();
}
// 靜態(tài)方法:設(shè)置用戶信息(示例)
public static void setUser(SessionUser user) {
getContext().user = user;
}
// 靜態(tài)方法:清除上下文(必須調(diào)用?。?
public static void clear() {
THREAD_LOCAL.remove();
}
}四、典型使用場(chǎng)景
ThreadLocal 最適合「線程級(jí)別的上下文傳遞」,無需層層傳遞參數(shù),常見場(chǎng)景:
Web 應(yīng)用:請(qǐng)求上下文傳遞
- 存儲(chǔ)當(dāng)前 HTTP 請(qǐng)求的用戶信息(登錄狀態(tài)、權(quán)限)、請(qǐng)求 ID(日志追蹤)、Token 等;
- 貫穿鏈路:Controller → Service → DAO,無需在每個(gè)方法參數(shù)中顯式聲明上下文。
// Spring MVC 攔截器示例:請(qǐng)求開始時(shí)設(shè)置上下文,結(jié)束時(shí)清除
public class ContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 從請(qǐng)求頭獲取用戶信息,設(shè)置到 ThreadLocal
SessionUser user = new SessionUser(request.getHeader("userId"));
ContextHolder.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 請(qǐng)求結(jié)束,清除上下文(避免內(nèi)存泄漏)
ContextHolder.clear();
}
}多線程任務(wù):線程池上下文隔離
- 線程池中的核心線程長期存活,每個(gè)任務(wù)線程需要獨(dú)立的配置(如數(shù)據(jù)庫連接、日志標(biāo)識(shí));
- 注意:任務(wù)執(zhí)行完畢后必須調(diào)用
remove(),否則核心線程會(huì)持有副本導(dǎo)致內(nèi)存泄漏。
框架底層:狀態(tài)隔離
- Spring 事務(wù)管理:
TransactionSynchronizationManager用ThreadLocal存儲(chǔ)當(dāng)前線程的事務(wù)狀態(tài); - MyBatis:用
ThreadLocal存儲(chǔ)當(dāng)前線程的SqlSession(數(shù)據(jù)庫會(huì)話)。
五、注意
1. 內(nèi)存泄漏風(fēng)險(xiǎn)
為什么會(huì)內(nèi)存泄漏?
ThreadLocalMap的鍵(ThreadLocal)是弱引用:當(dāng)ThreadLocal實(shí)例被回收(如工具類被卸載),鍵會(huì)變成null;- 但
ThreadLocalMap的值(變量副本)是強(qiáng)引用:若線程長期存活(如線程池核心線程),null鍵對(duì)應(yīng)的 value 無法被 GC,導(dǎo)致內(nèi)存泄漏。
解決方案
- 線程使用完畢后,主動(dòng)調(diào)用
remove():刪除ThreadLocalMap中的 value,是最穩(wěn)妥的方式; - 避免使用
static ThreadLocal長期持有強(qiáng)引用(若必須用,務(wù)必在合適時(shí)機(jī)remove()); - 不建議用「弱引用包裝 value」(易導(dǎo)致空指針,治標(biāo)不治本)。
2. 線程復(fù)用場(chǎng)景的坑(線程池)
線程池中的線程會(huì)被復(fù)用(如核心線程),若上一個(gè)任務(wù)未調(diào)用 remove(),下一個(gè)任務(wù)會(huì)復(fù)用上一個(gè)任務(wù)的變量副本,導(dǎo)致數(shù)據(jù)污染:
// 錯(cuò)誤示例:線程池任務(wù)未清除 ThreadLocal
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(() -> {
ContextHolder.getContext().user.setUserId("1001"); // 任務(wù)1設(shè)置用戶1001
// 未調(diào)用 ContextHolder.clear()!
});
executor.submit(() -> {
String userId = ContextHolder.getContext().user.getUserId();
System.out.println(userId); // 輸出 1001(數(shù)據(jù)污染,預(yù)期應(yīng)是默認(rèn)值)
});解決:任務(wù)執(zhí)行完畢后必須調(diào)用 remove(),或在任務(wù)開始時(shí)主動(dòng) set() 覆蓋舊值。
3. 父子線程共享問題
ThreadLocal 不支持父子線程共享副本(父線程的副本,子線程無法直接獲?。?。若需要父子線程共享,需使用 InheritableThreadLocal(繼承自 ThreadLocal):
// 父子線程共享示例
InheritableThreadLocal<GlobalContext> inheritableTl = new InheritableThreadLocal<>();
inheritableTl.set(new GlobalContext(new SessionUser("父線程用戶")));
new Thread(() -> {
GlobalContext context = inheritableTl.get();
System.out.println(context.user.getUserId()); // 輸出 "父線程用戶"(子線程繼承父線程副本)
}).start();注意:InheritableThreadLocal 僅在子線程創(chuàng)建時(shí)復(fù)制父線程的副本,子線程創(chuàng)建后父線程修改副本,子線程不會(huì)同步更新。
4. 線程安全的邊界
ThreadLocal 僅保證「變量副本的線程隔離」,若副本本身是線程共享對(duì)象(如靜態(tài)可變對(duì)象),仍會(huì)有線程安全問題:
// 錯(cuò)誤示例:副本是共享對(duì)象
static class GlobalContext {
public static SessionUser sharedUser; // 靜態(tài)變量(線程共享)
}
ThreadLocal<GlobalContext> threadLocal = ThreadLocal.withInitial(GlobalContext::new);
// 線程1修改靜態(tài)變量,線程2會(huì)受影響
new Thread(() -> threadLocal.get().sharedUser = new SessionUser("1001")).start();
new Thread(() -> System.out.println(threadLocal.get().sharedUser.getUserId())).start(); // 可能輸出 1001六、補(bǔ)充:
ThreadLocal 與 synchronized / 并發(fā)容器的區(qū)別
- ThreadLocal:「不共享,各用各的」—— 給每個(gè)線程分配獨(dú)立變量副本,從根源避免競(jìng)爭;
- synchronized:「共享但串行化」—— 通過互斥鎖限制線程并發(fā)訪問,同一時(shí)間僅一個(gè)線程操作共享資源;
- 并發(fā)容器(如
ConcurrentHashMap、CopyOnWriteArrayList):「共享且高效并發(fā)」—— 內(nèi)部封裝鎖 / 無鎖算法,提供線程安全的集合操作,無需手動(dòng)加鎖。
| 對(duì)比維度 | ThreadLocal | synchronized | 并發(fā)容器(如 ConcurrentHashMap) |
|---|---|---|---|
| 核心設(shè)計(jì)思路 | 線程隔離:每個(gè)線程持獨(dú)立副本,無共享 | 互斥同步:串行化訪問共享資源 | 安全封裝:內(nèi)部集成鎖 / 無鎖算法,支持并發(fā)訪問共享集合 |
| 線程安全保障方式 | 天然安全(副本獨(dú)立,無競(jìng)爭) | 鎖阻塞:未獲取鎖的線程進(jìn)入 BLOCKED 狀態(tài) | 分段鎖 / 無鎖 / CAS:減少鎖競(jìng)爭,支持多線程并行操作 |
| 共享性 | 線程間數(shù)據(jù)不共享(副本隔離) | 線程間共享同一資源 | 線程間共享同一集合資源 |
| 是否需要手動(dòng)控制鎖 | 不需要(無鎖機(jī)制) | 需要(手動(dòng)加鎖 / 釋放,JVM 自動(dòng)管理鎖生命周期) | 不需要(內(nèi)部封裝鎖邏輯,對(duì)外透明) |
| 性能特點(diǎn) | 無鎖開銷,性能極高(僅操作本地副本) | 有鎖競(jìng)爭開銷,線程阻塞 / 喚醒有成本 | 低競(jìng)爭開銷,支持并行操作,性能優(yōu)于 synchronized + 普通集合 |
| 數(shù)據(jù)一致性 | 無一致性問題(各線程操作自己的副本) | 強(qiáng)一致性(同一時(shí)間僅一個(gè)線程修改,結(jié)果唯一) | 多數(shù)是「最終一致性」(如 ConcurrentHashMap),部分是強(qiáng)一致性(如 CopyOnWriteArrayList) |
| 內(nèi)存開銷 | 線程越多,副本越多,內(nèi)存開銷越大 | 無額外內(nèi)存開銷(僅占用鎖對(duì)象資源) | 可能有額外內(nèi)存開銷(如分段鎖的段結(jié)構(gòu)、CopyOnWrite 的副本數(shù)組) |
| 典型使用場(chǎng)景 | 線程上下文傳遞(用戶信息、請(qǐng)求 ID) | 保護(hù)自定義共享資源(如普通對(duì)象、普通集合) | 多線程并發(fā)讀寫共享集合(如緩存、配置存儲(chǔ)) |
簡單概括
- ThreadLocal:「隔離式」—— 無鎖、線程私有、不共享,適合上下文傳遞;
- synchronized:「串行式」—— 悲觀鎖、共享資源、強(qiáng)一致,適合自定義共享資源保護(hù);
- 并發(fā)容器:「并發(fā)式」—— 封裝鎖 / 無鎖、共享集合、高效,適合多線程讀寫共享集合。
到此這篇關(guān)于Java ThreadLocal 線程本地存儲(chǔ)工具的文章就介紹到這了,更多相關(guān)Java ThreadLocal 線程本地存儲(chǔ)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解如何在Java中重寫equals()和hashCode()方法
在 Java 中,equals() 和 hashCode() 方法是 Object 類中定義的重要方法,它們用于比較對(duì)象的相等性以及計(jì)算對(duì)象的哈希值,本文將詳細(xì)介紹如何在 Java 中重寫 equals() 和 hashCode() 方法,并討論其最佳實(shí)踐,需要的朋友可以參考下2024-08-08
完美解決SpringCloud-OpenFeign使用okhttp替換不生效問題
這篇文章主要介紹了完美解決SpringCloud-OpenFeign使用okhttp替換不生效問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02
java使用google身份驗(yàn)證器實(shí)現(xiàn)動(dòng)態(tài)口令驗(yàn)證的示例
本篇文章主要介紹了java使用google身份驗(yàn)證器實(shí)現(xiàn)動(dòng)態(tài)口令驗(yàn)證的示例,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08
SpringBoot?自定義starter?yaml提示失效問題及解決方法
在自定義starter后,必不可少會(huì)有properties配置參數(shù)需要指定,而在有時(shí)又不知道為什么出現(xiàn)這個(gè)問題,這篇文章主要介紹了SpringBoot?自定義starter?yaml提示失效問題,需要的朋友可以參考下2022-12-12
關(guān)于SpringMVC在Controller層方法的參數(shù)解析詳解
在SpringMVC中,控制器Controller負(fù)責(zé)處理由DispatcherServlet分發(fā)的請(qǐng)求,下面這篇文章主要給大家介紹了關(guān)于SpringMVC在Controller層方法的參數(shù)解析的相關(guān)資料,需要的朋友可以參考下2021-12-12
關(guān)于maven pom文件配置加載lib下的jar包
在項(xiàng)目中引用放置在lib文件夾中的第三方j(luò)ar包需要通過POM文件進(jìn)行特定配置,雖然將依賴放置在公司的Maven私服是更好的做法,但如果遇到部署問題,可以利用maven-jar-plugin進(jìn)行配置,Spring Boot項(xiàng)目可以通過特定設(shè)置來實(shí)現(xiàn)2024-09-09
mybatis使用collection嵌套查詢的實(shí)現(xiàn)
本文主要介紹了mybatis使用collection嵌套查詢的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

