面試官再問你 ThreadLocal,就這樣狠狠 “懟” 回去!
本文大綱
用過 ThreadLocal 嗎?在什么場景下會使用 ThreadLocal 講講 ThreadLocal 的原理吧! 使用 ThreadLocal 有什么需要注意的嗎? 有什么方式能提高 ThreadLocal 的性能嗎? 如何將 ThreadLocal 的數(shù)據(jù)傳遞到子線程中? 線程池中如何實現(xiàn) ThreadLocal 的數(shù)據(jù)傳遞?
用過 ThreadLocal 嗎?在什么場景下會使用 ThreadLocal。
這個回答一定要足夠自信:必須用過啊,無論是在平時的業(yè)務(wù)開發(fā)過程中會用到,其他很多三方框架中也都用到了 ThreadLocal。
如果你回答沒用過,很有可能就涼涼了,因為 ThreadLocal 在很多場景都能用到,假如實在沒用過也不要沒信心,看完這篇文章你就知道如何回答了。
場景一:ThreadLocal+MDC 實現(xiàn)鏈路日志增強(qiáng)
日志增強(qiáng)之前也寫過一篇文章,講解了實現(xiàn)的功能,細(xì)節(jié)沒有講,可以看看下面這篇文章了解。
文章:有了鏈路日志增強(qiáng),排查 Bug 小意思啦!
比如我們需要在整個鏈路的日志中輸出當(dāng)前登錄的用戶 ID,首先就得在攔截器獲取過濾器中獲取用戶 ID,然后將用戶 ID 進(jìn)行存儲到 ThreadLocal。
然后再層層進(jìn)行透傳,如果用的 Dubbo,那么就在 Dubbo 的 Filter 中進(jìn)行傳遞到下一個服務(wù)中。問題來了,在 Dubbo 的 Filter 中如何獲取前面存儲的用戶 ID 呢?
答案就是 ThreadLocal。獲取后添加到 MDC 中,就可以在日志中輸出用戶 ID。
場景二:ThreadLocal 實現(xiàn)線程內(nèi)的緩存,避免重復(fù)調(diào)用
緩存這塊就不重復(fù)講了,之前有單獨(dú)寫過文章,大家直接看之前的文章就可以了。
文章:簡直騷操作,ThreadLocal 還能當(dāng)緩存用
場景三:ThreadLocal 實現(xiàn)數(shù)據(jù)庫讀寫分離下強(qiáng)制讀主庫
首先你的項目中要做了讀寫分離,如果有對讀寫分離不了解的同學(xué)可以查看這篇文章:讀寫分離
某些業(yè)務(wù)場景下,必須保證數(shù)據(jù)的及時性。主從同步有延遲,可以使用強(qiáng)制讀主庫來保證數(shù)據(jù)的一致性。
在 Sharding JDBC 中,有提供對應(yīng)的 API 來設(shè)置強(qiáng)制路由到主庫,具體代碼如下:
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
HintManager 中就使用了 ThreadLocal 來存儲相關(guān)信息。這樣就可以實現(xiàn)在業(yè)務(wù)代碼中設(shè)置路由信息,在底層的數(shù)據(jù)庫路由那塊獲取信息,實現(xiàn)優(yōu)雅的數(shù)據(jù)傳遞。
public final class HintManager implements AutoCloseable {
private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal();
// ...............
}
場景四:ThreadLocal 實現(xiàn)同一線程下多個類之間的數(shù)據(jù)傳遞
在 Spring Cloud Zuul 中,過濾器是必須要用的。用過濾器我們可以實現(xiàn)權(quán)限認(rèn)證,日志記錄,限流等功能。
過濾器有多個,而且是按順序執(zhí)行的。過濾器之前要透傳數(shù)據(jù)該如何處理?
Zuul 中已經(jīng)提供了 RequestContext 來實現(xiàn)數(shù)據(jù)傳遞,比如我們在進(jìn)行攔截的時候會使用下面的代碼告訴負(fù)責(zé)轉(zhuǎn)發(fā)的過濾器不要進(jìn)行轉(zhuǎn)發(fā)操作。
RequestContext.getCurrentContext().setSendZuulResponse(false);
RibbonRoutingFilter 中就可以通過 RequestContext 獲取對應(yīng)的信息。
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
RequestContext 中就用了 ThreadLocal。
public class RequestContext extends ConcurrentHashMap<String, Object> {
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
@Override
protected RequestContext initialValue() {
try {
return contextClass.newInstance();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
};
// .........................
}
講講 ThreadLocal 的原理吧!
ThreadLocal 在使用的時候是單獨(dú)創(chuàng)建對象的,更像一個全局的容器。但是大家有沒有想過一個問題,就是為啥要設(shè)計 ThreadLocal 這個類,而不使用 HashMap 這樣的容器類?
ThreadLocal 本質(zhì)上是要解決線程之間數(shù)據(jù)的隔離,以達(dá)到互不影響的目的。如果我們用一個 Map 做數(shù)據(jù)存儲,Key 為線程 ID, Value 為你要存儲的內(nèi)容,其實也是能達(dá)到隔離的效果。
沒錯,效果是能達(dá)到,但是性能就不一定好了,涉及到多個線程進(jìn)行數(shù)據(jù)操作。如果你不看 ThreadLocal 的源碼,你肯定也會以為 ThreadLocal 就是這么實現(xiàn)的。
ThreadLocal 在設(shè)計這塊很巧妙,會在 Thread 類中嵌入一個 ThreadLocalMap,ThreadLocalMap 就是一個容器,用于存儲數(shù)據(jù)的,但它在 Thread 類中,也就說存儲的就是這個 Thread 類專享的數(shù)據(jù)。
原本我們以為的 ThreadLocal 設(shè)置值的代碼:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.put(t.getId(), value);
}
正在的設(shè)置值的代碼:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,先是獲取當(dāng)前線程對象,然后從當(dāng)前線程中獲取線程的 ThreadLocalMap,值是添加到這個 ThreadLocalMap 中的,key 就是當(dāng)前 ThreadLocal 的對象。從使用的 API 看上去像是把值存儲在了 ThreadLocal 中,其實值是存儲在線程內(nèi)部,然后關(guān)聯(lián)了對應(yīng)的 ThreadLocal,這樣通過 ThreadLocal.get 時就能獲取到對應(yīng)的值。
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();
}
來張圖感受下:

