為什么大家都說 ThreadLocal 存在內(nèi)存泄漏的風(fēng)險?
01、背景介紹
在 Java web 項目中,想必很多的同學(xué)對ThreadLocal這個類并不陌生,它最常用的應(yīng)用場景就是用來做對象的跨層傳遞,避免多次傳遞,打破層次之間的約束。
比如下面這個HttpServletRequest參數(shù)傳遞的簡單例子!
public class RequestLocal {
/**
* 線程本地變量
*/
private static ThreadLocal<HttpServletRequest> local = new ThreadLocal<>();
/**
* 存儲請求對象
* @param request
*/
public static void set(HttpServletRequest request){
local.set(request);
}
/**
* 獲取請求對象
* @return
*/
public static HttpServletRequest get(){
return local.get();
}
/**
* 移除請求對象
* @return
*/
public static void remove(){
local.remove();
}
}
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 存儲請求對象變量
RequestLocal.set(req);
try {
// 業(yè)務(wù)邏輯...
} finally {
// 請求完畢之后,移除請求對象變量
RequestLocal.remove();
}
}
}
// 在需要的地方,通過 RequestLocal 類獲取HttpServletRequest對象
HttpServletRequest request = RequestLocal.get();
看完以上示例,相信大家對ThreadLocal的使用已經(jīng)有了大致的認(rèn)識。
當(dāng)然ThreadLocal的作用還不僅限如此,作為 Java 多線程模塊的一部分,ThreadLocal也經(jīng)常被一些面試官作為知識點用來提問,因此只有理解透徹了,回答才能更加游刃有余。
下面我們從ThreadLocal類的源碼解析到使用方式做一次總結(jié),如果有不正之處,請多多諒解,并歡迎批評指出。
02、源碼剖析
ThreadLocal類,也經(jīng)常被叫做線程本地變量,也有的叫做本地線程變量,意思其實差不多,通俗的解釋:ThreadLocal作用是為變量在每個線程中創(chuàng)建一個副本,這樣每個線程就可以訪問自己內(nèi)部的副本變量;同時,該變量對其他線程而言是封閉且隔離的。
字面的意思很容易理解,但是實際上ThreadLocal類的實現(xiàn)原理還有點復(fù)雜。
打開ThreadLocal類,它總共有 4 個public方法,內(nèi)容如下!
方法 | 描述 |
public void set(T value) | 設(shè)置當(dāng)前線程變量 |
public T get() | 獲取當(dāng)前線程變量 |
public void remove() | 移除當(dāng)前線程設(shè)置的變量 |
public static ThreadLocal withInitial(Supplier supplier) | 自定義初始化當(dāng)前線程的默認(rèn)值 |
其中使用最多的就是set()、get()和remove()方法,至于withInitial()方法,一般在ThreadLocal對象初始化的時候,給定一個默認(rèn)值,例如下面這個例子!
// 給所有線程初始化一個變量 1
private static ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 1);
下面我們重點來剖析以上三個方法的源碼,最后總結(jié)如何正確的使用。
以下源碼解析均基于jdk1.8。
2.1、set 方法
打開ThreadLocal類,set()方法的源碼如下!
public void set(T value) {
// 首先獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 獲取當(dāng)前線程中的變量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果不為空,就設(shè)置值
if (map != null)
map.set(this, value);
else
// 如果為空,初始化一個ThreadLocalMap變量,其中key為當(dāng)前的threadlocal變量
createMap(t, value);
}
我們繼續(xù)看看createMap()方法的源碼,內(nèi)容如下!
void createMap(Thread t, T firstValue) {
// 初始化一個 ThreadLocalMap 對象,并賦予給 Thread 對象
// 可以發(fā)現(xiàn),其實 ThreadLocalMap 是 Thread 類的一個屬性變量
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// INITIAL_CAPACITY 變量的初始值為 16
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;
}
}
從上面的源碼上你會發(fā)現(xiàn),通過ThreadLocal類設(shè)置的變量,最終保存在每個線程自己的ThreadLocal.ThreadLocalMap對象中,其中key是當(dāng)前線程的ThreadLocal變量,value就是我們設(shè)置的變量。
基于這點,可以得出一個結(jié)論:每個線程設(shè)置的變量只有自己可見,其它線程無法訪問,因為這個變量是線程自己獨有的屬性。
從上面的源碼也可以看出,真正負(fù)責(zé)存儲value變量的是Entry靜態(tài)類,并且這個類繼承了一個WeakReference類。稍有不同的是,Entry靜態(tài)類中的key是一個弱引用類型對象,而value是一個強(qiáng)引用類型對象。這樣設(shè)計的好處在于,弱引用的對象更容易被 GC 回收,當(dāng)ThreadLocal對象不再被其他對象使用時,可以被垃圾回收器自動回收,避免可能的內(nèi)存泄漏。關(guān)于這一點,我們在下文再詳細(xì)的介紹。
最后我們再來看看map.set(this, value)這個方法的源碼邏輯,內(nèi)容如下!
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根據(jù)hash和位運算,計算出數(shù)組中的存儲位置
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)遍歷檢查計算出來的位置上是否被占用
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 進(jìn)入循環(huán)體內(nèi),說明當(dāng)前位置已經(jīng)被占用了
ThreadLocal<?> k = e.get();
// 如果key相同,直接進(jìn)行覆蓋
if (k == key) {
e.value = value;
return;
}
// 如果key為空,說明key被回收了,重新覆蓋
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 當(dāng)沒有被占用,循環(huán)結(jié)束之后,取最后計算的空位,進(jìn)行存儲
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
// 下標(biāo)依次自增
return ((i + 1 < len) ? i + 1 : 0);
}
從上面的源碼分析可以看出,ThreadLocalMap和HashMap,雖然都是鍵值對的方式存儲數(shù)據(jù),當(dāng)在數(shù)組中存儲數(shù)據(jù)的下表沖突時,存儲數(shù)據(jù)的方式有很大的不同。jdk1.8種的HashMap采用的是鏈表法和紅黑樹來解決下表沖突,當(dāng)
ThreadLocalMap采用的是開放尋址法來解決hash沖突,簡單的說就是當(dāng)hash出來的存儲位置相同但key不一樣時,會繼續(xù)尋找下一個存儲位置,直到找到空位來存儲數(shù)據(jù)。
圖片
而jdk1.7中的HashMap采用的是鏈表法來解決hash沖突,當(dāng)hash出來的存儲位置相同但key不一樣時,會將變量通過鏈表的方式掛在數(shù)組節(jié)點上。
為了實現(xiàn)更高的讀寫效率,jdk1.8中的HashMap就更為復(fù)雜了,當(dāng)沖突的鏈表長度超過 8 時,鏈表會轉(zhuǎn)變成紅黑樹,在此不做過多的講解,有興趣的同學(xué)可以翻看關(guān)于HashMap的源碼分析文章。
一路分析下來,是不是感覺set()方法還是挺復(fù)雜的,總結(jié)下來set()大致的邏輯有以下幾個步驟:
- 1.首先獲取當(dāng)前線程對象,檢查當(dāng)前線程中的ThreadLocalMap是否存在
- 2.如果不存在,就給線程創(chuàng)建一個ThreadLocal.ThreadLocalMap對象
- 3.如果存在,就設(shè)置值,存儲過程中如果存在 hash 沖突時,采用開放尋址法,重新找一個空位進(jìn)行存儲
2.2、get 方法
了解完set()方法之后,get()方法就更容易了,get()方法的源碼如下!
public T get() {
// 獲取當(dāng)前線程對象
Thread t = Thread.currentThread();
// 從當(dāng)前線程對象中獲取 ThreadLocalMap 對象
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;
}
}
// 如果沒有值,重新初始化默認(rèn)值
return setInitialValue();
}
這里我們要重點看下 map.getEntry(this)這個方法,源碼如下!
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果找到key,直接返回
if (e != null && e.get() == key)
return e;
else
// 如果找不到,就嘗試清理,如果你總是訪問存在的key,那么這個清理永遠(yuǎn)不會進(jìn)來
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
// e指的是entry ,也就是一個弱引用
ThreadLocal<?> k = e.get();
// 如果找到了,就返回
if (k == key)
return e;
if (k == null)
// 如果key為null,說明已經(jīng)被回收了,同時將value設(shè)置為null,以便進(jìn)行回收
expungeStaleEntry(i);
else
// 如果key不是要找的那個,那說明有hash沖突,繼續(xù)找下一個entry
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
從上面的源碼可以看出,get()方法邏輯,總共有以下幾個步驟:
- 1.首先獲取當(dāng)前線程對象,從當(dāng)前線程對象中獲取 ThreadLocalMap 對象
- 2.然后判斷ThreadLocalMap是否存在,如果存在,就嘗試去獲取最終的value
- 3.如果不存在,就重新初始化默認(rèn)值,以便清理舊的value值
其中expungeStaleEntry()方法是真正用于清理value值的,setInitialValue()方法也具備清理舊的value變量作用。
從上面的代碼可以看出,ThreadLocal為了清楚value變量,花了不少的心思,其實本質(zhì)都是為了防止ThreadLocal出現(xiàn)可能的內(nèi)存泄漏。
2.3、remove 方法
我們再來看看remove()方法,源碼如下!
public void remove() {
// 獲取當(dāng)前線程里面的 ThreadLocalMap 對象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 如果不為空,就移除
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 循環(huán)遍歷目標(biāo)key,然后將key和value都設(shè)置為null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
// 清理value值
expungeStaleEntry(i);
return;
}
}
}
remove()方法邏輯比較簡單,首先獲取當(dāng)前線程的ThreadLocalMap對象,然后循環(huán)遍歷key,將目標(biāo)key以及對應(yīng)的value都設(shè)置為null。
從以上的源碼剖析中,可以得出一個結(jié)論:不管是set()、get()還是remove(),其實都會主動清理無效的value數(shù)據(jù),因此實際開發(fā)過程中,沒有必要過于擔(dān)心內(nèi)存泄漏的問題。
03、為什么要用 WeakReference?
另外細(xì)心的同學(xué)可能會發(fā)現(xiàn),ThreadLocal中真正負(fù)責(zé)存儲key和value變量的是Entry靜態(tài)類,并且它繼承了一個WeakReference類。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
關(guān)于WeakReference類,我們在上文只是簡單的說了一下,可能有的同學(xué)不太清楚,這個再次簡要的介紹一下。
了解過WeakHashMap類的同學(xué),可能對WeakReference有印象,它表示當(dāng)前對象為弱引用類型。
在 Java 中,對象有四種引用類型,分別是:強(qiáng)引用、軟引用、弱引用和虛引用,級別從高依次到低。
不同引用類型的對象,GC 回收的方式也不一樣,對于強(qiáng)引用類型,不會被垃圾收集器回收,即使當(dāng)內(nèi)存不足時,另可拋異常也不會主動回收,防止程序出現(xiàn)異常,通常我們自定義的類,初始化的對象都是強(qiáng)引用類型;對于軟引用類型的對象,當(dāng)不存在外部強(qiáng)引用的時候,GC 會在內(nèi)存不足的時候,進(jìn)行回收;對于弱引用類型的對象,當(dāng)不存在外部強(qiáng)引用的時候,GC 掃描到時會進(jìn)行回收;對于虛引用,GC 會在任何時候都可能進(jìn)行回收。
下面我們看一個簡單的示例,更容易直觀的了解它。
public static void main(String[] args) {
Map weakHashMap = new WeakHashMap();
//向weakHashMap中添加4個元素
for (int i = 0; i < 3; i++) {
weakHashMap.put("key-"+i, "value-"+ i);
}
//輸出添加的元素
System.out.println("數(shù)組長度:"+weakHashMap.size() + ",輸出結(jié)果:" + weakHashMap);
//主動觸發(fā)一次GC
System.gc();
//再輸出添加的元素
System.out.println("數(shù)組長度:"+weakHashMap.size() + ",輸出結(jié)果:" + weakHashMap);
}
輸出結(jié)果:
數(shù)組長度:3,輸出結(jié)果:{key-2=value-2, key-1=value-1, key-0=value-0}
數(shù)組長度:3,輸出結(jié)果:{}
以上存儲的弱引用對象,與外部對象沒有強(qiáng)關(guān)聯(lián),當(dāng)主動調(diào)用 GC 回收器的時候,再次查詢WeakHashMap里面的數(shù)據(jù)的時候,弱引用對象收回,所以內(nèi)容為空。其中WeakHashMap類底層使用的數(shù)據(jù)存儲對象,也是繼承了WeakReference。
采用WeakReference這種弱引用的方式,當(dāng)不存在外部強(qiáng)引用的時候,就會被垃圾收集器自動回收掉,減小內(nèi)存空間壓力。
需要注意的是,Entry靜態(tài)類中僅僅只是key被設(shè)計成弱引用類型,value依然是強(qiáng)引用類型。
回歸正題,為什么ThreadLocalMap類中的Entry靜態(tài)類中的key需要被設(shè)計成弱引用類型?
我們先看一張Entry對象的依賴圖!
圖片
如上圖所示,Entry持有ThreadLocal對象的引用,如果沒有設(shè)置引用類型,這個引用鏈就全是強(qiáng)引用,當(dāng)線程沒有結(jié)束時,它持有的強(qiáng)引用,包括遞歸下去的所有強(qiáng)引用都不會被垃圾回收器回收;只有當(dāng)線程生命周期結(jié)束時,才會被回收。
哪怕顯式的設(shè)置threadLocal = null,它也無法被垃圾收集器回收,因為Entry和key存在強(qiáng)關(guān)聯(lián)!
如果Entry中的key設(shè)置成弱引用,當(dāng)threadLocal = null時,key就可以被垃圾收集器回收,進(jìn)一步減少內(nèi)存使用空間。
但是也僅僅只是回收key,不能回收value,如果這個線程運行時間非常長,又沒有調(diào)用set()、get()或者remove()方法,隨著線程數(shù)的增多可能會有內(nèi)存溢出的風(fēng)險。
因此在實際的使用中,想要徹底回收value,使用完之后可以顯式調(diào)用一下remove()方法。
04、應(yīng)用介紹
通過以上的源碼分析,相信大家對ThreadLocal類已經(jīng)有了一些認(rèn)識,它主要的作用是在線程內(nèi)實現(xiàn)變量的傳遞,每個線程只能看到自己設(shè)定的變量。
我們可以看一個簡單的示例!
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("main");
for (int i = 0; i < 5; i++) {
final int j = i;
new Thread(new Runnable() {
@Override
public void run() {
// 設(shè)置變量
threadLocal.set(String.valueOf(j));
// 獲取設(shè)置的變量
System.out.println("thread name:" + Thread.currentThread().getName() + ", 內(nèi)容:" + threadLocal.get());
}
}).start();
}
System.out.println("thread name:" + Thread.currentThread().getName() + ", 內(nèi)容:" + threadLocal.get());
}
輸出結(jié)果:
thread name:Thread-0, 內(nèi)容:0
thread name:Thread-1, 內(nèi)容:1
thread name:Thread-2, 內(nèi)容:2
thread name:Thread-3, 內(nèi)容:3
thread name:main, 內(nèi)容:main
thread name:Thread-4, 內(nèi)容:4
從運行結(jié)果上可以很清晰的看出,每個線程只能看到自己設(shè)置的變量,其它線程不可見。
ThreadLocal可以實現(xiàn)線程之間的數(shù)據(jù)隔離,在實際的業(yè)務(wù)開發(fā)中,使用非常廣泛,例如文章開頭介紹的HttpServletRequest參數(shù)的上下文傳遞。
05、小結(jié)
最后我們來總結(jié)一下,ThreadLocal類經(jīng)常被叫做線程本地變量,它確保每個線程的ThreadLocal變量都是各自獨立的,其它線程無法訪問,實現(xiàn)線程之間數(shù)據(jù)隔離的效果。
ThreadLocal適合在一個線程的處理流程中實現(xiàn)參數(shù)上下文的傳遞,避免同一個參數(shù)在所有的方法中傳遞。
使用ThreadLocal時,如果當(dāng)前線程中的變量已經(jīng)使用完畢并且永久不在使用,推薦手動調(diào)用移除remove()方法,可以采用try ... finally結(jié)構(gòu),并在finally中清除變量,防止存在潛在的內(nèi)存溢出風(fēng)險。
06、參考
1、https://www.cnblogs.com/xrq730/p/4854813.html
2、https://www.cnblogs.com/xrq730/p/4854820.html
3、https://zhuanlan.zhihu.com/p/102744180