一文講透Java面試高頻之ThreadLocal原理、內(nèi)存泄漏、使用場(chǎng)景
前言
ThreadLocal 是 Java 面試中高級(jí)崗位的高頻考點(diǎn)。很多人知道它是"線程本地變量",但面試官一追問(wèn)"為什么會(huì)內(nèi)存泄漏"就答不上來(lái)了。本文從原理到源碼到實(shí)際應(yīng)用,幫你把 ThreadLocal 徹底搞懂。
一、ThreadLocal 解決什么問(wèn)題?
一句話:讓每個(gè)線程擁有自己獨(dú)立的變量副本,線程之間互不干擾。
最常見(jiàn)的場(chǎng)景:
// 每個(gè)請(qǐng)求一個(gè)線程,每個(gè)線程需要知道當(dāng)前登錄用戶(hù)是誰(shuí)
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void set(User user) { currentUser.set(user); }
public static User get() { return currentUser.get(); }
public static void remove() { currentUser.remove(); }
}Filter 中攔截請(qǐng)求,把用戶(hù)信息 set 進(jìn)去;Service 層任意位置直接 get,不需要一層層傳參數(shù)。請(qǐng)求結(jié)束 remove 掉。
不用 ThreadLocal 的話怎么辦?要么把 User 對(duì)象從 Controller 傳到 Service 再傳到 DAO(侵入性強(qiáng)),要么放在一個(gè)全局 Map 里自己管理線程安全(復(fù)雜、容易出 bug)。
二、底層原理:Thread、ThreadLocalMap、Entry
很多人以為數(shù)據(jù)存在 ThreadLocal 對(duì)象里。不是。數(shù)據(jù)存在 Thread 對(duì)象里。
// Thread 類(lèi)的源碼
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}每個(gè) Thread 都有一個(gè) `ThreadLocalMap`,這個(gè) Map 的 key 是 ThreadLocal 實(shí)例,value 是你存的值。
調(diào)用關(guān)系:
threadLocal.set(value) → 獲取當(dāng)前線程 Thread.currentThread() → 拿到該線程的 ThreadLocalMap → 以 this(當(dāng)前 ThreadLocal 實(shí)例)為 key,存入 value threadLocal.get() → 獲取當(dāng)前線程 Thread.currentThread() → 拿到該線程的 ThreadLocalMap → 以 this 為 key,取出 value
關(guān)鍵理解:ThreadLocal 本身不存數(shù)據(jù),它只是一個(gè)"鑰匙",真正的數(shù)據(jù)存在每個(gè)線程自己的 Map 里。 不同線程用同一個(gè) ThreadLocal 對(duì)象作為 key,但各自的 Map 是獨(dú)立的,所以取到的值不同。

三、ThreadLocalMap 的結(jié)構(gòu)
ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類(lèi),不是 java.util.HashMap,它自己實(shí)現(xiàn)了一套:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用
value = v;
}
}
private Entry[] table; // 底層數(shù)組
}兩個(gè)關(guān)鍵點(diǎn):
(1)底層是數(shù)組,不是鏈表/紅黑樹(shù)。哈希沖突用**開(kāi)放尋址法**(線性探測(cè)),不是拉鏈法
(2)key是弱引用(WeakReference)——這就是內(nèi)存泄漏問(wèn)題的根源

四、內(nèi)存泄漏:為什么會(huì)泄漏?怎么避免?
這是面試最?lèi)?ài)問(wèn)的部分。

