基本介紹
ThreadLocal很多地方叫線程本地變量,或者叫線程本地存儲。ThreadLocal為每一個使用該變量的線程都提供一個變量值的副本,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本沖突,實現線程間的數據隔離,至於是如何實現的,下面會在實現原理中介紹。但是我們需要知道,threadLocal只是實現了變量在不同線程中的數據隔離,即保證了同一變量在不同的線程中傳遞時可以有不同的值,換句話說ThreadLocal構建的是在同一線程中的全局變量,並沒有解決共享變量的問題,在多線程並發訪問共享變量時依然會出現並發問題,當然更不存在解決了什么同步問題。
但是這里有個ThreadLocal使用的誤區,因為ThreadLocal能夠是的參數在不同的方法中使用,所以有些小伙伴為了避免方法中寫了過多的參數就把參數的使用這些參數放到了threadLocal中,其實這是不合理的設計,也不是ThreadLocal設計的初衷。
使用場景
- 變量在每個線程需要有自己單獨的實例
- 實例需要在整個線程中共享,但不希望被多線程共享
實現原理
要想了解ThreadLocal是如何為每個線程實現數據隔離的,就需要了解下ThreadLocal中幾個重要的方法了。其實沖get和getMap方法中就可以明白了,每個線程中變量實例其實是放在一個類型為ThreadLocalMap的map的Value中的,而這個map是通過Thread中的變量threadLoacls來獲得的,這樣一來這個變量的值就會只存在於這個線程。另外我們注意到map中key是this,也就是ThreadLocal的實例,這也就是保證了不同的線程中使用的是同一個變量,但是可以有不同的值。
get()
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
getMap()
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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); }
createMap()
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
initialValue()
protected T initialValue() { return null; }
setInitialValue()
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
remove()
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
注意事項
- ThreadLocal 並不解決線程間共享數據的問題
- ThreadLocal 通過隱式的在不同線程內創建獨立實例副本避免了實例線程安全的問題
- ThreadLocalMap的map的Entry的key是對ThreadLocal的弱引用,主要是為了防止ThreadLocal回收失敗造成內存溢出問題
- ThreadLocalMap 的 set 方法通過調用 replaceStaleEntry 方法回收鍵為 null 的 Entry 對象的值(即為具體實例)以及 Entry 對象本身從而防止內存泄漏
- 每個線程持有一個 Map 並維護了 ThreadLocal 對象與具體實例的映射,該 Map 由於只被持有它的線程訪問,故不存在線程安全以及鎖的問題
- ThreadLocal 適用於變量在線程間隔離且在方法間共享的場景
- 使用ThreadLocal的get方法時必須先調用set,否者會報空指針,如果不像使用set方法,就必須重寫initialValue方法
- 使用 ThreadLocal 的時候,最好不要聲明為靜態的
- 使用完 ThreadLocal ,最好手動調用 remove() 方法,例如上面說到的 Session 的例子,如果不在攔截器或過濾器中處理,不僅可能出現內存泄漏問題,而且會影響業務邏輯;
應用實例
比如用來存儲用戶 Session。Session 的特性很適合 ThreadLocal ,因為 Session 之前當前會話周期內有效,會話結束便銷毀。我們先籠統但不正確的分析一次 web 請求的過程:
- 用戶在瀏覽器中訪問 web 頁面;
- 瀏覽器向服務器發起請求;
- 服務器上的服務處理程序(例如tomcat)接收請求,並開啟一個線程處理請求,期間會使用到 Session ;
- 最后服務器將請求結果返回給客戶端瀏覽器。
從這個簡單的訪問過程我們看到正好這個 Session 是在處理一個用戶會話過程中產生並使用的,如果單純的理解一個用戶的一次會話對應服務端一個獨立的處理線程,那用 ThreadLocal 在存儲 Session ,簡直是再合適不過了。但是例如 tomcat 這類的服務器軟件都是采用了線程池技術的,並不是嚴格意義上的一個會話對應一個線程。並不是說這種情況就不適合 ThreadLocal 了,而是要在每次請求進來時先清理掉之前的 Session ,一般可以用攔截器、過濾器來實現。
除此之外,還有數據庫鏈接,上下文管理器等都可以使用ThreadLocal。
session使用示例:
@Data public static class Session { private String id; private String user; private String status; }
public class SessionHandler { public static ThreadLocal<Session> session = new ThreadLocal<Session>(); public void createSession() { session.set(new Session()); } public String getUser() { return session.get().getUser(); } public String getStatus() { return session.get().getStatus(); } public void setStatus(String status) { session.get().setStatus(status); } public static void main(String[] args) { new Thread(() -> { SessionHandler handler = new SessionHandler(); handler.getStatus(); handler.getUser(); handler.setStatus("close"); handler.getStatus(); }).start(); } }
帶來的問題
資源消耗
由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的占用會比不使用ThreadLocal要大。
內存溢出
實際上 ThreadLocalMap
中使用的 key 為 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap
中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。
ThreadLocalMap
實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 為 null 的記錄后,沒有手動調用 remove() 方法,並且之后也不再調用 get()、set()、remove() 方法的情況下。