概述
ThreadLocal提供了線程內(nèi)存儲變量的能力,這些變量不同之處在于每一個線程讀取的變量是對應(yīng)的互相獨(dú)立的。比如我們熟知的Spring事務(wù)管理中就使用了ThreadLocal來保證多線程環(huán)境下connection的線程安全問題。再比如我們?nèi)粘5膉ava web項(xiàng)目開發(fā)中,經(jīng)常使用ThreadLocal來存儲一些用戶id等信息,在一次request請求中,首先拿到登錄的uid,然后放到ThreadLocal上下文中,這樣在service、dao層就不需要一直傳遞uid直接從上下文中獲取就可以了。
但是說起ThreadLocal,除了好用我們還經(jīng)常聽到的就是它有內(nèi)存泄露的風(fēng)險,那么到底是怎么產(chǎn)生內(nèi)存泄露的呢?難道jdk設(shè)計(jì)的類還會內(nèi)存泄露嗎?我們應(yīng)該怎么樣避免內(nèi)存泄露呢?
ThreadLocal內(nèi)部設(shè)計(jì)
關(guān)于ThreadLocal的使用說明就不提了,直接引用網(wǎng)上常見的一個圖來描述一下對象的引用關(guān)系

簡單解釋一下這個圖,假設(shè)有這樣幾行代碼
Thread t = new Thread(()->{
ThreadLocal tl = new ThreadLocal();
tl.set(object);
});
t.start();
Thread類定義有一個property叫做ThreadLocal.ThreadLocalMap threadLocals
threadLocals內(nèi)部是一個hashmap類似的結(jié)構(gòu),存儲著很多Entry
上面的代碼操作后的結(jié)果就是
Entry的key是 tl, value是object
那么棧上面的引用t代表的就是CurrentThreadRef,指向new Thread這個對象。tl代表的就是ThreadLocalRef,指向的就是new ThreadLocal這個對象。
內(nèi)存泄露探究
關(guān)于ThreadLocal的內(nèi)存泄露討論可能是由于ThreadLocal在我們平時代碼使用中越來越頻繁,又或許是高頻面試題的原因被討論的越來越多?,F(xiàn)在網(wǎng)上關(guān)于ThreadLocal內(nèi)存泄露的分析文章非常之多,但是我覺得并不全面,或者僅僅是提了一下弱引用這個問題就完了。
內(nèi)存泄露通常說的是key被回收后,value無法被訪問到但是仍然占用了內(nèi)存,key的弱引用當(dāng)然是最核心的點(diǎn),但是是否內(nèi)存泄露還跟我們的使用場景有關(guān)。通過上面的圖解分析我們可以發(fā)現(xiàn):
1、如果Thread生命周期比較長,是線程池場景,比如tomcat worker線程。那么除非ThreadLocal ref強(qiáng)引用被釋放掉,gc就會回收ThreadLocal對象,導(dǎo)致ThreadLocalMap中之前該ThreadLocal對應(yīng)的value無法回收,內(nèi)存泄露。
2、上一種情況下,ThreadLocal ref強(qiáng)引用什么情況下會釋放呢,如果我們平時使用的時候都是將ThreadLocal定義為static的變量,那么強(qiáng)引用是不會被釋放的,所以這時候key的弱引用就沒有那么重要了。
3、如果Thread本身生命周期結(jié)束了,CurrentThread ref強(qiáng)引用釋放了,gc以后ThreadLocalMap就完全被回收了,不會產(chǎn)生內(nèi)存泄露。
場景一
@Controller
@RequestMapping("/myThreadLocal")
public class MyThreadLocalTestController {
private static ThreadLocal staticThreadLocal = new MyThreadLocal();
/**
* ThreadLocal變量為非靜態(tài)變量,使用完以后釋放掉強(qiáng)引用,
* 只剩下threadLocalMap中entry的key這個弱引用,gc可以回收掉ThreadLocal對象
* 但是value My50MB對象不會被回收,除非thread的生命周期結(jié)束
* @return
*/
@RequestMapping(value = "/nonStaticWithTomcatThread", method = RequestMethod.GET)
@ResponseBody
public String nonStaticWithTomcatThread() {
System.out.println("staticThreadLocal.hashCode=" + staticThreadLocal.hashCode());
ThreadLocal t = new MyThreadLocal();
System.out.println("t.hashCode=" + t.hashCode());
t.set(new My50MB());// 注意禁用jvm逃逸分析的優(yōu)化 -XX:-DoEscapeAnalysis
t = null; // 釋放掉強(qiáng)引用
try {
System.gc();// 提示系統(tǒng)gc
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}
}
這種情況下Thread是池化的,但是ThreadLocal ref強(qiáng)引用會被釋放。
啟動tomcat運(yùn)行這段代碼,我們分3次獲取內(nèi)存dump信息
第一次在tomcat剛啟動成功,得到0.hprof
第二次在首次訪問/nonStaticWithTomcatThread這個請求后,得到1.hprof
第三次在二次訪問/nonStaticWithTomcatThread這個請求后,得到2.hprof
用MAT工具打開3個dump文件,查看Histogram信息,發(fā)現(xiàn)byte[]這個內(nèi)存的占用一次比一次多,每次多出50MB。打開with incoming reference 分別看到如下信息

0.hprof中看到最大的一個內(nèi)存占用是靜態(tài)變量staticThreadLocal中有一個1MB的byte[],這個跟我們的示例沒有關(guān)系

1.hprof中看到最大的一個內(nèi)存占用是My50MB對象內(nèi)部的一個byte[],My50MB被ThreadLocalMap$Entry引用,所以很明顯這個50MB就是那個內(nèi)存泄露的value。

2.hprof中看到有2個My50MB對象都引用了一個50MB的byte[],跟1.hprof一模一樣只是多了一個,因?yàn)槲覀冊L問了2次controller。如果我們訪問的次數(shù)越多,這個內(nèi)存泄露就越來越明顯。
場景二
@RequestMapping(value = "/staticWithTomcatThread", method = RequestMethod.GET)
@ResponseBody
public String staticWithTomcatThread() {
staticThreadLocal.set(new My50MB());
/**try {
// invoke service to do business
} finally {
staticThreadLocal.remove();
}**/
return "ok";
}
這種情況下Thread仍然是池化的,ThreadLocal ref強(qiáng)引用是不會被釋放的,如果還是調(diào)用2次controller方法,打印出來的dump文件是始終只會有一個My50MB存在,前提是2次是同一個線程對象(如果tomcat線程有n個,n個請求同時訪問,每一個線程都會存在一個My50MB的對象,不考慮內(nèi)存溢出的情況下),這里就不拿heap dump分析了。
當(dāng)然針對這種池化的線程,ThreadLocal就相當(dāng)于給這個線程增加狀態(tài)信息,線程復(fù)用的情況下容易出現(xiàn)業(yè)務(wù)邏輯錯誤。所以我們一般在使用線程處理完業(yè)務(wù)邏輯后要清理掉線程中的狀態(tài)信息,也就是加上代碼中被注釋掉的那段代碼。 這樣不但能避免邏輯錯誤,也可以使線程在非活躍狀態(tài)下系統(tǒng)內(nèi)存占用的更少,如果不調(diào)用remove方法清理其實(shí)也是一定程度的內(nèi)存泄露。
場景三
@RequestMapping(value = "/staticWithNewThread", method = RequestMethod.GET)
@ResponseBody
public String staticWithNewThread() {
new Thread(()->{
staticThreadLocal.set(new My50MB());
}).start();
return "ok";
}
這種情況下Thread生命周期在代碼執(zhí)行完畢后就會結(jié)束,Thread內(nèi)部的threadLocalMap就會被內(nèi)存回收了,所以不存在任何泄露的問題??雌饋韘et了一個對象到staticThreadLocal中,但是其實(shí)ThreadLocal只是一個工具,真實(shí)存儲是在Thread中,不能被表象所迷惑。
ThreadLocal對內(nèi)存泄露的預(yù)防
其實(shí)jdk將ThreadLocalMap$Entry的key設(shè)計(jì)為WeakReference的時候就已經(jīng)考慮了value的內(nèi)存泄露問題,我們看看ThreadLocalMap的注釋
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
最后一句話的意思大概就是當(dāng)map的空間占用過大后,那么弱引用的key被回收后,無用的entries就會被清理掉。在get() set()操作的時候都會有一些時機(jī)觸發(fā),具體可以自行看源碼

總結(jié)
1、ThreadLocal只是一個工具,具體的變量存儲是放在Thread中的,所以內(nèi)存泄露很大程度上要看Thread的生命周期
2、ThreadLocalMap$Entry中的key是弱引用,要防止key對象被回收造成value對象的內(nèi)存泄露
3、ThreadLocal一般都應(yīng)該定義成static變量
4、如果在線程池場景下使用ThreadLocal一定要記得調(diào)用remove
相關(guān)閱讀:
當(dāng)ThreadLocal碰上線程池 http://www.itdecent.cn/p/85d96fe9358b