Java中的ThreadLocal與ThreadLocalMap詳解
ThreadLocal與ThreadLocalMap(jdk 1.8)
使用場景
- 每個(gè)線程需要一個(gè)獨(dú)享的對象(通常是工具類)
- 每個(gè)線程內(nèi)需要保存全局變量,可以在不同的地方直接獲取,避免參數(shù)傳遞的麻煩
作用
- 讓某個(gè)需要用到的對象在線程間隔離(每個(gè)線程都有自己獨(dú)享的對象)
- 任何方法中都可以輕松獲取其對象
好處
- 可以達(dá)到線程安全
- 不需要加鎖,提高效率
- 高效利用內(nèi)存,相比于每個(gè)任務(wù)都新建一個(gè)對象,用ThreadLocal可以節(jié)省內(nèi)存和開銷
- 免去傳遞參數(shù)的繁瑣,降低了程序耦合度
主要方法
1)initialValue()
該方法會(huì)返回當(dāng)前線程對應(yīng)的初始值,采用了懶加載機(jī)制,當(dāng)?shù)谝淮蝕et的時(shí)候才會(huì)觸發(fā),當(dāng)線程第一次使用get方法的時(shí)候才會(huì)觸發(fā)。除非線程先前調(diào)用了set方法,在這種情況下,不會(huì)再調(diào)用InitValue方法
2)set(T value)
未當(dāng)前線程設(shè)置一個(gè)新的值
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
T get()
public T get() { Thread t = Thread.currentThread();//獲取當(dāng)前線程 ThreadLocalMap map = getMap(t);//從當(dāng)前線程中獲取ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//獲取Entry if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result;//返回對象 } } return setInitialValue();//如果第一次調(diào)用get,ThreadLocalMap未空或者在ThreadLocalMap中還未存儲(chǔ)對象,則進(jìn)行初始化并返回存儲(chǔ)對象 }
remove()
//移除線程所存儲(chǔ)對象 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
原理
在Thread類中又這樣一個(gè)ThreadLocalMap 類型成員變量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是ThreadLocal的內(nèi)部類,其結(jié)構(gòu)如HashMap很相似,在其內(nèi)部還有個(gè)Entry,保存ThreadLocal和其保存的對象。其默認(rèn)容量也為16,負(fù)載因子未2/3,并且不存在next指針,哈希沖突后采用的延后策略。具體請看最后問題欄
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; //閾值 private void setThreshold(int len) { threshold = len * 2 / 3; //負(fù)載因子是2/3, } //......省略............ }
總的來說,在Thead中維護(hù)了一個(gè)Map,在Map中存儲(chǔ)了ThreadLocal和其綁定的對象
每次獲取對象都會(huì)從當(dāng)前線程中獲取map并將ThreadLocal傳入從而獲得對象
內(nèi)存泄露
ThreadLocal被用作TheadLocalMap的弱引用key,這種設(shè)計(jì)也是ThreadLocal被討論內(nèi)存泄露的熱點(diǎn)問題,因此有必要了解一下什么是弱引用。
弱引用
弱引用是用來描述非必須的對象的,但它的強(qiáng)度比軟引用更弱,被弱引用關(guān)聯(lián)的對象只能生存到下一次GC發(fā)生之前,也就是說下一次GC就會(huì)被回收。JDK1.2之后,提供了WeakReference來實(shí)現(xiàn)弱引用。
? 由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對象引用,則在下一次GC的時(shí)候,ThreadLocal實(shí)例就會(huì)被回收,那么此時(shí)ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會(huì)被外部訪問到,而且只要Thread實(shí)例一直存在,Thread實(shí)例就強(qiáng)引用著ThreadLocalMap,因此ThreadLocalMap就不會(huì)被回收,那么這里K為null的V就一直占用著內(nèi)存。
綜上,發(fā)生內(nèi)存泄露的條件是
- ThreadLocal實(shí)例沒有被外部強(qiáng)引用,比如我們假設(shè)在提交到線程池的task中實(shí)例化的ThreadLocal對象,當(dāng)task結(jié)束時(shí),ThreadLocal的強(qiáng)引用也就結(jié)束了
- ThreadLocal實(shí)例被回收,但是在ThreadLocalMap中的V沒有被任何清理機(jī)制有效清理
- 當(dāng)前Thread實(shí)例一直存在,則會(huì)一直強(qiáng)引用著ThreadLocalMap,也就是說ThreadLocalMap也不會(huì)被GC
示例
class Test{ byte data[]=new byte[1024*1024*10]; @Override protected void finalize() throws Throwable { System.out.println("destroy"); } } public class ThreadLocalDemo { public ThreadLocal<Test> t = new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); Test test = new Test(); threadLocalDemo.t.set(test); test = null; //threadLocalDemo.t.remove(); threadLocalDemo = null; System.out.println("start gc"); System.gc(); try { Thread.sleep(1000L); }catch (Exception e) { e.printStackTrace(); } System.out.println("end"); } }
輸出
/*
start gc
end
*/
//當(dāng)threadLocalDemo.t.remove();不被注釋
/*
輸出:
start gc
destroy
end
*/
當(dāng)不在持有ThreadLocalDemo對象,因?yàn)閠hread中ThreadLoaclMap中保存有ThreadLocal的引用 ,如果ThreadLocal不是弱引用的話,ThreadLocal是不可能被gc的。而如果ThreadLocal與ThreadLocalMap之間是弱引用,如果除Thread外沒有任何對象可以獲得ThreadLocal,則ThreadLocal是可以為回收的
? 當(dāng)然,其仍然仍然存在一定的內(nèi)存泄露,即value與TreadLcoalMap之間存在引用,當(dāng)ThreadLocal被gc時(shí)value是無法被gc的,但是在ThreadLocalMap內(nèi)部也存在一些機(jī)制,當(dāng)map擴(kuò)容或者發(fā)生hash沖突的時(shí)候會(huì)判斷key鍵是否為null(即判斷ThreadLocal對象是否被回收),如果是null,則會(huì)將value值同樣設(shè)為Null.從而幫助value gc
ThreadLocal為什么經(jīng)常設(shè)置為static
public class ThreadLocalDemo2 { public ThreadLocal<Test> t = new ThreadLocal<>(); //public static ThreadLocal<Test> t = new ThreadLocal<>(); public static void main(String[] args) { ThreadLocalDemo2 threadLocalDemo = new ThreadLocalDemo2(); Test test = new Test(); test.name = "xxxx"; threadLocalDemo.t.set(test); ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2(); Test test2 = new Test(); test2.name = "yyyyy"; threadLocalDemo2.t.set(test2); System.out.println(threadLocalDemo.t.get().name); System.out.println(threadLocalDemo2.t.get().name); } }
/*
輸出:
xxxx
yyyyy
static 修飾 ThreadLocal
輸出:
yyyyy
yyyyy
*/
static修飾ThreadLocal后,單個(gè)線程無論創(chuàng)建多個(gè)對象,其ThreadLocal示例僅僅只有一個(gè)。
如果變量ThreadLocal是非static的就會(huì)造成每次生成實(shí)例都要生成不同的ThreadLocal對象,雖然這樣程序不會(huì)有什么異常,但是會(huì)浪費(fèi)內(nèi)存資源,甚至?xí)斐蓛?nèi)存泄漏.。
建議
- 通過前面幾節(jié)的分析,我們基本弄清楚了ThreadLocal相關(guān)設(shè)計(jì)和內(nèi)存模型,對于是否會(huì)發(fā)生內(nèi)存泄露做了分析,下面總結(jié)下幾點(diǎn)建議:
- 當(dāng)需要存儲(chǔ)線程私有變量的時(shí)候,可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 當(dāng)需要實(shí)現(xiàn)線程安全的變量時(shí),可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 當(dāng)需要減少線程資源競爭的時(shí)候,可以考慮使用ThreadLocal來實(shí)現(xiàn)
- 注意Thread實(shí)例和ThreadLocal實(shí)例的生存周期,因?yàn)樗麄冎苯雨P(guān)聯(lián)著存儲(chǔ)數(shù)據(jù)的生命周期
- 如果頻繁的在線程中new ThreadLocal對象,在使用結(jié)束時(shí),最好調(diào)用ThreadLocal.remove來釋放其value的引用,避免在ThreadLocal被回收時(shí)value無法被訪問卻又占用著內(nèi)存
問題:
為什么ThreadLocalMap不用HashMap而是自己寫了個(gè)Map
- 自定義Map限定了鍵值未ThreadLocal類型
- 其Entry對象繼承了弱引用類,用來存儲(chǔ)鍵值,從而不影響對象被回收,而HashMap中Key是強(qiáng)引用
- ThreadLocalMap在寫數(shù)據(jù)和查數(shù)據(jù)的過程中有一個(gè)清理過期數(shù)據(jù)的功能,能夠?qū)l(fā)現(xiàn)的過期數(shù)據(jù)清理到,從某種意義上也是解決了內(nèi)存泄漏問題。當(dāng)然不是完全解決
ThreadLocalMap達(dá)到擴(kuò)容的閾值時(shí)會(huì)真正的擴(kuò)容嗎?
不會(huì),達(dá)到閾值之后,進(jìn)行一個(gè)散列表的掃描清楚過期的數(shù)據(jù),如果清理完之后,數(shù)據(jù)量仍然達(dá)到其閾值的75%,才進(jìn)行擴(kuò)容
擴(kuò)容源碼:
private void rehash() { expungeStaleEntries();//清理 if (size >= threshold - threshold / 4)//數(shù)據(jù)量仍然達(dá)到其閾值的75%,才進(jìn)行擴(kuò)容 resize(); } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen];//新建一個(gè)數(shù)組 int count = 0; for (int j = 0; j < oldLen; ++j) {//遍歷 Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; //將value設(shè)為null從而幫助GC } else { //重新進(jìn)行hash int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen);//采用的時(shí)自定義hash算法 newTab[h] = e; count++; } } } setThreshold(newLen);//計(jì)算新的閾值 size = count; table = newTab; }
ThreadLocalMap獲取Entry的流程
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1);//hash運(yùn)算計(jì)算出位置 Entry e = table[i]; if (e != null && e.get() == key)//未發(fā)生過Hash沖突 return e; else//發(fā)生過沖突 return getEntryAfterMiss(key, i, e);//進(jìn)行下一個(gè)位置的判斷 } 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)//如果為空,則說明此位置被GC了,為過期數(shù)據(jù) expungeStaleEntry(i);//為了防止內(nèi)存泄漏,觸發(fā)一個(gè)“探測式”過期數(shù)據(jù)回收邏輯 else i = nextIndex(i, len);//計(jì)算下一個(gè)位置 e = tab[i]; } return null; } //“探測式”過期數(shù)據(jù)回收邏輯 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null;//將value設(shè)為空,幫助GC tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot, len);//根據(jù)hash和尋址算法遍歷所有與當(dāng)前hash相同的槽點(diǎn) (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) {//幫助GC e.value = null; tab[i] = null; size--; } else { //如果key不為空,則重新進(jìn)行hash,將其移動(dòng)到一個(gè)更靠近其hash位置的槽點(diǎn)(提高下次get的效率) 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; }
ThreadLocalMap中set的具體流程
private void set(ThreadLocal<?> key, Object value) { //尋址 Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //遍歷可能的slot 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; } //如果k為空,則進(jìn)行取代算法 if (k == null) { //大體就是遍歷可能的槽點(diǎn),直到碰到key值相同的,則將其移動(dòng)到距離真實(shí)hash位置最近的點(diǎn),如果沒有,則再最有好的位置new一個(gè)新的Entry replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size;//判斷是否達(dá)到擴(kuò)容條件 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
到此這篇關(guān)于Java中的ThreadLocal與ThreadLocalMap詳解的文章就介紹到這了,更多相關(guān)ThreadLocal與ThreadLocalMap內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決BeanUtils.copyProperties不支持復(fù)制集合的問題
這篇文章主要介紹了解決BeanUtils.copyProperties不支持復(fù)制集合的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06詳解maven中profiles使用實(shí)現(xiàn)
本文主要介紹了maven中profiles使用實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Spring AOP定義AfterReturning增加實(shí)例分析
這篇文章主要介紹了Spring AOP定義AfterReturning增加,結(jié)合實(shí)例形式分析了Spring面相切面AOP定義AfterReturning增加相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2020-01-01spring boot @ResponseBody轉(zhuǎn)換JSON 時(shí) Date 類型處理方法【兩種方法】
這篇文章主要介紹了spring boot @ResponseBody轉(zhuǎn)換JSON 時(shí) Date 類型處理方法,主要給大家介紹Jackson和FastJson兩種方式,每一種方法給大家介紹的都非常詳細(xì),需要的朋友可以參考下2018-08-08mybatis-flex與springBoot整合的實(shí)現(xiàn)示例
Mybatis-flex提供了簡單易用的API,開發(fā)者只需要簡單的配置即可使用,本文主要介紹了mybatis-flex與springBoot整合,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01如何使用 IntelliJ IDEA 編寫 Spark 應(yīng)用程序(Sc
本教程展示了如何在IntelliJIDEA中使用Maven編寫和運(yùn)行一個(gè)簡單的Spark應(yīng)用程序(例如WordCount程序),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-11-11Java聊天室之實(shí)現(xiàn)獲取Socket功能
這篇文章主要為大家詳細(xì)介紹了Java簡易聊天室之實(shí)現(xiàn)獲取遠(yuǎn)程服務(wù)器和客戶機(jī)的IP地址和端口號(hào)功能,文中的示例代碼講解詳細(xì),需要的可以了解一下2022-10-10