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