InheritableThreadLocal相比ThreadLocal多一個能力:在創建子線程Thread時,子線程Thread會自動繼承父線程的InheritableThreadLocal信息到子線程中,進而實現在在子線程獲取父線程的InheritableThreadLocal值的目的。
關於ThreadLocal詳細內容,可以看這篇文章:史上最全ThreadLocal 詳解
和 ThreadLocal 的區別
舉個簡單的栗子對比下InheritableThreadLocal和ThreadLocal:
public class InheritableThreadLocalTest {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
testThreadLocal();
testInheritableThreadLocal();
}
/** * threadLocal測試 */
public static void testThreadLocal() {
// 在主線程中設置值到threadLocal
threadLocal.set("我是父線程threadLocal的值");
// 創建一個新線程並啓動
new Thread(() -> {
// 在子線程裏面無法獲取到父線程設置的threadLocal,結果為null
System.out.println("從子線程獲取到threadLocal的值: " + threadLocal.get()); }
).start();
}
/** * inheritableThreadLocal測試 */
public static void testInheritableThreadLocal() {
// 在主線程中設置一個值到inheritableThreadLocal
inheritableThreadLocal.set("我是父線程inheritableThreadLocal的值");
// 創建一個新線程並啓動
new Thread(() -> {
// 在子線程裏面可以自動獲取到父線程設置的inheritableThreadLocal
System.out.println("從子線程獲取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());
}).start();
}
}
執行結果:
從子線程獲取到threadLocal的值:null
從子線程獲取到inheritableThreadLocal的值:我是父線程inheritableThreadLocal的值
可以看到子線程中可以獲取到父線程設置的inheritableThreadLocal值,但不能獲取到父線程設置的threadLocal值
實現原理
InheritableThreadLocal 的實現原理相當精妙,它通過在創建子線程的瞬間,“複製”父線程的線程局部變量,從而實現了數據從父線程到子線程的一次性、創建時的傳遞 。
其核心工作原理可以清晰地通過以下序列圖展示,它描繪了當父線程創建一個子線程時,數據是如何被傳遞的:
下面我們來詳細拆解圖中的關鍵環節。
核心實現機制
- **數據結構基礎:
Thread類內部維護了兩個ThreadLocalMap類型的變量 :threadLocals:用於存儲普通ThreadLocal設置的變量副本。inheritableThreadLocals:專門用於存儲InheritableThreadLocal設置的變量副本 。InheritableThreadLocal通過重寫getMap和createMap方法,使其所有操作都針對inheritableThreadLocals字段,從而與普通ThreadLocal分離開 。
- 繼承觸發時刻:子線程的創建。繼承行為發生在子線程被創建(即執行
new Thread())時。在Thread類的init方法中,如果判斷需要繼承(inheritThreadLocals參數為true)且父線程(當前線程)的inheritableThreadLocals不為null,則會執行復制邏輯 。 - 複製過程的核心:
createInheritedMap。這是實現複製的核心方法 。它會創建一個新的ThreadLocalMap,並將父線程inheritableThreadLocals中的所有條目遍歷拷貝到新 Map 中。- Key的複製:Key(即
InheritableThreadLocal對象本身)是直接複製的引用。 - Value的生成:Value 並非直接複製引用,而是通過調用
InheritableThreadLocal的childValue(T parentValue)方法來生成子線程中的初始值。默認實現是直接返回父值(return parentValue;),這意味着對於對象類型,父子線程將共享同一個對象引用 。
- Key的複製:Key(即
關鍵特性與注意事項
- 創建時複製,後續獨立:繼承只發生一次,即在子線程對象創建的瞬間。此後,父線程和子線程對各自
InheritableThreadLocal變量的修改互不影響 。 - 在線程池中的侷限性:這是
InheritableThreadLocal最需要警惕的問題。線程池中的線程是複用的,這些線程在首次創建時可能已經從某個父線程繼承了值。但當它們被用於執行新的任務時,新的任務提交線程(邏輯上的“父線程”)與工作線程已無直接的創建關係,因此之前繼承的值不會更新,這會導致數據錯亂(如用户A的任務拿到了用户B的信息)或內存泄漏 。對於線程池場景,應考慮使用阿里開源的 TransmittableThreadLocal (TTL) 。 - 淺拷貝與對象共享:由於
childValue方法默認是淺拷貝,如果存入的是可變對象(如Map、List),父子線程實際持有的是同一個對象的引用。在一個線程中修改該對象的內部狀態,會直接影響另一個線程 。若需隔離,可以重寫childValue方法實現深拷貝 。 - 內存泄漏風險:與
ThreadLocal類似,如果線程長時間運行(如線程池中的核心線程),並且未及時調用remove方法清理,那麼該線程的inheritableThreadLocals會一直持有值的強引用,導致無法被GC回收。良好的實踐是在任務執行完畢後主動調用remove()
線程池中侷限性
一般來説,在真實的業務場景下,沒人會直接 new Thread,而都是使用線程池的,因此InheritableThreadLocal在線程池中的使用侷限性要額外注意
首先,我們先理解 InheritableThreadLocal的繼承前提
InheritableThreadLocal的繼承只發生在 新線程被創建時(即new Thread()並啓動時)。在創建過程中,子線程會複製父線程的InheritableThreadLocal值。- 在線程池中,線程是預先創建或按需創建的,並且會被複用。因此,繼承只會在線程池創建新線程時發生,而不會在複用現有線程時發生。
再看線程池創建新線程的條件,對於標準的 ThreadPoolExecutor,新線程的創建遵循以下規則:
- 當前線程數 < 核心線程數:當提交新任務時,如果當前運行的線程數小於核心線程數,即使有空閒線程,線程池也會創建新線程來處理任務。此時,新線程會繼承父線程(提交任務的線程)的
InheritableThreadLocal。 - 當前線程數 >= 核心線程數 && 隊列已滿 && 線程數 < 最大線程數:當任務隊列已滿,且當前線程數小於最大線程數時,線程池會創建新線程來處理任務。同樣,新線程會繼承父線程的
InheritableThreadLocal。
不會繼承的場景
- 線程複用:當線程池中有空閒線程時(例如,當前線程數 >= 核心線程數,但隊列未滿),任務會被分配給現有線程執行。此時,沒有新線程創建,因此不會發生繼承。現有線程的
InheritableThreadLocal值保持不變(可能是之前任務設置的值),這可能導致數據錯亂(如用户A的任務看到用户B的數據)。 - 線程數已達最大值:如果線程數已達最大線程數,且隊列已滿,新任務會被拒絕(根據拒絕策略),也不會創建新線程,因此不會繼承。
不只是線程池污染,線程池使用 InheritableThreadLocal 還可能存在獲取不到值的情況。例如,在執行異步任務的時候,複用了某個已有的線程A,並且當時創建該線程A的時候,沒有繼承InheritableThreadLocal,進而導致後面複用該線程的時候,從InheritableThreadLocal獲取到的值為null:
public class InheritableThreadLocalWithThreadPoolTest {
private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
// 這裏線程池core/max數量都只有2
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
2,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(3000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public static void main(String[] args) {
// 先執行了不涉及InheritableThreadLocal的子任務初始化線程池線程
testAnotherFunction();
testAnotherFunction();
// 後執行了涉及InheritableThreadLocal
testInheritableThreadLocalWithThreadPool("張三");
testInheritableThreadLocalWithThreadPool("李四");
threadPoolExecutor.shutdown();
}
/** * inheritableThreadLocal+線程池測試 */
public static void testInheritableThreadLocalWithThreadPool(String param) {
// 1. 在主線程中設置一個值到inheritableThreadLocal
inheritableThreadLocal.set(param);
// 2. 提交異步任務到線程池
threadPoolExecutor.execute(() -> {
// 3. 在線程池-子線程裏面可以獲取到父線程設置的inheritableThreadLocal嗎?
System.out.println("線程名: " + Thread.currentThread().getName() + ", 父線程設置的inheritableThreadLocal值: " + param + ", 子線程獲取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());
});
// 4. 清除inheritableThreadLocal
inheritableThreadLocal.remove();
}
/** * 模擬另一個獨立的功能 */
public static void testAnotherFunction() {
// 提交異步任務到線程池
threadPoolExecutor.execute(() -> {
// 在線程池-子線程裏面可以獲取到父線程設置的inheritableThreadLocal嗎?
System.out.println("線程名: " + Thread.currentThread().getName() + ", 線程池-子線程摸個魚");
});
}
}
執行結果:
線程名:pool-1-thread-2,線程池-子線程摸個魚
線程名:pool-1-thread-1,線程池-子線程摸個魚
線程名:pool-1-thread-1,父線程設置的inheritableThreadLocal值:李四,子線程獲取到inheritableThreadLocal的值:null
線程名:pool-1-thread-2,父線程設置的inheritableThreadLocal值:張三,子線程獲取到inheritableThreadLocal的值:null
當然了,解決這個問題可以考慮使用阿里開源的 TransmittableThreadLocal (TTL),或者在提交異步任務前,先獲取線程數據,再傳入。例如:
// 1. 在主線程中先獲取inheritableThreadLocal的值
String name = inheritableThreadLocal.get();
// 2. 提交異步任務到線程池
threadPoolExecutor.execute(() -> {
// 3. 在線程池-子線程裏面直接傳入數據
System.out.println("線程名: " + Thread.currentThread().getName() + ", 父線程設置的inheritableThreadLocal值: " + param + ", 子線程獲取到inheritableThreadLocal的值: " + name);
});
與 ThreadLocal 的對比
| 特性 | ThreadLocal | InheritableThreadLocal |
|---|---|---|
| 數據隔離 | 線程絕對隔離 | 線程絕對隔離 |
| 子線程繼承 | 不支持 | 支持(創建時) |
| 底層存儲字段 | Thread.threadLocals |
Thread.inheritableThreadLocals |
| 適用場景 | 線程內全局變量,避免傳參 | 父子線程間需要傳遞上下文數據 |