使用 ThreadLocal 有什么需要注意的嗎?
避免跨線程異步傳遞,雖然有解決方案,文末介紹了方案 使用時記得及時 remove, 防止內(nèi)存泄露 注釋說明使用場景,方便后人 對性能有極致要求可以參考開源框架的做法,用一些優(yōu)化后的類,比如 FastThreadLocal
有什么方式能提高 ThreadLocal 的性能嗎?
這個問題其實是考察你對其他的一些框架的了解,因為在一些開源的框架中也有使用 ThreadLocal 的場景,但是這些框架為了讓性能更好,一般都會做一些優(yōu)化。
比如 Netty 中就重寫了一個 FastThreadLocal 來代替 ThreadLocal,性能在一定場景下比 ThreadLocal 更好。
性能提升主要表現(xiàn)在如下幾點:
FastThreadLocal 操作數(shù)據(jù)的時候,會使用下標(biāo)的方式在數(shù)組中進(jìn)行查找來代替 ThreadLocal 通過哈希的方式進(jìn)行查找。 FastThreadLocal 利用字節(jié)填充來解決偽共享問題。
其實除了 Netty 中對 ThreadLocal 進(jìn)行了優(yōu)化,自定義了 FastThreadLocal。在其他的框架中也有類似的優(yōu)化,比如 Dubbo 中就 InternalThreadLocal,根據(jù)源碼中的注釋,也是參考了 FastThreadLocal 的設(shè)計,基本上差不多。
如何將 ThreadLocal 的數(shù)據(jù)傳遞到子線程中?
InheritableThreadLocal 可以將值從當(dāng)前線程傳遞到子線程中,但這種場景其實用的不多,我相信很多人都沒怎么聽過 InheritableThreadLocal。
那為什么 InheritableThreadLocal 就可以呢?
InheritableThreadLocal 這個類繼承了 ThreadLocal,重寫了 3 個方法,在當(dāng)前線程上創(chuàng)建一個新的線程實例 Thread 時,會把這些線程變量從當(dāng)前線程傳遞給新的線程實例。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
通過上面的代碼我們可以看到 InheritableThreadLocal 重寫了 childValue, getMap,createMap 三個方法,當(dāng)我們往里面 set 值的時候,值保存到了 inheritableThreadLocals 里面,而不是之前的 threadLocals。
關(guān)鍵的點來了,為什么當(dāng)創(chuàng)建新的線程時,可以獲取到上個線程里的 threadLocal 中的值呢?原因就是在新創(chuàng)建線程的時候,會把之前線程的 inheritableThreadLocals 賦值給新線程的 inheritableThreadLocals,通過這種方式實現(xiàn)了數(shù)據(jù)的傳遞。
源碼最開始在 Thread 的 init 方法中,如下:
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
createInheritedMap 如下:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
賦值代碼:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
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++;
}
}
}
}
線程池中如何實現(xiàn) ThreadLocal 的數(shù)據(jù)傳遞?
如果涉及到線程池使用 ThreadLocal, 必然會出現(xiàn)問題。首先線程池的線程是復(fù)用的,其次,比如你從 Tomcat 的線程到自己的業(yè)務(wù)線程,也就是跨線程池了,線程也就不是之前的那個線程了,也就是說 ThreadLocal 就用不了,那么如何解決呢?
可以使用阿里的 ttl 來解決,之前我也寫過一篇文章,可以查看:Spring Cloud 中 Hystrix 線程隔離導(dǎo)致 ThreadLocal 數(shù)據(jù)丟失
貼上 ttl 的鏈接:https://github.com/alibaba/transmittable-thread-local
ttl 是基于代碼方式的改造,下面再給大家介紹一種不用改造代碼的方式,基于 Java Agent 來實現(xiàn)的,牛的一批。
鏈接:https://github.com/Nepxion/DiscoveryAgent
關(guān)于作者:尹吉?dú)g,簡單的技術(shù)愛好者,《Spring Cloud 微服務(wù)-全棧技術(shù)與案例解析》, 《Spring Cloud 微服務(wù) 入門 實戰(zhàn)與進(jìn)階》作者, 公眾號猿天地發(fā)起人。
后臺回復(fù) 學(xué)習(xí)資料 領(lǐng)取學(xué)習(xí)視頻
如有收獲,點個在看,誠摯感謝
