ThreadLocal的內(nèi)存泄露真的存在?(第一話)
內(nèi)存泄露?
ThreadLocal的前世今生
ThreadLocal這個對象相信大家并不陌生,但是這玩意有人說會造成內(nèi)存泄露(感覺很嚴重的樣子),聽到這,怕是有些經(jīng)驗不足的童鞋會直接放棄使用它了。那么基于此,我們就研究一下,ThreadLocal到底是個什么東西,有什么用?又為什么會有人說他會造成內(nèi)存泄露呢?真的有內(nèi)存泄露嗎?你真正的會用ThreadLocal嗎?
如果你對上面的一連串發(fā)問,回答時帶著好像、似乎、差不多的詞匯,建議你看看本文。
01
ThreadLocal是什么?

ThreadLocal就是一個java類,是jdk【java.lang】包下的一個很普通的類。他有一個非常重要的靜態(tài)子類 ThreadLocalMap,該子類也有一個靜態(tài)子類叫Entry,可以說ThreadLocal基本就是靠這哥倆,完成他的功能。
02
ThreadLocal有什么用?
A
●??線程級別的變量存儲,成為線程的一個全局變量,從而可以讓變量在各個方法中均可使用而不用傳遞參數(shù)
B
● 在高并發(fā) 多線程環(huán)境,實現(xiàn)不同線程之間的數(shù)據(jù)隔離
很多人說ThreadLocal可以讓線程擁有一個共享變量的獨立副本,且不同線程間修改其自己的副本,不會影響其他線程。聽起來就像是ThreadLocal可以給每個線程拷貝該變量的副本,像Object.clone()一樣。經(jīng)過查看源碼,私以為線程間的數(shù)據(jù)隔離這種說法比較靠譜。實際上,錯誤的使用ThreadLocal,不理解它的實現(xiàn)機制的話,往往會造成變量的共享。一個線程修改變量,導(dǎo)致其他線程的變量的值也被修改了。

