反問(wèn)面試官三個(gè) ThreadLocal 的問(wèn)題
ThreadLocal,一個(gè)Java人面試?yán)@不開(kāi)的話題,我也很奇怪為什么那些面試官很喜歡問(wèn)這個(gè),也不知道他們自己有沒(méi)有搞清楚。
接下來(lái),我想先說(shuō)說(shuō)ThreadLocal的用法和使用場(chǎng)景,然后反問(wèn)面試官3個(gè)關(guān)于ThreadLocal的話題。
使用方法和場(chǎng)景
一句話總結(jié):ThreadLocal是給每個(gè)線程準(zhǔn)備一份“獨(dú)立的小空間”,它讓每個(gè)線程都擁有自己獨(dú)立的變量副本。在多個(gè)線程并發(fā)訪問(wèn)時(shí),不用擔(dān)心變量之間的沖突問(wèn)題,避免了多線程之間的數(shù)據(jù)共享風(fēng)險(xiǎn)。
1.使用場(chǎng)景
ThreadLocal的使用場(chǎng)景主要在多線程環(huán)境中,能夠?yàn)槊總€(gè)線程提供獨(dú)立的變量副本。比如:
- 用戶上下文信息:比如,在Web應(yīng)用中,每個(gè)請(qǐng)求可能由不同的線程處理,在處理用戶請(qǐng)求時(shí)為每個(gè)線程維護(hù)獨(dú)立的用戶信息。
- 數(shù)據(jù)庫(kù)連接管理:比如,在多線程環(huán)境下,每個(gè)線程需要有自己獨(dú)立的數(shù)據(jù)庫(kù)連接。
- 事務(wù)管理:比如,在處理事務(wù)時(shí),每個(gè)線程可能需要有自己的事務(wù)上下文,確保線程安全的事務(wù)操作。
- 數(shù)據(jù)傳遞:比如,同一個(gè)線程,在不同的方法之間傳遞數(shù)據(jù),但又不想使用方法參數(shù)去傳遞,就可以使用ThreadLocal。像我們常用的日志跟蹤場(chǎng)景,跟蹤的ID會(huì)存在ThreadLocal中貫穿整個(gè)鏈條。
總之有2個(gè)場(chǎng)景:
- 在多線程場(chǎng)景下,每個(gè)線程需要獨(dú)立管理變量的場(chǎng)景。
- 某個(gè)線程想在整條鏈路上共享獨(dú)立變量的場(chǎng)景。
2.使用方法
使用時(shí),記住3條核心原則:
- 每個(gè)線程都有一份獨(dú)立的數(shù)據(jù)。
- 線程內(nèi)部使用的是ThreadLocalMap來(lái)保存數(shù)據(jù),Key就是ThreadLocal對(duì)象。
- 使用完畢后,記得調(diào)用remove方法,防止內(nèi)存溢出。
代碼示例
獨(dú)立保存變量的示例:
public class ThreadLocal4Independent {
private static ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
public static void main(String[] args) {
Runnable task = () -> {
int num = (int) (Math.random() * 100);
threadLocalVar.set(num);
System.out.println("線程:" + Thread.currentThread().getName() + "的值:" + threadLocalVar.get());
threadLocalVar.remove();
};
new Thread(task, "1").start();
new Thread(task, "2").start();
}
}
傳遞參數(shù)的示例:
public class ThreadLocal4DataPass {
// 使用ThreadLocal來(lái)存儲(chǔ)需要在多個(gè)方法間傳遞的數(shù)據(jù)
private static final ThreadLocal<String> threadLocalData = new ThreadLocal<>();
public static void main(String[] args) {
// 在主線程中設(shè)置數(shù)據(jù)
threadLocalData.set("ThreadLocal");
// 在主線程中調(diào)用不同的方法
method1();
method2();
// 清除ThreadLocal變量,防止內(nèi)存泄露
threadLocalData.remove();
}
private static void method1() {
// 在method1中獲取數(shù)據(jù)并打印
String data = threadLocalData.get();
System.out.println("方法1拿到的數(shù)據(jù)是:" + data);
}
private static void method2() {
// 在method2中獲取數(shù)據(jù)并打印
String data = threadLocalData.get();
System.out.println("方法2拿到的數(shù)據(jù)是:" + data);
}
}
聊完使用場(chǎng)景和方法,接下來(lái)問(wèn)面試官幾個(gè)問(wèn)題。
問(wèn)題1:請(qǐng)畫(huà)出ThreadLocal和Thread的關(guān)系圖
ThreadLocal和Thread的關(guān)系圖如下。
這里要牢記3點(diǎn):
(1) 數(shù)據(jù)實(shí)際上是存在ThreadLocalMap中的,ThreadLocalMap歸Thread所持有。見(jiàn)源代碼。
(2) ThreadLocalMap內(nèi)部使用的是K-V結(jié)構(gòu),Key是我們定義的ThreadLocal對(duì)象。見(jiàn)源代碼。
(3) ThreadLocalMap對(duì)ThreadLocal是弱引用關(guān)系。見(jiàn)源代碼。
問(wèn)題2:為什么ThreadLocalMap里的Key是弱引用
那為什么ThreadLocalMap里的Key是使用ThreadLocal呢?為什么又是弱引用呢?
這就不得不說(shuō)JDK的設(shè)計(jì)者的思想非常精妙了,有3點(diǎn)妙處:
- 一個(gè)線程要是存了多種數(shù)據(jù),總得有個(gè)規(guī)則去找他們,那就根據(jù)定義的ThreadLocal對(duì)象去找吧。
- 對(duì)于開(kāi)發(fā)者來(lái)說(shuō),他只需要使用ThreadLocal去保存數(shù)據(jù)即可,無(wú)需關(guān)系底層結(jié)構(gòu)。也就是說(shuō)對(duì)外暴露簡(jiǎn)單的使用方式即可,對(duì)于不需要調(diào)用方知道的細(xì)節(jié)全部隱藏。
- 一般情況下Thread的生命周期會(huì)很長(zhǎng),比如Web容器啟動(dòng)后,就會(huì)啟動(dòng)大量的線程丟到線程池中復(fù)用。所以ThreadLocalMap的生命周期也會(huì)很長(zhǎng)。但是,ThreadLocal對(duì)象存在的周期不一定長(zhǎng),如果,ThreadLocalMap的Key對(duì)ThreadLocal是強(qiáng)引用的話,那么ThreadLocal對(duì)象就會(huì)一直存在于內(nèi)存中得不到釋放,最終會(huì)導(dǎo)致內(nèi)存溢出,所以采用了弱引用。
問(wèn)題3:為什么ThreadLocal使用不當(dāng)會(huì)造成內(nèi)存溢出
從上圖的關(guān)系圖可以看出,Value的生命周期是跟著Thread的生命周期來(lái)的,如果一直不處理的話,也會(huì)出現(xiàn)內(nèi)存溢出的情況。
為了避免內(nèi)存溢出的情況,我們?cè)谑褂猛闠hreadLocal后,要即使調(diào)用remove方法,以便JVM回收Value。
總結(jié)
ThreadLocal 是并發(fā)編程中的強(qiáng)大工具,能夠?yàn)槊總€(gè)線程提供獨(dú)立的變量副本,避免線程安全問(wèn)題。并且這個(gè)ThreadLocal存入的值能夠貫穿整個(gè)流程。使用時(shí)要注意上文的幾點(diǎn),防止造成內(nèi)存溢出。