[TOC]
1. ThreadLocal簡介
網(wǎng)上看到一些文章,提到關(guān)于ThreadLocal可能引起的內(nèi)存泄露派敷,搞得都不敢在代碼里隨意使用了所刀,于是來研究下,看看到底ThreadLocal會(huì)不會(huì)導(dǎo)致內(nèi)存泄露瓶埋,什么情況下會(huì)導(dǎo)致泄露希柿。
ThreadLocal,顧名思義养筒,其存儲的內(nèi)容是線程私本地的/私有的曾撤,我們常使用ThreadLocal來存儲/維護(hù)一些資源或者變量,以避免線程爭用或者同步問題晕粪,例如使用ThreadLocal來為每個(gè)線程維持一個(gè)redis連接(生產(chǎn)中這也許不是一個(gè)好的方式挤悉,還是推薦專業(yè)的連接池)或者維持一些線程私有的變量等。
例如巫湘,假設(shè)我們在一個(gè)線程應(yīng)用中需要對時(shí)間做格式化装悲,我們很容易想到的是使用SimpleDateFormat這個(gè)工具類,但是SimpleDateFormat不是線程安全的尚氛,那么我們通常用兩種做法:
- 每次用到的時(shí)候new一個(gè)SimpleDateFormat對象诀诊,使用完丟棄,交給gc
- 每個(gè)線程維護(hù)一個(gè)SimpleDateFormat實(shí)例阅嘶,線程運(yùn)行期間不重復(fù)創(chuàng)建
那么無論從執(zhí)行效率還是內(nèi)存占用方面属瓣,我們都傾向于使用后者,即線程私有一個(gè)SimpleDateFormat對象,這時(shí)候奠涌,ThreadLocal就是很好的應(yīng)用宪巨,示例代碼如下:
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestTask implements Runnable {
private boolean stop = false;
private ThreadLocal<SimpleDateFormat> sdfHolder = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
@Override
public void run() {
while (!stop) {
String formatedDateStr = sdfHolder.get().format(new Date());
System.out.println("formated date str:" + formatedDateStr);
//may be sleep for a while to avoid high cpu cost
}
sdfHolder.remove();
}
//something else
}
代碼中模擬了一個(gè)需要反復(fù)執(zhí)行的Task,其run方法中溜畅,while條件除非stop是true捏卓,否則就一直運(yùn)轉(zhuǎn)下去。在該示例中通過ThreadLocal為每個(gè)線程實(shí)例化了一個(gè)SimpleDateFormat對象慈格,當(dāng)需要的時(shí)候怠晴,通過get()獲取即可,實(shí)現(xiàn)了每個(gè)線程全程只有一個(gè)SimpleDateFormat對象浴捆。同時(shí)在stop為true時(shí)使用ThreadLocal的remove方法刪除當(dāng)前線程使用的SimpleDateFormat對象蒜田,以便于垃圾回收。
僅演示ThreadLocal用法选泻,暫不討論代碼設(shè)計(jì)
2. ThreadLocal內(nèi)存模型
上面我們簡單介紹了ThreadLocal的概念和使用冲粤,下面看下ThreadLocal的內(nèi)存模型。
2.1 ThreadLocal內(nèi)存模型
2.1.1 私有變量存儲在哪里
在代碼中页眯,我們使用ThreadLocal實(shí)例提供的set/get方法來存儲/使用value梯捕,但ThreadLocal實(shí)例其實(shí)只是一個(gè)引用,真正存儲值的是一個(gè)Map窝撵,其key實(shí)ThreadLocal實(shí)例本身傀顾,value是我們設(shè)置的值,分布在堆區(qū)碌奉。這個(gè)Map的類型是ThreadL.ThreadLocalMap(ThreadLocalMap是ThreadLocal的內(nèi)部類)短曾,其key的類型是ThreadLocal,value是Object赐劣,類定義如下:
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
那么當(dāng)我們重寫init或者調(diào)用set/get的時(shí)候嫉拐,內(nèi)部的邏輯是怎樣的呢,按照上面的說法隆豹,應(yīng)該是將value存儲到了ThreadLocalMap中椭岩,或者從已有的ThreadLocalMap中獲取value茅逮,我們來通過代碼分析一下璃赡。
ThreadLocal.set(T value)
set的邏輯比較簡單,就是獲取當(dāng)前線程的ThreadLocalMap献雅,然后往map里添加KV碉考,K是this,也就是當(dāng)前ThreadLocal實(shí)例挺身,V是我們傳入的value侯谁。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
其內(nèi)部實(shí)現(xiàn)首先需要獲取關(guān)聯(lián)的Map,我們看下getMap和createMap的實(shí)現(xiàn)
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到,getMap就是返回了當(dāng)前Thread實(shí)例的map(t.threadLocals)墙贱,create也是創(chuàng)建了Thread的map(t.threadLocals)热芹,也就是說對于一個(gè)Thread實(shí)例,ThreadLocalMap是其內(nèi)部的一個(gè)屬性惨撇,在需要的時(shí)候伊脓,可以通過ThreadLocal創(chuàng)建或者獲取,然后存放相應(yīng)的值魁衙。我們看下Thread類的關(guān)鍵代碼
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
//省略了其他代碼
}
可以看到报腔,Thread中定義了屬性threadLocals,但其初始化和使用的過程剖淀,都是通過ThreadLocal這個(gè)類來執(zhí)行的纯蛾。
ThreadLocal.get()
get是獲取當(dāng)前線程的對應(yīng)的私有變量,是我們之前set或者通過initialValue指定的變量纵隔,其代碼如下
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
可以看到翻诉,其邏輯也比較簡單清晰:
- 獲取當(dāng)前線程的ThreadLocalMap實(shí)例
- 如果不為空,以當(dāng)前ThreadLocal實(shí)例為key獲取value
- 如果ThreadLocalMap為空或者根據(jù)當(dāng)前ThreadLocal實(shí)例獲取的value為空捌刮,則執(zhí)行setInitialValue()
setInitialValue()
內(nèi)部如下:
- 調(diào)用我們重寫的initialValue得到一個(gè)value
- 將value放入到當(dāng)前線程對應(yīng)的ThreadLocalMap中
- 如果map為空米丘,先實(shí)例化一個(gè)map,然后賦值KV
關(guān)鍵設(shè)計(jì)小結(jié)
代碼分析到這里糊啡,其實(shí)對于ThreadLocal的內(nèi)部主要設(shè)計(jì)以及其和Thread的關(guān)系比較清楚了:
- 每個(gè)線程拄查,是一個(gè)Thread實(shí)例,其內(nèi)部擁有一個(gè)名為threadLocals的實(shí)例成員棚蓄,其類型是ThreadLocal.ThreadLocalMap
- 通過實(shí)例化ThreadLocal實(shí)例堕扶,我們可以對當(dāng)前運(yùn)行的線程設(shè)置一些線程私有的變量,通過調(diào)用ThreadLocal的set和get方法存取
- ThreadLocal本身并不是一個(gè)容器梭依,我們存取的value實(shí)際上存儲在ThreadLocalMap中稍算,ThreadLocal只是作為TheadLocalMap的key
- 每個(gè)線程實(shí)例都對應(yīng)一個(gè)TheadLocalMap實(shí)例,我們可以在同一個(gè)線程里實(shí)例化很多個(gè)ThreadLocal來存儲很多種類型的值役拴,這些ThreadLocal實(shí)例分別作為key糊探,對應(yīng)各自的value
- 當(dāng)調(diào)用ThreadLocal的set/get進(jìn)行賦值/取值操作時(shí),首先獲取當(dāng)前線程的ThreadLocalMap實(shí)例河闰,然后就像操作一個(gè)普通的map一樣科平,進(jìn)行put和get
當(dāng)然,這個(gè)ThreadLocalMap并不是一個(gè)普通的Map(比如常用的HashMap)姜性,而是一個(gè)特殊的瞪慧,key為弱引用的map,這個(gè)我們后面再詳談
2.1.2 ThreadLocal內(nèi)存模型
通過上一節(jié)的分析部念,其實(shí)我們已經(jīng)很清楚ThreadLocal的相關(guān)設(shè)計(jì)了弃酌,對數(shù)據(jù)存儲的具體分布也會(huì)有個(gè)比較清晰的概念氨菇。下面的圖是網(wǎng)上找來的常見到的示意圖,我們可以通過該圖對ThreadLocal的存儲有個(gè)更加直接的印象妓湘。
我們知道Thread運(yùn)行時(shí)查蓉,線程的的一些局部變量和引用使用的內(nèi)存屬于Stack(棧)區(qū),而普通的對象是存儲在Heap(堆)區(qū)榜贴。根據(jù)上圖奶是,基本分析如下:
- 線程運(yùn)行時(shí),我們定義的TheadLocal對象被初始化竣灌,存儲在Heap聂沙,同時(shí)線程運(yùn)行的棧區(qū)保存了指向該實(shí)例的引用,也就是圖中的ThreadLocalRef
- 當(dāng)ThreadLocal的set/get被調(diào)用時(shí)初嘹,虛擬機(jī)會(huì)根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對應(yīng)在堆區(qū)的實(shí)例及汉,然后查看其對用的TheadLocalMap實(shí)例是否被創(chuàng)建,如果沒有屯烦,則創(chuàng)建并初始化坷随。
- Map實(shí)例化之后,也就拿到了該ThreadLocalMap的句柄驻龟,然后如果將當(dāng)前ThreadLocal對象作為key温眉,進(jìn)行存取操作
- 圖中的虛線,表示key對ThreadLocal實(shí)例的引用是個(gè)弱引用
3. 插曲:強(qiáng)引用/弱引用
java中的引用分為四種翁狐,按照引用強(qiáng)度不同类溢,從強(qiáng)到弱依次為:強(qiáng)引用、軟引用露懒、弱引用和虛引用闯冷,如果不是專門做jvm研究,對其概念很難清晰的定義懈词,我們大致可以理解為蛇耀,引用的強(qiáng)度,代表了對內(nèi)存占用的能力大小坎弯,具體體現(xiàn)在GC的時(shí)候纺涤,會(huì)不會(huì)被回收,什么時(shí)候被回收抠忘。
ThreadLocal被用作TheadLocalMap的弱引用key撩炊,這種設(shè)計(jì)也是ThreadLocal被討論內(nèi)存泄露的熱點(diǎn)問題,因此有必要了解一下什么是弱引用褐桌。
3.1 強(qiáng)引用
強(qiáng)引用雖然在開發(fā)過程中并不怎么提及衰抑,但是無處不在,例如我們在一個(gè)對象中通過如下代碼實(shí)例化一個(gè)StringBuffer對象
StringBuffer buffer = new StringBuffer();
我們知道StringBuffer的實(shí)例通常是被創(chuàng)建在堆中的荧嵌,而當(dāng)前對象持有該StringBuffer對象的引用呛踊,以便后續(xù)的訪問,這個(gè)引用啦撮,就是一個(gè)強(qiáng)引用谭网。
對GC知識比較熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般默認(rèn)是可達(dá)性算法赃春,即在每一輪GC的時(shí)候愉择,選定一些對象作為GC ROOT,然后以它們?yōu)楦l(fā)散遍歷织中,遍歷完成之后锥涕,如果一個(gè)對象不被任何GC ROOT引用,那么它就是不可達(dá)對象狭吼,則在接下來的GC過程中很可能會(huì)被回收层坠。
強(qiáng)引用最重要的就是它能夠讓引用變得強(qiáng)(Strong),這就決定了它和垃圾回收器的交互刁笙。具體來說破花,如果一個(gè)對象通過一串強(qiáng)引用鏈接可到達(dá)(Strongly reachable),它是不會(huì)被回收的疲吸。如果你不想讓你正在使用的對象被回收座每,這就正是你所需要的。
3.2 軟引用
軟引用是用來描述一些還有用但是并非必須的對象摘悴。對于軟引用關(guān)聯(lián)著的對象峭梳,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對象列進(jìn)回收返回之后進(jìn)行第二次回收蹂喻。如果這次回收還沒有足夠的內(nèi)存延赌,才會(huì)拋出內(nèi)存溢出異常。JDK1.2之后提供了SoftReference來實(shí)現(xiàn)軟引用叉橱。
相對于強(qiáng)引用挫以,軟引用在內(nèi)存充足時(shí)可能不會(huì)被回收,在內(nèi)存不夠時(shí)會(huì)被回收窃祝。
3.3 弱引用
弱引用也是用來描述非必須的對象的掐松,但它的強(qiáng)度更弱,被弱引用關(guān)聯(lián)的對象只能生存到下一次GC發(fā)生之前粪小,也就是說下一次GC就會(huì)被回收大磺。JDK1.2之后,提供了WeakReference來實(shí)現(xiàn)弱引用探膊。
3.4 虛引用
虛引用也成為幽靈引用或者幻影引用杠愧,它是最弱的一種引用關(guān)系。一個(gè)瑞祥是否有虛引用的存在逞壁,完全不會(huì)對其生存時(shí)間造成影響流济,也無法通過虛引用來取得一個(gè)對象的實(shí)例锐锣。為一個(gè)對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是在這個(gè)對象被GC時(shí)收到一個(gè)系統(tǒng)通知。JDK1.2之后提供了PhantomReference來實(shí)現(xiàn)虛引用
4. 可能的內(nèi)存泄露分析
了解了ThreadLocal的內(nèi)部模型以及弱引用绳瘟,接下來可以分析一下是否有內(nèi)存泄露的可能以及如何避免雕憔。
4.1 內(nèi)存泄露分析
根據(jù)上一節(jié)的內(nèi)存模型圖我們可以知道,由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal糖声,換句話說斤彼,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對象引用蘸泻,則在下一次GC的時(shí)候琉苇,ThreadLocal實(shí)例就會(huì)被回收,那么此時(shí)ThreadLocalMap里的一組KV的K就是null了悦施,因此在沒有額外操作的情況下并扇,此處的V便不會(huì)被外部訪問到,而且只要Thread實(shí)例一直存在歼争,Thread實(shí)例就強(qiáng)引用著ThreadLocalMap拜马,因此ThreadLocalMap就不會(huì)被回收,那么這里K為null的V就一直占用著內(nèi)存沐绒。
綜上俩莽,發(fā)生內(nèi)存泄露的條件是
- ThreadLocal實(shí)例沒有被外部強(qiáng)引用,比如我們假設(shè)在提交到線程池的task中實(shí)例化的ThreadLocal對象乔遮,當(dāng)task結(jié)束時(shí)扮超,ThreadLocal的強(qiáng)引用也就結(jié)束了
- ThreadLocal實(shí)例被回收,但是在ThreadLocalMap中的V沒有被任何清理機(jī)制有效清理
- 當(dāng)前Thread實(shí)例一直存在蹋肮,則會(huì)一直強(qiáng)引用著ThreadLocalMap出刷,也就是說ThreadLocalMap也不會(huì)被GC
也就是說,如果Thread實(shí)例還在,但是ThreadLocal實(shí)例卻不在了,則ThreadLocal實(shí)例作為key所關(guān)聯(lián)的value無法被外部訪問俭令,卻還被強(qiáng)引用著,因此出現(xiàn)了內(nèi)存泄露坷檩。
也就是說,我們回答了文章開頭的第一個(gè)問題改抡,ThreadLocal如果使用的不當(dāng)矢炼,是有可能引起內(nèi)存泄露的,雖然觸發(fā)的場景不算很容易阿纤。
這里要額外說明一下句灌,這里說的內(nèi)存泄露,是因?yàn)閷ζ鋬?nèi)存模型和設(shè)計(jì)不了解欠拾,且編碼時(shí)不注意導(dǎo)致的內(nèi)存管理失聯(lián)胰锌,而不是有意為之的一直強(qiáng)引用或者頻繁申請大內(nèi)存骗绕。比如如果編碼時(shí)不停的人為塞一些很大的對象,而且一直持有引用最終導(dǎo)致OOM匕荸,不能算作ThreadLocal導(dǎo)致的“內(nèi)存泄露”爹谭,只是代碼寫的不當(dāng)而已枷邪!
4.2 TheadLocal本身的優(yōu)化
進(jìn)一步分析ThreadLocalMap的代碼榛搔,可以發(fā)現(xiàn)ThreadLocalMap內(nèi)部也是做了一定的優(yōu)化的
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以看到,在set值的時(shí)候东揣,有一定的幾率會(huì)執(zhí)行replaceStaleEntry(key, value, i)
方法践惑,其作用就是將當(dāng)前的值替換掉以前的key為null的值,重復(fù)利用了空間嘶卧。
5. ThreadLocal使用建議
通過前面幾節(jié)的分析尔觉,我們基本弄清楚了ThreadLocal相關(guān)設(shè)計(jì)和內(nèi)存模型,對于是否會(huì)發(fā)生內(nèi)存泄露做了分析芥吟,下面總結(jié)下幾點(diǎn)建議:
- 當(dāng)需要存儲線程私有變量的時(shí)候侦铜,可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 當(dāng)需要實(shí)現(xiàn)線程安全的變量時(shí),可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 當(dāng)需要減少線程資源競爭的時(shí)候钟鸵,可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 注意Thread實(shí)例和ThreadLocal實(shí)例的生存周期钉稍,因?yàn)樗麄冎苯雨P(guān)聯(lián)著存儲數(shù)據(jù)的生命周期
- 如果頻繁的在線程中new ThreadLocal對象,在使用結(jié)束時(shí)棺耍,最好調(diào)用ThreadLocal.remove來釋放其value的引用贡未,避免在ThreadLocal被回收時(shí)value無法被訪問卻又占用著內(nèi)存
其實(shí)對于ThreadLocalMap還有很多設(shè)計(jì),關(guān)于其詳細(xì)內(nèi)容蒙袍,可以參考文后參考文章的最后一篇
參考文章