[TOC]
1. ThreadLocal簡介
網上看到一些文章,提到關于ThreadLocal可能引起的內存泄露,搞得都不敢在代碼里隨意使用了,于是來研究下,看看到底ThreadLocal會不會導致內存泄露,什么情況下會導致泄露。
ThreadLocal,顧名思義,其存儲的內容是線程私本地的/私有的,我們常使用ThreadLocal來存儲/維護一些資源或者變量,以避免線程爭用或者同步問題,例如使用ThreadLocal來為每個線程維持一個redis連接(生產中這也許不是一個好的方式,還是推薦專業的連接池)或者維持一些線程私有的變量等。
例如,假設我們在一個線程應用中需要對時間做格式化,我們很容易想到的是使用SimpleDateFormat這個工具類,但是SimpleDateFormat不是線程安全的,那么我們通常用兩種做法:
- 每次用到的時候new一個SimpleDateFormat對象,使用完丟棄,交給gc
- 每個線程維護一個SimpleDateFormat實例,線程運行期間不重復創建
那么無論從執行效率還是內存占用方面,我們都傾向于使用后者,即線程私有一個SimpleDateFormat對象,這時候,ThreadLocal就是很好的應用,示例代碼如下:
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
}
代碼中模擬了一個需要反復執行的Task,其run方法中,while條件除非stop是true,否則就一直運轉下去。在該示例中通過ThreadLocal為每個線程實例化了一個SimpleDateFormat對象,當需要的時候,通過get()獲取即可,實現了每個線程全程只有一個SimpleDateFormat對象。同時在stop為true時使用ThreadLocal的remove方法刪除當前線程使用的SimpleDateFormat對象,以便于垃圾回收。
僅演示ThreadLocal用法,暫不討論代碼設計
2. ThreadLocal內存模型
上面我們簡單介紹了ThreadLocal的概念和使用,下面看下ThreadLocal的內存模型。
2.1 ThreadLocal內存模型
2.1.1 私有變量存儲在哪里
在代碼中,我們使用ThreadLocal實例提供的set/get方法來存儲/使用value,但ThreadLocal實例其實只是一個引用,真正存儲值的是一個Map,其key實ThreadLocal實例本身,value是我們設置的值,分布在堆區。這個Map的類型是ThreadL.ThreadLocalMap(ThreadLocalMap是ThreadLocal的內部類),其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;
}
}
}
那么當我們重寫init或者調用set/get的時候,內部的邏輯是怎樣的呢,按照上面的說法,應該是將value存儲到了ThreadLocalMap中,或者從已有的ThreadLocalMap中獲取value,我們來通過代碼分析一下。
ThreadLocal.set(T value)
set的邏輯比較簡單,就是獲取當前線程的ThreadLocalMap,然后往map里添加KV,K是this,也就是當前ThreadLocal實例,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);
}
其內部實現首先需要獲取關聯的Map,我們看下getMap和createMap的實現
/**
* 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就是返回了當前Thread實例的map(t.threadLocals),create也是創建了Thread的map(t.threadLocals),也就是說對于一個Thread實例,ThreadLocalMap是其內部的一個屬性,在需要的時候,可以通過ThreadLocal創建或者獲取,然后存放相應的值。我們看下Thread類的關鍵代碼
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這個類來執行的。
ThreadLocal.get()
get是獲取當前線程的對應的私有變量,是我們之前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;
}
可以看到,其邏輯也比較簡單清晰:
- 獲取當前線程的ThreadLocalMap實例
- 如果不為空,以當前ThreadLocal實例為key獲取value
- 如果ThreadLocalMap為空或者根據當前ThreadLocal實例獲取的value為空,則執行setInitialValue()
setInitialValue()
內部如下:
- 調用我們重寫的initialValue得到一個value
- 將value放入到當前線程對應的ThreadLocalMap中
- 如果map為空,先實例化一個map,然后賦值KV
關鍵設計小結
代碼分析到這里,其實對于ThreadLocal的內部主要設計以及其和Thread的關系比較清楚了:
- 每個線程,是一個Thread實例,其內部擁有一個名為threadLocals的實例成員,其類型是ThreadLocal.ThreadLocalMap
- 通過實例化ThreadLocal實例,我們可以對當前運行的線程設置一些線程私有的變量,通過調用ThreadLocal的set和get方法存取
- ThreadLocal本身并不是一個容器,我們存取的value實際上存儲在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key
- 每個線程實例都對應一個TheadLocalMap實例,我們可以在同一個線程里實例化很多個ThreadLocal來存儲很多種類型的值,這些ThreadLocal實例分別作為key,對應各自的value
- 當調用ThreadLocal的set/get進行賦值/取值操作時,首先獲取當前線程的ThreadLocalMap實例,然后就像操作一個普通的map一樣,進行put和get
當然,這個ThreadLocalMap并不是一個普通的Map(比如常用的HashMap),而是一個特殊的,key為弱引用的map,這個我們后面再詳談
2.1.2 ThreadLocal內存模型
通過上一節的分析,其實我們已經很清楚ThreadLocal的相關設計了,對數據存儲的具體分布也會有個比較清晰的概念。下面的圖是網上找來的常見到的示意圖,我們可以通過該圖對ThreadLocal的存儲有個更加直接的印象。
我們知道Thread運行時,線程的的一些局部變量和引用使用的內存屬于Stack(棧)區,而普通的對象是存儲在Heap(堆)區。根據上圖,基本分析如下:
- 線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區保存了指向該實例的引用,也就是圖中的ThreadLocalRef
- 當ThreadLocal的set/get被調用時,虛擬機會根據當前線程的引用也就是CurrentThreadRef找到其對應在堆區的實例,然后查看其對用的TheadLocalMap實例是否被創建,如果沒有,則創建并初始化。
- Map實例化之后,也就拿到了該ThreadLocalMap的句柄,然后如果將當前ThreadLocal對象作為key,進行存取操作
- 圖中的虛線,表示key對ThreadLocal實例的引用是個弱引用
3. 插曲:強引用/弱引用
java中的引用分為四種,按照引用強度不同,從強到弱依次為:強引用、軟引用、弱引用和虛引用,如果不是專門做jvm研究,對其概念很難清晰的定義,我們大致可以理解為,引用的強度,代表了對內存占用的能力大小,具體體現在GC的時候,會不會被回收,什么時候被回收。
ThreadLocal被用作TheadLocalMap的弱引用key,這種設計也是ThreadLocal被討論內存泄露的熱點問題,因此有必要了解一下什么是弱引用。
3.1 強引用
強引用雖然在開發過程中并不怎么提及,但是無處不在,例如我們在一個對象中通過如下代碼實例化一個StringBuffer對象
StringBuffer buffer = new StringBuffer();
我們知道StringBuffer的實例通常是被創建在堆中的,而當前對象持有該StringBuffer對象的引用,以便后續的訪問,這個引用,就是一個強引用。
對GC知識比較熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般默認是可達性算法,即在每一輪GC的時候,選定一些對象作為GC ROOT,然后以它們為根發散遍歷,遍歷完成之后,如果一個對象不被任何GC ROOT引用,那么它就是不可達對象,則在接下來的GC過程中很可能會被回收。
強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的交互。具體來說,如果一個對象通過一串強引用鏈接可到達(Strongly reachable),它是不會被回收的。如果你不想讓你正在使用的對象被回收,這就正是你所需要的。
3.2 軟引用
軟引用是用來描述一些還有用但是并非必須的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收返回之后進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。JDK1.2之后提供了SoftReference來實現軟引用。
相對于強引用,軟引用在內存充足時可能不會被回收,在內存不夠時會被回收。
3.3 弱引用
弱引用也是用來描述非必須的對象的,但它的強度更弱,被弱引用關聯的對象只能生存到下一次GC發生之前,也就是說下一次GC就會被回收。JDK1.2之后,提供了WeakReference來實現弱引用。
3.4 虛引用
虛引用也成為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個瑞祥是否有虛引用的存在,完全不會對其生存時間造成影響,也無法通過虛引用來取得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是在這個對象被GC時收到一個系統通知。JDK1.2之后提供了PhantomReference來實現虛引用
4. 可能的內存泄露分析
了解了ThreadLocal的內部模型以及弱引用,接下來可以分析一下是否有內存泄露的可能以及如何避免。
4.1 內存泄露分析
根據上一節的內存模型圖我們可以知道,由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關聯著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對象引用,則在下一次GC的時候,ThreadLocal實例就會被回收,那么此時ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會被外部訪問到,而且只要Thread實例一直存在,Thread實例就強引用著ThreadLocalMap,因此ThreadLocalMap就不會被回收,那么這里K為null的V就一直占用著內存。
綜上,發生內存泄露的條件是
- ThreadLocal實例沒有被外部強引用,比如我們假設在提交到線程池的task中實例化的ThreadLocal對象,當task結束時,ThreadLocal的強引用也就結束了
- ThreadLocal實例被回收,但是在ThreadLocalMap中的V沒有被任何清理機制有效清理
- 當前Thread實例一直存在,則會一直強引用著ThreadLocalMap,也就是說ThreadLocalMap也不會被GC
也就是說,如果Thread實例還在,但是ThreadLocal實例卻不在了,則ThreadLocal實例作為key所關聯的value無法被外部訪問,卻還被強引用著,因此出現了內存泄露。
也就是說,我們回答了文章開頭的第一個問題,ThreadLocal如果使用的不當,是有可能引起內存泄露的,雖然觸發的場景不算很容易。
這里要額外說明一下,這里說的內存泄露,是因為對其內存模型和設計不了解,且編碼時不注意導致的內存管理失聯,而不是有意為之的一直強引用或者頻繁申請大內存。比如如果編碼時不停的人為塞一些很大的對象,而且一直持有引用最終導致OOM,不能算作ThreadLocal導致的“內存泄露”,只是代碼寫的不當而已!
4.2 TheadLocal本身的優化
進一步分析ThreadLocalMap的代碼,可以發現ThreadLocalMap內部也是做了一定的優化的
/**
* 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值的時候,有一定的幾率會執行replaceStaleEntry(key, value, i)
方法,其作用就是將當前的值替換掉以前的key為null的值,重復利用了空間。
5. ThreadLocal使用建議
通過前面幾節的分析,我們基本弄清楚了ThreadLocal相關設計和內存模型,對于是否會發生內存泄露做了分析,下面總結下幾點建議:
- 當需要存儲線程私有變量的時候,可以考慮使用ThreadLocal來實現
- 當需要實現線程安全的變量時,可以考慮使用ThreadLocal來實現
- 當需要減少線程資源競爭的時候,可以考慮使用ThreadLocal來實現
- 注意Thread實例和ThreadLocal實例的生存周期,因為他們直接關聯著存儲數據的生命周期
- 如果頻繁的在線程中new ThreadLocal對象,在使用結束時,最好調用ThreadLocal.remove來釋放其value的引用,避免在ThreadLocal被回收時value無法被訪問卻又占用著內存
其實對于ThreadLocalMap還有很多設計,關于其詳細內容,可以參考文后參考文章的最后一篇
參考文章