博客 / 詳情

返回

InheritableThreadLocal,從入門到放棄

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 的實現原理相當精妙,它通過在創建子線程的瞬間,“複製”父線程的線程局部變量,從而實現了數據從父線程到子線程的一次性、創建時的傳遞 。

其核心工作原理可以清晰地通過以下序列圖展示,它描繪了當父線程創建一個子線程時,數據是如何被傳遞的:

sequenceDiagram participant Parent as 父線程 participant Thread as Thread構造方法 participant ITL as InheritableThreadLocal participant ThMap as ThreadLocalMap participant Child as 子線程 Parent->>Thread: 創建 new Thread() Note over Parent,Thread: 關鍵步驟:初始化 Thread->>Thread: 調用 init() 方法 Note over Thread,ITL: 檢查父線程的 inheritableThreadLocals Thread->>+ThMap: createInheritedMap(<br/>parent.inheritableThreadLocals) ThMap->>ThMap: 新建一個ThreadLocalMap loop 遍歷父線程Map中的每個Entry ThMap->>+ITL: 調用 key.childValue(parentValue) ITL-->>-ThMap: 返回子線程初始值<br/>(默認返回父值,可重寫) ThMap->>ThMap: 將 (key, value) 放入新Map end ThMap-->>-Thread: 返回新的ThreadLocalMap對象 Thread->>Child: 將新Map賦給子線程的<br/>inheritableThreadLocals屬性 Note over Child: 子線程擁有父線程變量的副本

下面我們來詳細拆解圖中的關鍵環節。

核心實現機制

  1. **數據結構基礎:Thread類內部維護了兩個 ThreadLocalMap類型的變量 :
    • threadLocals:用於存儲普通 ThreadLocal設置的變量副本。
    • inheritableThreadLocals:專門用於存儲 InheritableThreadLocal設置的變量副本 。InheritableThreadLocal通過重寫 getMapcreateMap方法,使其所有操作都針對 inheritableThreadLocals字段,從而與普通 ThreadLocal分離開 。
  2. 繼承觸發時刻:子線程的創建。繼承行為發生在子線程被創建(即執行 new Thread())時。在 Thread類的 init方法中,如果判斷需要繼承(inheritThreadLocals參數為 true父線程(當前線程)的 inheritableThreadLocals不為 null,則會執行復制邏輯 。
  3. 複製過程的核心:createInheritedMap。這是實現複製的核心方法 。它會創建一個新的 ThreadLocalMap,並將父線程 inheritableThreadLocals中的所有條目遍歷拷貝到新 Map 中。
    • Key的複製:Key(即 InheritableThreadLocal對象本身)是直接複製的引用。
    • Value的生成:Value 並非直接複製引用,而是通過調用 InheritableThreadLocalchildValue(T parentValue)方法來生成子線程中的初始值。默認實現是直接返回父值return parentValue;),這意味着對於對象類型,父子線程將共享同一個對象引用 。

關鍵特性與注意事項

  1. 創建時複製,後續獨立:繼承只發生一次,即在子線程對象創建的瞬間。此後,父線程和子線程對各自 InheritableThreadLocal變量的修改互不影響 。
  2. 在線程池中的侷限性:這是 InheritableThreadLocal最需要警惕的問題。線程池中的線程是複用的,這些線程在首次創建時可能已經從某個父線程繼承了值。但當它們被用於執行新的任務時,新的任務提交線程(邏輯上的“父線程”)與工作線程已無直接的創建關係,因此之前繼承的值不會更新,這會導致數據錯亂(如用户A的任務拿到了用户B的信息)或內存泄漏​ 。對於線程池場景,應考慮使用阿里開源的 TransmittableThreadLocal (TTL)​ 。
  3. 淺拷貝與對象共享:由於 childValue方法默認是淺拷貝,如果存入的是可變對象(如 MapList),父子線程實際持有的是同一個對象的引用。在一個線程中修改該對象的內部狀態,會直接影響另一個線程 。若需隔離,可以重寫 childValue方法實現深拷貝 。
  4. 內存泄漏風險:與 ThreadLocal類似,如果線程長時間運行(如線程池中的核心線程),並且未及時調用 remove方法清理,那麼該線程的 inheritableThreadLocals會一直持有值的強引用,導致無法被GC回收。良好的實踐是在任務執行完畢後主動調用 remove()

線程池中侷限性

一般來説,在真實的業務場景下,沒人會直接 new Thread,而都是使用線程池的,因此InheritableThreadLocal在線程池中的使用侷限性要額外注意

首先,我們先理解 InheritableThreadLocal的繼承前提

  • InheritableThreadLocal的繼承只發生在 新線程被創建時(即 new Thread()並啓動時)。在創建過程中,子線程會複製父線程的 InheritableThreadLocal值。
  • 在線程池中,線程是預先創建或按需創建的,並且會被複用。因此,繼承只會在線程池創建新線程時發生,而不會在複用現有線程時發生。

再看線程池創建新線程的條件,對於標準的 ThreadPoolExecutor,新線程的創建遵循以下規則:

  1. 當前線程數 < 核心線程數:當提交新任務時,如果當前運行的線程數小於核心線程數,即使有空閒線程,線程池也會創建新線程來處理任務。此時,新線程會繼承父線程(提交任務的線程)的 InheritableThreadLocal
  2. 當前線程數 >= 核心線程數 && 隊列已滿 && 線程數 < 最大線程數:當任務隊列已滿,且當前線程數小於最大線程數時,線程池會創建新線程來處理任務。同樣,新線程會繼承父線程的 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
適用場景 線程內全局變量,避免傳參 父子線程間需要傳遞上下文數據
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.