新聞中心
前言
當(dāng)多線程訪問共享且可變的數(shù)據(jù)時(shí),涉及到線程間同步的問題,并不是所有時(shí)候,都要用到共享數(shù)據(jù),所以就需要ThreadLocal出場了。

ThreadLocal又稱線程本地變量,使用其能夠?qū)?shù)據(jù)封閉在各自的線程中,每一個(gè)ThreadLocal能夠存放一個(gè)線程級別的變量且它本身能夠被多個(gè)線程共享使用,并且又能達(dá)到線程安全的目的,且絕對線程安全,其用法如下所示:
public final static ThreadLocalRESOURCE = new ThreadLocal ();
RESOURCE代表一個(gè)能夠存放String類型的ThreadLocal對象。此時(shí)不論什么一個(gè)線程能夠并發(fā)訪問這個(gè)變量,對它進(jìn)行寫入、讀取操作,都是線程安全的。
除了線程安全之外,使用ThreadLocal也能夠作為一種“方便傳參”的工具,在業(yè)務(wù)邏輯冗長的代碼中,同一個(gè)參數(shù)需要傳入在多個(gè)方法之間層層傳遞,當(dāng)這種需要傳遞的參數(shù)過多時(shí)代碼會顯得十分臃腫、丑陋;
之前我給公司做過企微會話存檔的功能,就是將企業(yè)微信聊天信息拉取下來保存,由于企業(yè)微信消息類型很多(至少有三十多種),為了后期便于維護(hù)在對消息解析、保存時(shí)根據(jù)消息類型封分別封裝了對應(yīng)的方法每個(gè)消息類型解析、保存時(shí)又會進(jìn)一步細(xì)分拆分成多個(gè)方法(比如說文件資源的分片拉取、上傳到靜態(tài)資源服務(wù)器),這個(gè)時(shí)候麻煩的事情就來了,每個(gè)方法的入?yún)⒍夹枰笪挻鏅n的相關(guān)配置參數(shù)和封裝的對話信息參數(shù),導(dǎo)致入?yún)⒘斜矸浅iL,閱讀性比較差。
實(shí)際上可以把企微會話存檔的相關(guān)配置參數(shù)存入到ThreadLocal中,各個(gè)方法內(nèi)需要使用直接從ThreadLocal中獲取就可以了,以后有時(shí)間了要把這塊代碼重構(gòu)一下。
后來我又做了公司的短信模塊的需求,主要是記錄短信發(fā)送記錄、發(fā)送統(tǒng)計(jì)及短信發(fā)送狀態(tài),短信發(fā)送的接口有多個(gè)(單條發(fā)送、批量發(fā)送、根據(jù)模板發(fā)送等等),需要記錄多個(gè)接口的調(diào)用情況,當(dāng)時(shí)就抽象出了短信上下文、模板上下文等實(shí)體,在調(diào)用方法時(shí)首先構(gòu)造對應(yīng)的上下文并將其保存到ThreadLocal中,在短信余額校驗(yàn)、違禁詞過濾、余額不足提醒等業(yè)務(wù)處理方法中只需要從ThreadLocal中取出對應(yīng)的上下文即可,而且發(fā)送狀態(tài)是通過切面進(jìn)行記錄的,在切入點(diǎn)記錄日志時(shí)也是直接從ThreadLocal中直接獲取的上下文信息,代碼簡潔、可讀性高。
說了不少廢話,現(xiàn)在就步入正題了,讓我們揭開ThreadLocal的廬山真面目。
原理
先看一下ThreadLocal的結(jié)構(gòu)
需要我們重點(diǎn)關(guān)注的方法有:
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到底是怎么實(shí)現(xiàn)的了。這塊代碼其實(shí)很有意思,我們發(fā)現(xiàn)在向ThreadLocal中存放值時(shí)需要先從當(dāng)前線程中獲取ThreadLocalMap,最后實(shí)際是要把當(dāng)前ThreadLocal對象作為key、要存入的值作為value存放到ThreadLocalMap中,那我們就不得不先看一下ThreadLocalMap的結(jié)構(gòu)。
static class ThreadLocalMap {
/**
* 鍵值對實(shí)體的存儲結(jié)構(gòu)
*/
static class Entry extends WeakReference> {
// 當(dāng)前線程關(guān)聯(lián)的 value,這個(gè) value 并沒有用弱引用追蹤
Object value;
/**
* 構(gòu)造鍵值對
*
* @param k k 作 key,作為 key 的 ThreadLocal 會被包裝為一個(gè)弱引用
* @param v v 作 value
*/
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
// 初始容量,必須為 2 的冪
private static final int INITIAL_CAPACITY = 16;
// 存儲 ThreadLocal 的鍵值對實(shí)體數(shù)組,長度必須為 2 的冪
private Entry[] table;
// ThreadLocalMap 元素?cái)?shù)量
private int size = 0;
// 擴(kuò)容的閾值,默認(rèn)是數(shù)組大小的三分之二
private int threshold;
}
ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類,當(dāng)一個(gè)線程有多個(gè) ThreadLocal 時(shí),需要一個(gè)容器來管理多個(gè) ThreadLocal,ThreadLocalMap 的作用就是管理線程中多個(gè) ThreadLocal,從源碼中看到 ThreadLocalMap 其實(shí)就是一個(gè)簡單的 Map 結(jié)構(gòu),底層是數(shù)組,有初始化大小,也有擴(kuò)容閾值大小,數(shù)組的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal內(nèi)存入 的值。ThreadLocalMap 解決 hash 沖突的方式采用的是「線性探測法」,如果發(fā)生沖突會繼續(xù)尋找下一個(gè)空的位置。
每個(gè)Thread內(nèi)部都持有一個(gè)ThreadLoalMap對象。
至此,我們都能夠明白ThreadLocal存值的過程了,雖然我們是按照前言中的用法聲明了一個(gè)全局常量,但是這個(gè)常量在每次設(shè)置時(shí)實(shí)際都是向當(dāng)前線程的ThreadLocalMap內(nèi)存值,從而確保了數(shù)據(jù)在不同線程之間的隔離。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
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();
}
有了上面的鋪墊,這段代碼就不難理解了,獲取ThreadLocal內(nèi)的值時(shí),實(shí)際上是從當(dāng)前線程的ThreadLocalMap中以當(dāng)前ThreadLocal對象作為key取出對應(yīng)的值,由于值在保存時(shí)時(shí)線程隔離的,所以現(xiàn)在取值時(shí)只會取得當(dāng)前線程中的值,所以是絕對線程安全的。
remove
private void remove(ThreadLocal> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
remove將ThreadLocal對象關(guān)聯(lián)的鍵值對從Entry中移除,正確執(zhí)行remove方法能夠避免使用ThreadLocal出現(xiàn)內(nèi)存泄漏的潛在風(fēng)險(xiǎn),int i = key.threadLocalHashCode & (len-1)這行代碼很有意思,從一個(gè)集合中找到一個(gè)元素存放位置的最簡單方法就是利用該元素的hashcode對這個(gè)集合的長度取余,如果我們能夠?qū)⒓系拈L度限制成2的整數(shù)次冪就能夠?qū)⑷∮噙\(yùn)算轉(zhuǎn)換成hashcode與[集合長度-1]的與運(yùn)算,這樣就能夠提高查找效率,HashMap中也是這樣處理的,這里就不再展開了。
下面的一張圖很好的解釋了ThreadLocal的原理
ThreadLocal內(nèi)存泄漏及正確用法
在提及ThreadLocal使用的注意事項(xiàng)時(shí),所有的文章都會指出內(nèi)存泄漏這一風(fēng)險(xiǎn),但是我發(fā)現(xiàn)很少有文章能夠真正的把這一部分講清楚,這里我就斗膽嘗試一下,由于ThreadLocalMap中的Entry的key持有的是ThreadLocal對象的弱引用,當(dāng)這個(gè)ThreadLocal對象當(dāng)且僅當(dāng)被ThreadLocalMap中的Entry引用時(shí)發(fā)生了GC,會導(dǎo)致當(dāng)前ThreadLocal對象被回收;那么 ThreadLocalMap 中保存的 key 值就變成了 null,而Entry 又被 ThreadLocalMap 對象引用,ThreadLocalMap 對象又被 Thread 對象所引用,那么當(dāng) Thread 一直不銷毀的話,value 對象就會一直存在于內(nèi)存中,也就導(dǎo)致了內(nèi)存泄漏,直至 Thread 被銷毀后,才會被回收。
下面我們就來驗(yàn)證一下這個(gè)情景,我們在方法內(nèi)部聲明了一個(gè)ThreadLocal對象,為了更好的演示內(nèi)存泄漏的情景我們在使用這個(gè)對象存值后將方法內(nèi)取消對其的強(qiáng)引用,并且通過System.gc()觸發(fā)了一次垃圾回收(準(zhǔn)確的說是希望jvm執(zhí)行一次垃圾回收,不能保證垃圾回收一定會進(jìn)行,而且具體什么時(shí)候進(jìn)行是取決于具體的虛擬機(jī)的),這樣再垃圾回收時(shí)會將ThreadLocal對象回收,代碼如下所示:
@Test
public void loop() throws Exception {
for (int i = 0; i < 1; i++) {
ThreadLocalthreadLocal = new ThreadLocal<>();
threadLocal.set(new SysUser(System.currentTimeMillis(), "李四"));
// threadLocal = null;
//System.gc();
printEntryInfo();
}
//System.gc();
//printEntryInfo();
}
private void printEntryInfo() throws Exception {
Thread currentThread = Thread.currentThread();
Class extends Thread> clz = currentThread.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(currentThread);
Class> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
}
在不發(fā)生GC時(shí),控制臺輸出如下:
ThreadLocal對象并未被回收,將System.gc();放開,控制臺輸入如下:
可以看出key確實(shí)變成了null值,而Entry內(nèi)會一直持有對value的引用,導(dǎo)致value無法被回收,如果當(dāng)前線程一直在執(zhí)行未被銷毀,則確實(shí)會出現(xiàn)內(nèi)存泄漏(在使用線程池時(shí)更容易出現(xiàn)這樣的問題)。讓我們分析一下上面的為什么會出現(xiàn)內(nèi)存泄漏的原因,在上面的代碼里,我們在方法內(nèi)部聲明了一個(gè)ThreadLocal對象,該ThreadLocal對象僅有一個(gè)方法內(nèi)部的強(qiáng)引用且的生命周期很短,當(dāng)該方法執(zhí)行完成之后此ThreadLocal對象在下一次gc時(shí)就會被回收,當(dāng)然我們可以在方法結(jié)束前手動執(zhí)行一個(gè)該對象的remove方法,但是這樣就失去了使用ThreadLocal的意義。
由此,我們知道出現(xiàn)內(nèi)存泄漏的原因是失去了對ThreadLocal對象的強(qiáng)引用,避免內(nèi)存泄漏最簡單的方法就是始終保持對ThreadLocal對象的強(qiáng)引用,為每個(gè)線程聲明一個(gè)對ThreadLocal對象的強(qiáng)引用顯然是不合適的(太麻煩且缺乏聲明的時(shí)機(jī)),所以,我們可以將ThreadLocal對象聲明為一個(gè)全局常量,所有的線程均使用這一常量即可,例如:
private static final ThreadLocalRESOURCE = new ThreadLocal<>();
@Test
public void multiThread() {
Thread thread1 = new Thread(() -> {
RESOURCE.set("thread1");
System.gc();
try {
printEntryInfo();
} catch (Exception e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
RESOURCE.set("thread2");
System.gc();
try {
printEntryInfo();
} catch (Exception e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
按照上面的方式聲明ThreadLocal對象后,所有的線程共用此對象,在使用此對象存值時(shí)會把此對象作為key然后把對應(yīng)的值作為value存入到當(dāng)前線程的ThreadLocalMap中,由于此對象始終存在著一個(gè)全局的強(qiáng)引用,所以其不會被垃圾回收,調(diào)用remove方法后就能夠?qū)⒋藢ο箨P(guān)聯(lián)的Entry清除。
驗(yàn)證一下:
弱引用key:java.lang.ThreadLocal@10c6d8a7,值:thread1
弱引用key:java.lang.ThreadLocal@10c6d8a7,值:thread2
可以看出兩個(gè)線程內(nèi)對應(yīng)的Entry的key為同一個(gè)對象且即使發(fā)生了垃圾回收該對象也不會被回收。
那么是不是說將ThreadLocal對象聲明為一個(gè)全局常量后使用就沒有問題了呢,當(dāng)然不是,我們需要確保在每次使用完ThreadLocal對象后確保要執(zhí)行一下該對象的remove方法,清除當(dāng)前線程保存的信息,這樣當(dāng)此線程再被利用時(shí)不會取到錯(cuò)誤的信息(使用線程池極易出現(xiàn));
我們的項(xiàng)目之前就出現(xiàn)過這種場景,從線程池中獲取線程,并在每次請求時(shí)在當(dāng)前線程記錄下對應(yīng)的用戶信息,結(jié)果有一天出現(xiàn)了串號的問題,B用戶訪問時(shí)使用了A用戶的信息,這就是在每次請求結(jié)束后沒有執(zhí)行remove方法,線程復(fù)用時(shí)內(nèi)部還保存著上一個(gè)用戶的信息,貼上一份使用ThreadLocal的正確姿勢:
package com.cube.share.thread.config;
import com.cube.share.thread.entity.SysUser;
/**
* @author poker.li
* @date 2021/7/31 14:50
*
* 線程當(dāng)前用戶信息
*/
public class CurrentUser {
private static final ThreadLocalUSER = new ThreadLocal<>();
private static final ThreadLocalUSER_ID = new ThreadLocal<>();
private static final InheritableThreadLocalINHERITABLE_USER = new InheritableThreadLocal<>();
private static final InheritableThreadLocalINHERITABLE_USER_ID = new InheritableThreadLocal<>();
public static void setUser(SysUser sysUser) {
USER.set(sysUser);
INHERITABLE_USER.set(sysUser);
}
public static void setUserId(Long id) {
USER_ID.set(id);
INHERITABLE_USER_ID.set(id);
}
public static SysUser user() {
return USER.get();
}
public static SysUser inheritableUser() {
return INHERITABLE_USER.get();
}
public static Long inheritableUserId() {
return INHERITABLE_USER_ID.get();
}
public static Long userId() {
return USER_ID.get();
}
public static void removeAll() {
USER.remove();
USER_ID.remove();
INHERITABLE_USER.remove();
INHERITABLE_USER_ID.remove();
}
}
我們可以通過切面或者請求監(jiān)聽器在請求結(jié)束時(shí)將當(dāng)前線程保存的ThreadLocal信息清除。
/**
* @author poker.li
* @date 2021/7/31 15:12
*
* ServletRequest請求監(jiān)聽器
*/
@Component
@Slf4j
public class ServletRequestHandledEventListener implements ApplicationListener{
@Override
public void onApplicationEvent(ServletRequestHandledEvent event) {
CurrentUser.removeAll();
log.debug("清除當(dāng)前線程用戶信息,uri = {},method = {},servletName = {},clientAddress = {}", event.getRequestUrl(),
event.getMethod(), event.getServletName(), event.getClientAddress());
}
}
可傳遞給子線程的InheritableThreadLocal
如果我們在當(dāng)前線程中開辟新的子線程并希望子線程獲取父線程保存的線程本地變量要怎么做呢,在子線程中聲明ThreadLocal對象并將父線程中對應(yīng)的值存入自然是可以的,但是大可不必如此繁瑣,jdk已經(jīng)為我們提供了一種可傳遞給子線程的InheritableThreadLocal,實(shí)現(xiàn)的原理也很簡單,可以在Thread中一窺究竟。
//持有了一個(gè)可傳遞給子線程的ThreadLocalMap
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//線程創(chuàng)建時(shí)都會執(zhí)行這個(gè)初始化方法,inheritThreadLocals表示是否需要在構(gòu)造時(shí)從父線程中繼承thread-locals,默認(rèn)為true
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//忽略了一部分代碼
setPriority(priority);
//從父線程中繼承thread-locals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
使用場景
ThreadLocal 的特性也導(dǎo)致了應(yīng)用場景比較廣泛,主要的應(yīng)用場景如下:
- 線程間數(shù)據(jù)隔離,各線程的 ThreadLocal 互不影響
- 方便同一個(gè)線程使用某一對象,避免不必要的參數(shù)傳遞
- 全鏈路追蹤中的 traceId 或者流程引擎中上下文的傳遞一般采用 ThreadLocal
- Spring 事務(wù)管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的實(shí)現(xiàn)使用了 ThreadLocal
總結(jié)
本文主要從源碼的角度解析了 ThreadLocal,并分析了發(fā)生內(nèi)存泄漏的原因及正確用法,最后對它的應(yīng)用場景進(jìn)行了簡單介紹。ThreadLocal還有其他變種例如FastThreadLocal和TransmittableThreadLocal,F(xiàn)astThreadLocal主要解決了偽共享的問題比ThreadLocal擁有更好的性能,TransmittableThreadLocal主要解決了線程池中線程復(fù)用導(dǎo)致后續(xù)提交的任務(wù)并不會繼承到父線程的線程變量的問題,這里限于篇幅就不展開了。
分享文章:如何正確使用ThreadLocal,你真的用對了嗎?
分享網(wǎng)址:http://fisionsoft.com.cn/article/cdjocsp.html


咨詢
建站咨詢