4.1 弱引用導(dǎo)致的問(wèn)題
棧中的引用(強(qiáng)引用)→ ThreadLocal 對(duì)象 ← Entry.key(弱引用)
Entry.value → 實(shí)際數(shù)據(jù)(強(qiáng)引用)
正常情況下,棧中的引用和 Entry 的弱引用同時(shí)指向 ThreadLocal 對(duì)象。
當(dāng)棧中的引用被回收了(比如方法結(jié)束、變量置 null):
棧中的引用(已回收) ThreadLocal 對(duì)象 ← Entry.key(弱引用,GC 后變 null)
Entry.value → 實(shí)際數(shù)據(jù)(強(qiáng)引用,還在!)
GC 會(huì)回收只有弱引用指向的 ThreadLocal 對(duì)象,導(dǎo)致 Entry 的 key 變成 null。但 value 是強(qiáng)引用,不會(huì)被回收。
這時(shí)候 Entry 就變成了一個(gè)"key 為 null,value 還在"的廢棄節(jié)點(diǎn)。只要線程還活著(比如線程池中的線程),這個(gè) value 就永遠(yuǎn)不會(huì)被回收——這就是內(nèi)存泄漏。
4.2 ThreadLocal 的自我清理機(jī)制
ThreadLocal 在 get/set/remove 時(shí),會(huì)順便清理 key 為 null 的廢棄 Entry(源碼中叫 expungeStaleEntry)。
所以如果你一直在調(diào)用 ThreadLocal 的方法,廢棄 Entry 會(huì)被逐步清理。但如果你 set 完就再也不碰了,廢棄 Entry 就一直留著。
4.3 正確用法:一定要 remove
try {
threadLocal.set(value);
// 業(yè)務(wù)邏輯
} finally {
threadLocal.remove(); // 必須!
}特別是在線程池環(huán)境下(Web 應(yīng)用幾乎都是線程池),線程會(huì)被復(fù)用,如果不 remove:
- 下一個(gè)請(qǐng)求可能拿到上一個(gè)請(qǐng)求的數(shù)據(jù)(數(shù)據(jù)串了)
- 廢棄 Entry 越積越多(內(nèi)存泄漏)
4.4 為什么 key 要用弱引用?
面試官可能會(huì)追問(wèn)這個(gè)。
如果 key 是強(qiáng)引用,那即使外部不再使用某個(gè) ThreadLocal 對(duì)象,Entry 的 key 也會(huì)阻止它被 GC。這樣 ThreadLocal 對(duì)象本身也會(huì)泄漏。
用弱引用至少能保證 ThreadLocal 對(duì)象可以被回收,泄漏的只是 value。而且 ThreadLocal 的自我清理機(jī)制可以逐步回收這些 value。
弱引用是一種"盡力而為"的兜底策略,不是完美方案。真正的保障還是要靠手動(dòng) remove。
五、實(shí)際使用場(chǎng)景
場(chǎng)景 1:Web 應(yīng)用中傳遞用戶(hù)上下文
public class RequestContext {
private static final ThreadLocal<Long> userId = new ThreadLocal<>();
private static final ThreadLocal<String> traceId = new ThreadLocal<>();
// set/get/remove 方法...
}在 Filter 或 Interceptor 中 set,業(yè)務(wù)代碼中 get,請(qǐng)求結(jié)束 remove。Spring 的 `RequestContextHolder` 就是這么做的。
場(chǎng)景 2:SimpleDateFormat 線程安全問(wèn)題
SimpleDateFormat 不是線程安全的。要么每次 new(浪費(fèi)),要么加鎖(性能差),要么用 ThreadLocal:
private static final ThreadLocal<SimpleDateFormat> sdf =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));每個(gè)線程一份 SimpleDateFormat 實(shí)例,既線程安全又不浪費(fèi)。
(當(dāng)然 JDK 8+ 建議直接用 `DateTimeFormatter`,它本身就是線程安全的。)
場(chǎng)景 3:數(shù)據(jù)庫(kù)連接 / 事務(wù)管理
Spring 的事務(wù)管理器用 ThreadLocal 存儲(chǔ)當(dāng)前線程的數(shù)據(jù)庫(kù)連接,保證同一個(gè)事務(wù)中的多次 SQL 操作用的是同一個(gè) Connection。
六、面試回答模板
- ThreadLocal 是線程本地變量,讓每個(gè)線程擁有自己獨(dú)立的變量副本。
- 底層實(shí)現(xiàn):每個(gè) Thread 對(duì)象里有一個(gè) ThreadLocalMap,key 是 ThreadLocal 實(shí)例(弱引用),value 是實(shí)際的值。調(diào)用 set/get 時(shí),先拿到當(dāng)前線程的 Map,再以 ThreadLocal 自身為 key 進(jìn)行操作。
- 關(guān)于內(nèi)存泄漏:因?yàn)?key 是弱引用,當(dāng)外部不再持有 ThreadLocal 的強(qiáng)引用時(shí),GC 會(huì)回收 key,導(dǎo)致 Entry 的 key 變成 null 但 value 還在。特別是在線程池環(huán)境下,線程不會(huì)銷(xiāo)毀,這些廢棄 Entry 就一直占著內(nèi)存。所以用完必須調(diào)用 remove。
- 常見(jiàn)使用場(chǎng)景:Web 應(yīng)用中傳遞用戶(hù)上下文、解決 SimpleDateFormat 線程安全問(wèn)題、Spring 事務(wù)管理器存儲(chǔ)當(dāng)前連接等。
七、高頻追問(wèn)
- ThreadLocalMap 為什么用開(kāi)放尋址法而不是拉鏈法? → ThreadLocalMap 的元素通常很少(一個(gè)線程不會(huì)有太多 ThreadLocal 變量),開(kāi)放尋址法在元素少時(shí)緩存命中率更高,性能更好
- InheritableThreadLocal 是什么? → 子線程可以繼承父線程的 ThreadLocal 值。但在線程池中不適用,因?yàn)榫€程是復(fù)用的不是新建的。阿里的 TransmittableThreadLocal 解決了這個(gè)問(wèn)題
- ThreadLocal 和 synchronized 的區(qū)別? → synchronized 是多個(gè)線程競(jìng)爭(zhēng)同一個(gè)資源,用鎖保證同一時(shí)刻只有一個(gè)線程訪問(wèn);ThreadLocal 是每個(gè)線程一份副本,用空間換時(shí)間,根本不存在競(jìng)爭(zhēng)
到此這篇關(guān)于Java面試高頻之ThreadLocal原理、內(nèi)存泄漏、使用場(chǎng)景的文章就介紹到這了,更多相關(guān)Java ThreadLocal原理、內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中的SynchronousQueue阻塞隊(duì)列使用代碼實(shí)例
這篇文章主要介紹了Java中的SynchronousQueue阻塞隊(duì)列使用代碼實(shí)例,SynchronousQueue是無(wú)緩沖區(qū)的阻塞隊(duì)列,即不能直接向隊(duì)列中添加數(shù)據(jù),會(huì)報(bào)隊(duì)列滿異常,需要的朋友可以參考下2023-12-12
如何通過(guò)java將doc文件轉(zhuǎn)換為docx文件詳解
在數(shù)字化時(shí)代文檔處理成為了我們?nèi)粘9ぷ骱蛯W(xué)習(xí)中不可或缺的一部分,其中doc和docx作為兩種常見(jiàn)的文檔格式,各自具有不同的特點(diǎn)和優(yōu)勢(shì),這篇文章主要給大家介紹了關(guān)于如何通過(guò)java將doc文件轉(zhuǎn)換為docx文件的相關(guān)資料,需要的朋友可以參考下2024-07-07
spring?boot自動(dòng)裝配之@ComponentScan注解用法詳解
@ComponentScan的作用就是根據(jù)定義的掃描路徑,把符合掃描規(guī)則的類(lèi)裝配到spring容器中,下面這篇文章主要給大家介紹了關(guān)于spring?boot自動(dòng)裝配之@ComponentScan注解用法的相關(guān)資料,需要的朋友可以參考下2023-04-04
mybatisplus如何在xml的連表查詢(xún)中使用queryWrapper
這篇文章主要介紹了mybatisplus如何在xml的連表查詢(xún)中使用queryWrapper,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
IDEA代碼規(guī)范&質(zhì)量檢查的實(shí)現(xiàn)
這篇文章主要介紹了IDEA代碼規(guī)范&質(zhì)量檢查的實(shí)現(xiàn),文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Spring?AOP通知類(lèi)型與實(shí)戰(zhàn)示例講解
Spring?AOP提供了五種通知類(lèi)型:@Before、@After、@AfterReturning、@AfterThrowing和@Around,每種通知類(lèi)型都有其特定的使用場(chǎng)景和實(shí)現(xiàn)方式,通過(guò)合理使用這些通知類(lèi)型,可以實(shí)現(xiàn)各種橫切關(guān)注點(diǎn)的模塊化和解耦,感興趣的朋友跟隨小編一起看看吧2024-11-11
自定義log4j2中的Appender來(lái)獲取日志內(nèi)容的示例代碼
在 Log4j2 中,Appender 是負(fù)責(zé)將日志事件輸出到目標(biāo)地點(diǎn)的組件,本文講述的是通過(guò) log4j 中自定義的 Appender 來(lái)獲取需要打印的日志信息,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-02-02
Java和JVM的重載識(shí)別,重寫(xiě)方法是怎樣進(jìn)行的
這篇文章主要介紹了Java和JVM的重載識(shí)別,重寫(xiě)方法是怎樣進(jìn)行的,違章圍繞了Java和JVM的重載識(shí)別,重寫(xiě)方法展開(kāi)相關(guān)資料,需要的小伙伴可以參考一下,希望對(duì)你的工作或?qū)W習(xí)有所幫助2022-01-01