所以在工作中,用ThreadLocal的時候,有人說會造成內(nèi)存泄露,用的時候就很忐忑,不知道會有什么影響,不太敢用,于是下決心搞明白ThreadLocal到底該怎么用,會有什么問題?
沒有調(diào)查就沒有發(fā)言權(quán),如果你也對ThreadLocal很疑惑,那我們就一起來看看它吧。
03
ThreadLocal的用法
我們先從ThreadLocal的基本用法出發(fā),然后再根據(jù)代碼去一步一步的看源碼,這樣知道它的來龍去脈,就會好很多,比直接進入這個類里面去看源碼,思路要清楚的多。
1跨方法調(diào)用
public class Bean {//先定義一個類private int num;public int getNum() {return num;}public void setNum(int num) {this.num = num;}}
public class Demo {private static ThreadLocal<Bean> tl = new ThreadLocal<>();public static void main(String[] args) {//執(zhí)行main函數(shù)的線程是main線程Bean bean = tl.get();System.out.println("直接調(diào)用get方法輸出:"+bean);methodA();methodB();}private static void methodA() {Bean bean = new Bean();bean.setNum(100);tl.set(bean);}private static void methodB() {Bean bean = tl.get();System.out.println(bean.getNum());}}
demo很簡單,tl成員變量在main線程中被調(diào)用,methodA方法中set我們的Bean對象,在methodB方法中獲取該對象,然后輸出該對象的num值,輸出結(jié)果如下:
直接調(diào)用get方法輸出:null100
如此可以實現(xiàn)變量的跨方法調(diào)用,通常在非常復(fù)雜的業(yè)務(wù)邏輯中,在A方法中的變量要在G方法中使用,但是A-G之間的方法又不用這個變量,如果將該變量層層傳遞,就會顯得過于累贅。使用ThreadLocal就會簡單的多,看起來Bean變量就變成了main線程的全局變量,只要調(diào)用ThreadLocal的set方法以后,在main線程的其他地方就都可以使用了。這是ThreadLocal的其中一個用法。ThreadLocal不一定非要定義為成員變量,也可以在方法中定義。

2線程間的數(shù)據(jù)隔離
public class Demo2 {private static ThreadLocal<Bean> tl = new ThreadLocal<Bean>(){@Overrideprotected Bean initialValue() {return new Bean();}};public static void main(String[] args) {new Thread(()->{Bean bean = tl.get();bean.setNum(100);//線程設(shè)置num的值 驗證另一個線程的num值}).start();new Thread(()->{Bean bean = tl.get();//獲取Bean變量System.out.println(bean.getNum());//輸出num值}).start();}}
上面這個例子,首先在成員變量定義了一個ThreadLocal的子類,復(fù)寫了它的?initialValue()方法,這個方法很重要,待會源碼就能看到它了。然后在main函數(shù)中開啟了兩個線程,第一個線程將bean的num設(shè)置為100,第二個線程獲取到bean以后,再輸出num值。最后看到輸出結(jié)果是0。兩個線程都擁有了Bean變量,但是兩個bean是不一樣的。這樣線程修改自己的變量對其他線程的變量就不會造成影響。但是如果將return new Bean();換成一個已經(jīng)存在的Bean對象,那么結(jié)果就完全不一樣了。
這樣的用法有什么意義呢???在數(shù)據(jù)庫連接和session的管理中很有用。就不在多說了。
了解了用法以后,我們看一下源碼,為什么可以這么用?
04
源碼解析
01
首先看ThreadLocal的get方法
為什么?直接調(diào)用get方法輸出:null
public T get() {//ThreadLocal.get()Thread t = Thread.currentThread();//獲取當前線程ThreadLocalMap map = getMap(t);//獲取線程的屬性:ThreadLocalMapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T)e.value;return result;}}return setInitialValue();}
看一下getMap(t)的實現(xiàn)
ThreadLocalMap getMap(Thread t) {//ThreadLocal.getMap()return t.threadLocals;//直接取線程額threadLocals變量}
ThreadLocal.ThreadLocalMap?threadLocals?=?null;//Thread類的成員變量顯然?ThreadLocalMap?map 是線程的屬性?threadLocals?,第一次獲取肯定是null,所以要走setInitialValue()方法。暫時先不管ThreadLocalMap?是什么。
private T setInitialValue() {//ThreadLocal.setInitialValue()T value = initialValue();//demo2中復(fù)寫的方法Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
protected T initialValue() {//ThreadLocal.initialValue()return null;//直接new一個ThreadLocal 該方法返回null}
代碼看到此處就很明了了,Demo中我們直接new出來的ThreadLocal,所以調(diào)用get方法時,返回的是null,所以?直接調(diào)用get方法輸出:null;輸出沒有問題。由于線程的threadLocals變量還未被賦值,所以setInitialValue方法再次調(diào)用getMap(t)返回的仍然是null,這時候就要去創(chuàng)建這個ThreadLocalMap 了--createMap(t, value);
02
調(diào)用set方法后發(fā)生了什么
在Demo中的methodA中set了一個new Bean();我們有必要看一下ThreadLocal的set方法的實現(xiàn):
public void set(T value) {//ThreadLocal.set()Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}
發(fā)現(xiàn)這里也有一個createMap(t, value);方法,假如我們在使用ThreadLocal之前沒有調(diào)用過get方法而直接調(diào)用set方法,getMap(t)還是返回null,要走createMap(t, value);所以我們要重點看一下這個方法做了什么事情。
void createMap(Thread t, T firstValue) {//ThreadLocal.createMap()t.threadLocals = new ThreadLocalMap(this, firstValue);}
createMap(t, value)讓線程的threadLocals指向new出的ThreadLocalMap。此時 getMap(t)就不會再返回null了。
03
ThreadLocal中的數(shù)據(jù)存到哪了?
然后我們需要看一下new?ThreadLocalMap(this,?firstValue);的內(nèi)容,請注意this是當前ThreadLocal的引用,也就是Demo和Demo2中的ThreadLocal<Bean> tl成員變量。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//ThreadLocalMap構(gòu)造方法table = new Entry[INITIAL_CAPACITY];//初始化Entry數(shù)組 默認16int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//計算value應(yīng)該放在哪個槽table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);//設(shè)置擴容的臨界值}
這里又遇到了陌生的Entry類,這個類繼承了WeakReference。之前也提到了ThreadLocalMap是ThreadLocal的靜態(tài)子類,Entry是ThreadLocalMap的靜態(tài)子類。暫時先不管Entry為什么要繼承WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {Object value; /** The value associated with this ThreadLocal. */Entry(ThreadLocal<?> k, Object v) {super(k);value = v;//最終Bean存儲到了valuez中}}
上面兩個構(gòu)造函數(shù)很好理解,ThreadLocalMap構(gòu)造函數(shù)?創(chuàng)造了一個長度為16的Entry數(shù)組,然后根據(jù)ThreadLocal的hash值確定new出的Entry對象放在哪個數(shù)組的哪個下標位置。Entry的構(gòu)造函數(shù)有一個key,一個value。value被存儲到Entry對象的屬性O(shè)bject value中。至此告一段落,我們看一下變量的引用關(guān)系:
線程持有threadLocals 屬性,該屬性是ThreadLocalMap對象,ThreadLocalMap內(nèi)有一個Entry[]數(shù)組table,默認16大小,Entry對象持有ThreadLocal和Value,可以看成Entry[key,value]的形式。Entry的弱引用問題待會再講。

類的關(guān)系圖

為了便于理解,對象引用的關(guān)系如圖(不代表實際內(nèi)存位置)
我們的new出的Bean對象最終存到了Entry對象中的value屬性中。
04
ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)
所以ThreadLocal的get方法初次調(diào)用時,Entry[] table數(shù)據(jù)結(jié)構(gòu)是這樣的:

當我們再調(diào)用了set方法后,由于此時線程的threadLocals 屬性已經(jīng)不再為null,就會去調(diào)用ThreadlocalMap的set方法遍歷Entry數(shù)組,判斷key是否等于當前的ThreadLocal對象,如果是,將此前的Entry的value屬性覆蓋為新的value。源碼如下:
private void set(ThreadLocal<?> key, Object value) {// map.set(this, value);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) {//這里將value值覆蓋掉e.value = value;return;}if (k == null) {//由于弱引用的存在,key可能會被回收,所以要處理replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);//如果entry數(shù)組中沒有,那就再放進去一個int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();//將Entry數(shù)組擴大 類似Hashmap}
經(jīng)過Demo中methodA方法的tl.set(bean)之后,value的原來的null值被替換成bean。假如Demo中有一個methodC方法,new了一個新的ThreadLocal并set了值,那么數(shù)據(jù)結(jié)構(gòu)將如下圖所示:

當線程的ThreadLocalMap屬性有值的時候,通過ThreadLocalMap.get方法就可以獲取到對應(yīng)的Entry,Entry再調(diào)用get方法獲取Bean,從而實現(xiàn)變量的存儲。
05
ThreadLocal之真假變量拷貝
我們再回顧一下Demo2中,復(fù)寫了initialValue()方法,線程第一次調(diào)用get方法時,返回的都是new的一個新的Bean,所以兩個線程的bean是不一樣的,從而修改它不會對其他線程產(chǎn)生影響,因為不存在共享。所以在Demo2中,即便第一個線程將num設(shè)置為100,對第二個線程的Bean對象沒有任何影響,輸出0。
如果在Demo2中的return new Bean();換一種寫法,就會造成多個線程之間的數(shù)據(jù)共享。
public class Demo3 {private static Bean bean = new Bean();private static ThreadLocal<Bean> tl = new ThreadLocal<Bean>(){@Overrideprotected Bean initialValue() {return bean;}};public static void main(String[] args) {new Thread(()->{Bean bean = tl.get();bean.setNum(100);}).start();new Thread(()->{Bean bean = tl.get();System.out.println(bean.getNum());}).start();}}
此時輸出的結(jié)果是100。
因為兩個線程初次調(diào)用ThreadLocal的get方法時,都是從initialValue方法返回值,而該值是同一個,都指向成員變量bean,所以就造成了多個線程之間的變量共享了。
所以正確使用ThreadLocal真的很重要。

至此,ThreadLocal的存儲模型基本就摸清楚了。我們在看一下內(nèi)存泄露的那些事兒。
ThreadLocal的內(nèi)存泄露真的存在?(第二話)
若你喜歡本文,可以分享給身邊的朋友,或者關(guān)注我,謝謝?。?!
我。
