三個重要方法:
-
set()
如果沒有set操作的
, 很容易引起髒資料問題ThreadLocal
-
get()
始終沒有get操作的
對象是沒有意義的ThreadLocal
-
remove()
如果沒有remove操作,則容易引起記憶體洩漏
- 如果ThreadLocal是非靜态的,屬于某個線程執行個體,那就失去了線程間共享的本質屬性;
那麼ThreadLocal到底有什麼作用呢?
我們知道,局部變量在方法内各個代碼塊間進行傳遞,而類變量在類内方法間進行傳遞;
複雜的線程方法可能需要調用很多方法來實作某個功能,這時候用什麼來傳遞線程内變量呢?
即ThreadLocal,它通常用于同一個線程内,跨類、跨方法傳遞資料;
如果沒有ThreadLocal,那麼互相之間的資訊傳遞,勢必要靠傳回值和參數,這樣無形之中,有些類甚至有些架構會互相耦合;
通過将Thread構造方法的最後一個參數設定為true,可以把目前線程的變量繼續往下傳遞給它建立的子線程
public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [
this (group, target, name, stackSize, null, inheritThreadLocals) ;
}
parent為其父線程
if (inheritThreadLocals && parent. inheritableThreadLocals != null)
this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;
createlnheritedMap()
其實就是調用
ThreadLocalMap
的私有構造方法來産生一個執行個體對象,把父線程中不為
null
的線程變量都拷貝過來
private ThreadLocalMap (ThreadLocalMap parentMap) {
// table就是存儲
Entry[] parentTable = parentMap. table;
int len = parentTable. length;
setThreshold(len) ;
table = new Entry[len];
for (Entry e : parentTable) {
if (e != null) {
ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
if (key != null) {
object value = key. childValue(e.value) ;
Entry c = new Entry(key, value) ;
int h = key. threadLocalHashCode & (len - 1) ;
while (table[h] != null)
h = nextIndex(h, len) ;
table[h] = C;
size++;
}
}
}
很多場景下可通過ThreadLocal來透傳全局上下文的;
比如用ThreadLocal來存儲監控系統的某個标記位,暫且命名為traceld.
某次請求下所有的traceld都是一緻的,以獲得可以統一解析的日志檔案;
但在實際開發過程中,發現子線程裡的traceld為null,跟主線程的traceld并不一緻,是以這就需要剛才說到的InheritableThreadLocal來解決父子線程之間共享線程變量的問題,使整個連接配接過程中的traceld一緻。
示例代碼如下
import org.apache.commons.lang3.StringUtils;
/**
* @author sss
* @date 2019/1/17
*/
public class RequestProcessTrace {
private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
= new InheritableThreadLocal<FullLinkContext>();
public static FullLinkContext getContext() {
FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
if (fullLinkContext == null) {
FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
}
return fullLinkContext;
}
private static class FullLinkContext {
private String traceId;
public String getTraceId() {
if (StringUtils.isEmpty(traceId)) {
FrameWork.startTrace(null, "JavaEdge");
traceId = FrameWork.getTraceId();
}
return traceId;
}
public void setTraceId(String traceId) {
this.traceId = traceId;
}
}
}
ThreadLocal的副作用
為了使線程安全地共享某個變量,JDK給出了ThreadLocal.
但ThreadLocal的主要問題是會産生髒資料和記憶體洩漏;
這兩個問題通常是線上程池的線程中使用ThreadLocal引發的,因為線程池有線程複用和記憶體常駐兩是線上程池的線程中使用ThreadLocal 引發的,因為線程池有線程複用和記憶體常駐兩個特點
髒資料
線程複用會産生髒資料。
由于線程池會重用 Thread 對象,與 Thread 綁定的靜态屬性 ThreadLocal 變量也會被重用。
如果在實作的線程run()方法中不顯式調用remove()清理與線程相關的ThreadLocal資訊,那麼若下一個線程不調用set(),就可能get() 到重用的線程資訊。包括ThreadLocal所關聯的線程對象的value值。
髒讀案例
比如,使用者A下單後沒有看到訂單記錄,而使用者B卻看到了使用者A的訂單記錄。通過排查發現是由于 session 優化引發。
在原來的請求過程中,使用者每次請求Server,都需要通過 sessionId 去緩存裡查詢使用者的session資訊,這樣無疑增加了一次調用。
是以工程師決定采用某架構來緩存每個使用者對應的SecurityContext,它封裝了session 相關資訊。優化後雖然會為每個使用者建立一個 session 相關的上下文,但由于Threadlocal沒有線上程處理結束時及時remove()。在高并發場景下,線程池中的線程可能會讀取到上一個線程緩存的使用者資訊。
示例代碼

輸出結果
重用錯誤案例
生産環境中,有時擷取到的使用者資訊是别人的。檢視代碼後,發現是使用了
ThreadLocal
緩存擷取到的使用者資訊。
ThreadLocal
适用于變量線上程間隔離,而在方法或類間共享的場景。
若使用者資訊的擷取比較昂貴(比如從DB查詢),則在
ThreadLocal
中緩存比較合适。
問題來了,為什麼有時會出現使用者資訊錯亂?
1.1 案例
使用Spring Boot建立一個Web應用程式,使用ThreadLocal存放一個Integer值,代表需要線上程中儲存的使用者資訊,這個值初始是null。在業務邏輯中,我先從ThreadLocal擷取一次值,然後把外部傳入的參數設定到ThreadLocal中,來模拟從目前上下文擷取到使用者資訊的邏輯,随後再擷取一次值,最後輸出兩次獲得的值和線程名稱。
固定思維認為,在設定使用者資訊前第一次擷取的值始終是null,但要清楚程式運作在Tomcat,執行程式的線程是Tomcat的工作線程,其基于線程池。
而線程池會重用固定線程,一旦線程重用,那麼很可能首次從ThreadLocal擷取的值是之前其他使用者的請求遺留的值。這時,ThreadLocal中的使用者資訊就是其他使用者的資訊。
1.2 bug 重制
在配置檔案設定Tomcat參數-工作線程池最大線程數設為1,這樣始終是同一線程在處理請求:
server.tomcat.max-threads=1
先讓使用者1請求接口,第一、第二次擷取到使用者ID分别是null和1,符合預期
使用者2請求接口,bug複現!第一、第二次擷取到使用者ID分别是1和2,顯然第一次擷取到了使用者1的資訊,因為Tomcat線程池重用了線程。兩次請求線程都是同一線程:
http-nio-45678-exec-1
。
寫業務代碼時,首先要了解代碼會跑在什麼線程上:
- Tomcat伺服器下跑的業務代碼,本就運作在一個多線程環境(否則接口也不可能支援這麼高的并發),并不能認為沒有顯式開啟多線程就不會有線程安全問題
- 線程建立較昂貴,是以Web伺服器會使用線程池處理請求,線程會被重用。使用類似ThreadLocal工具存放資料時,需注意在代碼運作完後,顯式清空設定的資料。