ThreadLocal 不知道大家有沒有用過,但至少聽說過,今天主要記錄一下 ThreadLocal 的原理和使用場景。
使用場景
直接定位到 ThreadLocal 的源碼,可以看到源碼注釋中有很清楚的解釋:它是線程的局部變量,這些變量只能在這個線程內被讀寫,在其他線程內是無法訪問的。 ThreadLocal 定義的通常是與線程關聯的私有靜態字段(例如,用戶ID或事務ID)。
變量有局部的還有全局的,局部變量沒什么好說的,一涉及到全局,那自然就會出現多線程的安全問題,要保證多線程安全訪問,不出現臟讀臟寫,那就要涉及到線程同步了。而 ThreadLocal 相當於提供了介於局部變量與全局變量中間的這樣一種線程內部的全局變量。
總結了半天,發現使用場景說到底就概括成一個:就是當我們只想在本身的線程內使用的變量,可以用 ThreadLocal 來實現,並且這些變量是和線程的生命周期密切相關的,線程結束,變量也就銷毀了。
所以說 ThreadLocal 不是為了解決線程間的共享變量問題的,如果是多線程都需要訪問的數據,那需要用全局變量加同步機制。
舉幾個例子說明一下:
1、比如線程中處理一個非常復雜的業務,可能方法有很多,那么,使用 ThreadLocal 可以代替一些參數的顯式傳遞;
2、比如用來存儲用戶 Session。Session 的特性很適合 ThreadLocal ,因為 Session 之前當前會話周期內有效,會話結束便銷毀。我們先籠統但不正確的分析一次 web 請求的過程:
- 用戶在瀏覽器中訪問 web 頁面;
- 瀏覽器向服務器發起請求;
- 服務器上的服務處理程序(例如tomcat)接收請求,並開啟一個線程處理請求,期間會使用到 Session ;
- 最后服務器將請求結果返回給客戶端瀏覽器。
從這個簡單的訪問過程我們看到正好這個 Session 是在處理一個用戶會話過程中產生並使用的,如果單純的理解一個用戶的一次會話對應服務端一個獨立的處理線程,那用 ThreadLocal 在存儲 Session ,簡直是再合適不過了。但是例如 tomcat 這類的服務器軟件都是采用了線程池技術的,並不是嚴格意義上的一個會話對應一個線程。並不是說這種情況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉之前的 Session ,一般可以用攔截器、過濾器來實現。
3、在一些多線程的情況下,如果用線程同步的方式,當並發比較高的時候會影響性能,可以改為 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 來保證高性能和線程安全;
4、還有像線程內上線文管理器、數據庫連接等可以用到 ThreadLocal;
使用方式
ThreadLocal 的使用非常簡單,最核心的操作就是四個:創建、創建並賦初始值、賦值、取值。
1、創建
ThreadLocal<String> mLocal = new ThreadLocal<>();
2、創建並賦初值。下面代碼表示創建了一個 String 類型的 ThreadLocal 並且重寫了 initialValue
方法,並返回初始字符串,之后調用 get() 方法獲取的值便是 initialValue
方法返回的值。
private static ThreadLocal<String> mLocal = new ThreadLocal<String>(){
@Override
protected String initialValue(){
return "init value";
}
};
System.out.println(mLocal.get());
3、設置值
mLocal.set("hello");
4、取值
mLocal.get()
實現原理
首先 ThreadLocal 是一個泛型類,保證可以接受任何類型的對象。
因為一個線程內可以存在多個 ThreadLocal 對象,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap
的靜態內部類。而我們使用的 get()、set() 方法其實都是調用了這個 ThreadLocalMap
類對應的 get()、set() 方法。例如下面的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
調用 ThreadLocal 的 set 方法時,首先獲取到了當前線程,然后獲取當前線程維護的 ThreadLocalMap
對象,最后在ThreadLocalMap
實例中添加上。如果 ThreadLocalMap
實例不存在則初始化並賦初始值。
這里看到 set 方法的第一個參數是 this
,this
即指的是當前的 ThreadLocal 對象,會看上看的代碼就是指的 mLocal 這個對象。而在 ThreadLocalMap
的 set 方法中會根據當前 ThreadLocal 對象實例,做一些操作和判斷,最終實現賦值操作(具體參考源碼)。
所以說,最終的變量是放在了當前線程的 ThreadLocalMap
中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是一個中間工具,傳遞了變量值。
內存泄漏問題
實際上 ThreadLocalMap
中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap
中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。
ThreadLocalMap
實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 為 null 的記錄后,沒有手動調用 remove() 方法,並且之后也不再調用 get()、set()、remove() 方法的情況下。
最后
- 使用 ThreadLocal 的時候,最好要聲明為靜態的;
- 使用完 ThreadLocal ,最好手動調用 remove() 方法,例如上面說到的 Session 的例子,如果不在攔截器或過濾器中處理,不僅可能出現內存泄漏問題,而且會影響業務邏輯;
更多文章請關注我的公眾號:古時的風箏