該文章屬于Android Handler系列文章,如果想了解更多,請點(diǎn)擊
《Android Handler機(jī)制之總目錄》
前言
要想了解Android 的Handle機(jī)制,我們首先要了解ThreadLocal,根據(jù)字面意思我們都能猜出個(gè)大概。就是線程本地變量。那么我們把變量存儲(chǔ)在本地有什么好處呢?其中的原理又是什么呢?下面我們就一起來討論一下ThreadLocal的使用與原理。
ThreadLocal簡單介紹
該類提供線程局部變量。這些變量不同于它們的正常變量,即每一個(gè)線程訪問自身的局部變量時(shí),都有它自己的,獨(dú)立初始化的副本。該變量通常是與線程關(guān)聯(lián)的私有靜態(tài)字段,列如用于ID或事物ID。大家看了介紹后,有可能還是不了解其主要的主要作用,簡單的畫個(gè)圖幫助大家理解。
從圖上可以看出,通過ThreadLocal,每個(gè)線程都能獲取自己線程內(nèi)部的私有變量,有可能大家覺得無圖無真相,“你一個(gè)人在那里神吹,我怎么知道你說的對還是不對呢?”,下面我們通過具體的例子詳細(xì)的介紹,來看下面的代碼。
class ThreadLocalTest {
//會(huì)出現(xiàn)內(nèi)存泄漏的問題,下文會(huì)描述
private static ThreadLocal<String> mThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
mThreadLocal.set("線程main");
new Thread(new A()).start();
new Thread(new B()).start();
System.out.println(mThreadLocal.get());
}
static class A implements Runnable {
@Override
public void run() {
mThreadLocal.set("線程A");
System.out.println(mThreadLocal.get());
}
}
static class B implements Runnable {
@Override
public void run() {
mThreadLocal.set("線程B");
System.out.println(mThreadLocal.get());
}
}
}
在上訴代碼中,我們在主線程中設(shè)置mThreadLocal的值為"線程main",在線程A中設(shè)置為”線程A“,在線程B中設(shè)置為”線程B",運(yùn)行程序打印結(jié)果如下圖所示:
main
線程A
線程B
從上面結(jié)果可以看出,雖然是在不同的線程中訪問的同一個(gè)變量mThreadLocal,但是他們通過ThreadLocl獲取到的值卻是不一樣的。也就驗(yàn)證了上面我們所畫的圖是正確的了,那么現(xiàn)在,我們已經(jīng)知道了ThreadLocal的用法,那么我們現(xiàn)在來看看其中的內(nèi)部原理。
ThreadLocal原理
為了幫助大家快速的知曉ThreadLocal原理,這里我將ThreadLocal的原理用下圖表示出來了:
在上圖中我們可以發(fā)現(xiàn),整個(gè)ThreadLocal的使用都涉及到線程中ThreadLocalMap
,雖然我們在外部調(diào)用的是ThreadLocal.set(value)方法,但本質(zhì)是通過線程中的ThreadLocalMap中的set(key,value)方法
,那么通過該情況我們大致也能猜出get方法也是通過ThreadLocalMap。那么接下來我們一起來看看ThreadLocal中set與get方法的具體實(shí)現(xiàn)與ThreadLocalMap的具體結(jié)構(gòu)。
ThreadLocal的set方法
在使用ThreadLocal時(shí),我們會(huì)調(diào)用ThreadLocal的set(T value)方法對線程中的私有變量設(shè)置,我們來查看ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();//獲取當(dāng)前線程
ThreadLocalMap map = getMap(t);//拿到線程的LocalMap
if (map != null)
map.set(this, value);//設(shè)值 key->當(dāng)前ThreadLocal對象。value->為當(dāng)前賦的值
else
createMap(t, value);//創(chuàng)建新的ThreadLocalMap并設(shè)值
}
當(dāng)調(diào)用set(T value) 方法時(shí),方法內(nèi)部會(huì)獲取當(dāng)前線程中的ThreadLocalMap,獲取后進(jìn)行判斷,如果不為空,就調(diào)用ThreadLocalMap的set方法(其中key為當(dāng)前ThreadLocal對象,value為當(dāng)前賦的值)。反之,讓當(dāng)前線程創(chuàng)建新的ThreadLocalMap并設(shè)值,其中g(shù)etMap()與createMap()方法具體代碼如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
簡簡單單的通過ThreadLocalMap的set()方法,我們已經(jīng)大致了解了。ThreadLocal為什么能操作線程內(nèi)的私有數(shù)據(jù)了,ThreadLocal中所有的數(shù)據(jù)操作都與線程中的ThreadLocalMap有關(guān),同時(shí)那我們接下來看看ThreadLocalMap相關(guān)代碼。
ThreadLocalMap 內(nèi)部結(jié)構(gòu)
ThreadLocalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,官方的注釋寫的很全面,這里我大概的翻譯了一下,ThreadLocalMap是為了維護(hù)線程私有值創(chuàng)建的自定義哈希映射。其中線程的私有數(shù)據(jù)都是非常大且使用壽命長的數(shù)據(jù)(其實(shí)想一想,為什么要存儲(chǔ)這些數(shù)據(jù)呢,第一是為了把常用的數(shù)據(jù)放入線程中提高了訪問的速度,第二是如果數(shù)據(jù)是非常大的,避免了該數(shù)據(jù)頻繁的創(chuàng)建,不僅解決了存儲(chǔ)空間的問題,也減少了不必要的IO消耗)。
ThreadLocalMap 具體代碼如下:
static class ThreadLocalMap {
//存儲(chǔ)的數(shù)據(jù)為Entry,且key為弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//table初始容量
private static final int INITIAL_CAPACITY = 16;
//table 用于存儲(chǔ)數(shù)據(jù)
private Entry[] table;
//負(fù)載因子,用于數(shù)組容量擴(kuò)容
private int threshold; // Default to 0
//負(fù)載因子,默認(rèn)情況下為當(dāng)前數(shù)組長度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//第一次放入Entry數(shù)據(jù)時(shí),初始化數(shù)組長度,定義擴(kuò)容閥值,
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始化數(shù)組長度為16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);//閥值為當(dāng)前數(shù)組默認(rèn)長度的2/3
}
從代碼中可以看出,雖然官方申明為ThreadLocalMap是一個(gè)哈希表,但是它與我們傳統(tǒng)認(rèn)識(shí)的HashMap等哈希表內(nèi)部結(jié)構(gòu)是不一樣的。ThreadLocalMap內(nèi)部僅僅維護(hù)了Entry[] table,數(shù)組。其中Entry實(shí)體中對應(yīng)的key為弱引用(下文會(huì)將為什么會(huì)用弱引用),在第一次放入數(shù)據(jù)時(shí),會(huì)初始化數(shù)組長度(為16),定義數(shù)組擴(kuò)容閥值(當(dāng)前默認(rèn)數(shù)組長度的2/3)。
ThreadLocalMap 的set()方法
private void set(ThreadLocal<?> key, Object value) {
//根據(jù)哈希值計(jì)算位置
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//判斷當(dāng)前位置是否有數(shù)據(jù),如果key值相同,就替換,如果不同則找空位放數(shù)據(jù)。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//獲取下一個(gè)位置的數(shù)據(jù)
ThreadLocal<?> k = e.get();
//判斷key值相同否,如果是直接覆蓋 (第一種情況)
if (k == key) {
e.value = value;
return;
}
//如果當(dāng)前Entry對象對應(yīng)Key值為null,則清空所有Key為null的數(shù)據(jù)(第二種情況)
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//以上情況都不滿足,直接添加(第三種情況)
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果當(dāng)前數(shù)組到達(dá)閥值,那么就進(jìn)行擴(kuò)容。
rehash();
}
直接通過代碼理解比較困難,這里直接將set方法分為了三個(gè)步驟,下面我們我們就分別對這個(gè)三個(gè)步驟,分別通過圖與代碼的方式講解。
第一種情況, Key值相同
如果當(dāng)前數(shù)組中,如果當(dāng)前位置對應(yīng)的Entry的key值與新添加的Entry的key值相同,直接進(jìn)行覆蓋操作。具體情況如下圖所示
如果當(dāng)前數(shù)組中。存在key值相同的情況,ThreadLocal內(nèi)部操作是直接覆蓋的。這種情況就不過多的介紹了。
第二種情況,如果當(dāng)前位置對應(yīng)Entry的Key值為null
第二種情況相對來說比較復(fù)雜,這里先給圖,然后會(huì)根據(jù)具體代碼來講解。
從圖中我們可以看出來。當(dāng)我們添加新Entry(key=19,value =200,index = 3)時(shí),數(shù)組中已經(jīng)存在舊Entry(key =null,value = 19),當(dāng)出現(xiàn)這種情況是,方法內(nèi)部會(huì)將新Entry的值全部賦值到舊Entry中,同時(shí)會(huì)將所有數(shù)組中key為null的Entry全部置為null(圖中大黃色數(shù)據(jù))。在源碼中,當(dāng)新Entry對應(yīng)位置存在數(shù)據(jù),且key為null的情況下,會(huì)走replaceStaleEntry
方法。具體代碼如下:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//記錄當(dāng)前要清除的位置
int slotToExpunge = staleSlot;
//往前找,找到第一個(gè)過期的Entry(key為空)
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)//判斷引用是否為空,如果為空,擦除的位置為第一個(gè)過期的Entry的位置
slotToExpunge = i;
//往后找,找到最后一個(gè)過期的Entry(key為空),
for (int i = nextIndex(staleSlot, len);//這里要注意獲得位置有可能為0,
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//在往后找的時(shí)候,如果獲取key值相同的。那么就重新賦值。
if (k == key) {
//賦值到之前傳入的staleSlot對應(yīng)的位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果往前找的時(shí)候,沒有過期的Entry,那么就記錄當(dāng)前的位置(往后找相同key的位置)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//那么就清除slotToExpunge位置下所有key為null的數(shù)據(jù)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果往前找的時(shí)候,沒有過期的Entry,且key =null那么就記錄當(dāng)前的位置(往后找key==null位置)
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 把當(dāng)前key為null的對應(yīng)的數(shù)據(jù)置為null,并創(chuàng)建新的Entry在該位置上
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//如果往后找,沒有過期的實(shí)體,
//且staleSlot之前能找到第一個(gè)過期的Entry(key為空),
//那么就清除slotToExpunge位置下所有key為null的數(shù)據(jù)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
上面代碼看起來比較繁雜,但是大家仔細(xì)梳理就會(huì)發(fā)現(xiàn)其實(shí)該方法,主要對四種情況進(jìn)行了判斷,具體情況如下圖表所示:
我們已經(jīng)了解了replaceStaleEntry方法內(nèi)部會(huì)清除key==null的數(shù)據(jù),而其中具體的方法與expungeStaleEntry()方法與cleanSomeSlots()方法有關(guān),所以接下來我們來分析這兩個(gè)方法。看看其的具體實(shí)現(xiàn)。
expungeStaleEntry ()方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 將staleSlot位置下的數(shù)據(jù)置為null
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//往后找。
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {//清除key為null的數(shù)據(jù)
e.value = null;
tab[i] = null;
size--;
} else {
//如果key不為null,但是該key對應(yīng)的threadLocalHashCode發(fā)生變化,
//計(jì)算位置,并將元素放入新位置中。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;//返回最后一個(gè)tab[i]) != null的位置
}
expungeStaleEntry()方法主要干了三件事,第一件,將staleSlot的位置對應(yīng)的數(shù)據(jù)置為null,第二件,刪除并刪除此位置后對應(yīng)相關(guān)聯(lián)位置key = null的數(shù)據(jù)。第三件,如果如果key不為null,但是該key對應(yīng)的threadLocalHashCode發(fā)生變化,計(jì)算變化后的位置,并將元素放入新位置中。
cleanSomeSlots()方法
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;//如果有過期的數(shù)據(jù)被刪除,就返回true,反之false
}
在了解了expungeStaleEntry()方法后,再來理解cleanSomeSlots()方法就很簡單了。其中第一個(gè)參數(shù)表示開始掃描的位置,第二個(gè)參數(shù)是掃描的長度。從代碼我們明顯的看出。就是簡單的遍歷刪除所有位置下key==null的數(shù)據(jù)。
第三種情況,當(dāng)前對應(yīng)位置為null
圖上為了方便大家,理解清空上下數(shù)據(jù)的情況,我并沒有重新計(jì)算位置(希望大家注意?。。。?/strong>
看到這里,為了方便大家避免不必要的查閱代碼,我直接將代碼貼出來了。代碼如下。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
從上述代碼其實(shí),大家很明顯的看出來,就是清除key==null的數(shù)據(jù),判斷當(dāng)前數(shù)據(jù)的長度是不是到達(dá)閥值(默認(rèn)沒擴(kuò)容前為INITIAL_CAPACITY *2/3,其中INITIAL_CAPACITY = 16),如果達(dá)到了重新計(jì)算數(shù)據(jù)的位置。關(guān)于rehash()方法,具體代碼如下:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
//清空所有key==null的數(shù)據(jù)
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
//重新計(jì)算key!=null的數(shù)據(jù)。新的數(shù)組長度為之前的兩倍
private void resize() {
//對原數(shù)組進(jìn)行擴(kuò)容,容量為之前的兩倍
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
//重新計(jì)算位置
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//重新計(jì)算閥值(負(fù)載因子)為擴(kuò)容之后的數(shù)組長度的2/3
setThreshold(newLen);
size = count;
table = newTab;
}
rehash內(nèi)部所有涉及到的方法,我都列舉出來了??梢钥闯鲈谔砑訑?shù)據(jù)的時(shí)候,會(huì)進(jìn)行判斷是否擴(kuò)容操作,如果需要擴(kuò)容,會(huì)清除所有的key==null的數(shù)據(jù),(也就是調(diào)用expungeStaleEntries()方法,其中expungeStaleEntry()方法已經(jīng)介紹了,就不過多描述),同時(shí)會(huì)重新計(jì)算數(shù)據(jù)中的位置。
ThreadLocal的get()方法
在了解了ThreadLocal的set()方法之后,我們看看怎么獲取ThreadLocal中的數(shù)據(jù),具體代碼如下:
public T get() {
Thread t = Thread.currentThread();//獲取當(dāng)前線程
ThreadLocalMap map = getMap(t);//拿到線程中的Map
if (map != null) {
//根據(jù)key值(ThreadLocal)對象,獲取存儲(chǔ)的數(shù)據(jù)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap為空,創(chuàng)建新的ThreadLocalMap
return setInitialValue();
}
其實(shí)ThreadLocal的get方法其實(shí)很簡單,就是獲取當(dāng)前線程中的ThreadLocalMap對象,如果沒有則創(chuàng)建,如果有,則根據(jù)當(dāng)前的 key(當(dāng)前ThreadLocal對象),獲取相應(yīng)的數(shù)據(jù)。其中內(nèi)部調(diào)用了ThreadLocalMap的getEntry()方法區(qū)獲取數(shù)據(jù),我們繼續(xù)查看getEntry()方法。
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
return getEntryAfterMiss(key, i, e);
}
getEntry()方法內(nèi)部也很簡單,也只是根據(jù)當(dāng)前key哈希后計(jì)算的位置,去找數(shù)組中對應(yīng)位置是否有數(shù)據(jù),如果有,直接將數(shù)據(jù)放回,如果沒有,則調(diào)用getEntryAfterMiss()方法,我們繼續(xù)往下看 。
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)//如果key相同,直接返回
return e;
if (k == null)//如果key==null,清除當(dāng)前位置下所有key=null的數(shù)據(jù)。
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;//沒有數(shù)據(jù)直接返回null
}
從上述代碼我們可以知道,如果從數(shù)組中,獲取的key==null的情況下,get方法內(nèi)部也會(huì)調(diào)用expungeStaleEntry()方法,去清除當(dāng)前位置所有key==null的數(shù)據(jù),也就是說現(xiàn)在不管是調(diào)用ThreadLocal的set()還是get()方法,都會(huì)去清除key==null的數(shù)據(jù)。
ThreadLocal內(nèi)存泄漏的問題
通過整個(gè)ThreadLocal機(jī)制的探索,我相信大家肯定會(huì)有一個(gè)疑惑,為什么ThreadLocalMap中采用是的是弱引用作為Key?
關(guān)于該問題,涉及到Java的回收機(jī)制。
為什么使用弱引用
在Java中判斷一個(gè)對象到底是不是需要回收,都跟引用相關(guān)。在Java中引用分為了4類。
- 強(qiáng)引用:只要引用存在,垃圾回收器永遠(yuǎn)不會(huì)回收Object obj = new Object();而這樣 obj對象對后面new Object的一個(gè)強(qiáng)引用,只有當(dāng)obj這個(gè)引用被釋放之后,對象才會(huì)被釋放掉。
- 軟引用:是用來描述,一些還有但并非必須的對象,對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收。(SoftReference)
- 弱引用:也是用來描述非必須的對象,但是它的強(qiáng)度要比軟引用更弱一些。被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器工作是,無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉被弱引用關(guān)聯(lián)的對象。(WeakReference)
- 虛引用:也被稱為幽靈引用,它是最弱的一種關(guān)系。一個(gè)對象是否有引用的存在,完全不會(huì)對其生存時(shí)間構(gòu)成影響,也無法通過一個(gè)虛引用來取得一個(gè)實(shí)例對象。
通過該知識(shí)點(diǎn)的了解后,我們再來了解為什么ThreadLocal不能使用強(qiáng)引用,如果key使用強(qiáng)引用,那么當(dāng)引用ThreadLocal的對象被回收了,但ThreadLocalMap中還持有ThreadLocal的強(qiáng)引用,如果沒有手動(dòng)刪除,ThreadLocal不會(huì)被回收,導(dǎo)致內(nèi)存泄漏。
弱引用帶來的問題
當(dāng)我們知道了為什么采用弱引用來作為ThreadLocalMap中的key的知識(shí)點(diǎn)后,這個(gè)時(shí)候又會(huì)引申出另一個(gè)問題不管是調(diào)用ThreadLocal的set()還是get()方法,都會(huì)去清除key==null的數(shù)據(jù)。為毛我們要去清除那些key==null的Entry呢?
為什么清除key==null的Entry主要有以下兩個(gè)原因,具體如下所示:
- 從上面我們已經(jīng)知道了,ThreadLocalMap使用ThreadLocal的弱引用作為key,也就是說,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用來引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢必會(huì)被回收。這樣一來,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,
- 如果當(dāng)前線程遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref(當(dāng)前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,那么將會(huì)導(dǎo)致這些Entry永遠(yuǎn)無法回收,造成內(nèi)存泄漏。
通過以上分析,我們可以了解在ThreadLocalMap的設(shè)計(jì)中其實(shí)已經(jīng)考慮到上述兩種情況,也加上了一些防護(hù)措施。(在調(diào)用ThreadLocal的get(),set(),remove()方法的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的Entry)
ThreadLocal使用注意事項(xiàng)
雖然ThreadLocal幫我們考慮了內(nèi)存泄漏的問題,為我們加上了一些防護(hù)措施。但是在實(shí)際使用中,我們還是需要注意避免以下兩種情況,下述兩種情況仍然有可能會(huì)導(dǎo)致內(nèi)存泄漏。
避免使用static的ThreadLocal
使用static修飾的ThreadLocal,延長了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏。具體原因是在Java虛擬機(jī)在加載類的過程中為靜態(tài)變量分配內(nèi)存。static變量的生命周期取決于類的生命周期,也就是說類被卸載時(shí),靜態(tài)變量才會(huì)被銷毀并釋放內(nèi)存空間。而類的生命周期結(jié)束與下面三個(gè)條件相關(guān)。
- 該類所有的實(shí)例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實(shí)例。
- 加載該類的ClassLoader已經(jīng)被回收。
- 該類對應(yīng)的java.lang.Class對象沒有任何地方被引用,沒有在任何地方通過反射訪問該類的方法。
分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法
其實(shí)理解起來也很簡單,就是第一次調(diào)用了ThreadLocal設(shè)置數(shù)據(jù)后,就不在調(diào)用get()、set()、remove()方法。也就是說現(xiàn)在ThreadLocalMap中就只有一條數(shù)據(jù)。那么如果調(diào)用ThreadLocal的線程一直不結(jié)束的話,即使ThreadLocal已經(jīng)被置為null(被GC回收),也一直存在一條強(qiáng)引用鏈:Thread Ref(當(dāng)前線程引用) -> Thread -> ThreadLocalMap -> Entry -> value,導(dǎo)致數(shù)據(jù)無法回收,造成內(nèi)存泄漏。
總結(jié)
- ThreadLocal本質(zhì)是操作線程中ThreadLocalMap來實(shí)現(xiàn)本地線程變量的存儲(chǔ)的
- ThreadLocalMap是采用數(shù)組的方式來存儲(chǔ)數(shù)據(jù),其中key(弱引用)指向當(dāng)前ThreadLocal對象,value為設(shè)的值
- ThreadLocal為內(nèi)存泄漏采取了處理措施,在調(diào)用ThreadLocal的get(),set(),remove()方法的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的Entry
- 在使用ThreadLocal的時(shí)候,我們?nèi)匀恍枰⒁猓苊馐褂胹tatic的ThreadLocal,分配使用了ThreadLocal后,一定要根據(jù)當(dāng)前線程的生命周期來判斷是否需要手動(dòng)的去清理ThreadLocalMap中清key==null的Entry。