相關文章:
談到 ThreadLocal
在使用的時候的注意事項,其中有一個就是內存泄露,那麼 ThreadLocal
會發生內存泄漏嗎?或者說什麼情況下 ThreadLocal
會內存泄漏呢?
存入超大對象
無須多言,存入“超大對象”肯定會有內存泄漏的風險。
key 爲 null 造成內存泄漏
這裏引用一篇博客的觀點:
Threadlocal 裏面使用了一個存在弱引用的 Map,當釋放掉 Threadlocal 的強引用以後,Map裏面的 value 卻沒有被回收,而這塊 value 永遠不會被訪問到了, 所以存在着內存泄露。
https://my.oschina.net/u/658658/blog/1830812
很多文章都是這個觀點,主要說法是:由於 ThreadLocal.ThreadLocalMap.Entry
中的 key
是弱引用,會存在 key
爲 null
的情況,這時候 value
無法被訪問到,但是由於 value
被 Entry
關聯,最主要原因是大部分情況下會使用線程池,造成 value
所佔內存無法被釋放,從而導致內存泄漏。
其實我們一般使用都會將 ThreadLocal
作爲一個類級別變量,會被強引用,這樣使用是不會出現"key
爲 null
的情況"。
但是不可否認的是,如果將 ThreadLocal
作爲線程池中線程的局部變量的確會出現"key
爲 null
的情況"。可以先看一個例子:
public class ThreadLocalOomDemo1 {
private static final ExecutorService FIXED_EXECUTOR = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
FIXED_EXECUTOR.submit(() -> {
ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected void finalize() {
System.out.println("finalize........");
}
};
threadLocal.set("a");
System.out.println(threadLocal.get());
});
System.out.println("-------------------");
sleep(100);
System.gc();
FIXED_EXECUTOR.submit(()->{
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
});
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出結果:
[0.002s][warning][gc] -XX:+PrintGC is deprecated. Will use -Xlog:gc instead.
[0.013s][info ][gc] Using G1
-------------------
a
[0.296s][info ][gc] GC(0) Pause Full (System.gc()) 5M->3M(17M) 4.705ms
finalize........
pool-1-thread-1
Debug
調試:
發現的確會存在出現 key
爲 null
,已被 GC 的情況,此時 value
是無法獲取的。
ThreadLocalMap#set
ThreadLocal
中的數據本質是存儲到 ThreadLocalMap
中的,看下 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);
//出現 hash 衝突,使用開放地址法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 相同,覆蓋
if (k == key) {
e.value = value;
return;
}
//key 爲 null
if (k == null) {
//進入替換方法
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
當出現 hash
衝突的時候,使用開放地址法向後查找,出現 key
爲 null
,通過replaceStaleEntry
方法處理。
ThreadLocalMap#get
同樣地,再看看 get
方法:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//發生 hash 衝突
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//刪除過期數據
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
也就是說當出現 hash
衝突的時候,get
方法繼續查找,同樣會清除循環中遇到的過期 entry
。
總結
ThreadLocal
的確會出現內存溢出的情況,但是其實說ThreadLocal
是否會出現內存泄漏,這個表述本身就有問題,真正應該關注的是在線程池中複用的Thread
對象的成員變量是否會出現內存泄漏,- 不能直接以
key
出現null
,value
無法被訪問回收作爲緣由,我們平常都是將ThreadLocal
作爲類級別變量,幾乎不可能會出現key
爲null
的情況,即使出現了這樣的情況,ThreadLocal
在設計時 通過get
和set
方法已經做了key
爲null
的優化處理; - 使用完
ThreadLocal
後,都應該調用它的remove
方法,避免可能出現的一段時間數據保留(如存入一個大對象,key
GC 後,未出現hash
衝突,那麼這個對象會有一段時間長期存在),也可以避免線程複用時出現髒數據;
References
- https://www.jianshu.com/p/56f64e3c1b6c
- https://www.iteye.com/blog/liuinsect-1827012
歡迎關注公衆號