ThreadLocal 和 InheritableThreadLocal
在學習ThreadLocal之前,建議先了解Java中的4種引用
一、先看一下Thread,ThreadMap,ThreadLocal的關系
Thread中持有一個ThreadLocalMap ,這里你可以簡單理解為就是持有一個數組,這個數組是Entry 類型的。 Entry 的key 是ThreadLocal 類型的(準確說是一個指向ThreadLocal的引用),value 是Object 類型。也就是一個ThreadLocalMap 可以持有多個ThreadLocal。他們是一對多的關系
同時通過源碼我們可以看到,ThreadLocalMap是ThreadLocal里的一個靜態內部類,Entry又是ThreadLocalMap的一個靜態內部類。這里可以思考一下為何不直接使用HashMap?(HashMap節點之間是通過強引用關聯)
這里明明確一點:ThreadLocal不是隸屬于Thread的,他是一個獨立的(一個用于操作Thread中threadLocals屬性的工具類),使用他的時候,必須new出來,而不是通過Thread來獲得,明確這點,我感覺理解起來會好很多。
- ThreadLocal的作用:為共享變量在每一個線程中創建一個副本,以保證不同的線程中拿到該變量的值都是不一樣的
二、ThreadLocal的基本方法
在實際使用之前我們先來了解一下ThreadLocal的幾個常用方法void
- set(Object value)設置當前線程的線程局部變量的值。
- 該方法返回當前線程所對應的線程局部變量。
- public void remove()
將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {//根據當前ThreadLocal變量刪除Thread.ThreadLocals中對應的值m.remove(this);}}- protected Object initialValue()
返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,并且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
三、引用ThreadLocal時示意圖
代碼:
/*** @author Cristianoxm*/ public class TestThreadLocal {static ThreadLocal<String> threadLocal1=new ThreadLocal<String>();static ThreadLocal<Integer> threadLocal2=new ThreadLocal<Integer>();public static void main(String[] args) {Thread [] runs=new Thread[3];for (int i=0;i<runs.length;i++){new MyThread(i).start();}}public static class MyThread extends Thread{int id;public MyThread(int id){this.id=id;}@Overridesynchronized public void run(){threadLocal1.set("線程-"+id);if(id == 1){threadLocal2.set(1);}System.out.println(Thread.currentThread().getName()+"編號:"+threadLocal1.get());System.out.println(threadLocal2.get());}} }內存圖大概如下:只有Thread-1中有指向Threadlocal2的Entry,thread-2中是沒有的
四、底層原理解析
掌握了基本用法之后,我們就來聊聊ThreadLocal 的底層原理吧。在java中每一個線程都會有一個 ThrealLocalMap ,里面存放了一個一個的鍵值對(我們一般稱之為Entry),鍵值對的key 就是 Threadlocal本身,而value 就是他的值,正如下圖所示。
我們仔細的看一下這張圖。左邊是stack(棧),棧里面有用戶線程和threadlocal的引用。而在堆里,threadlocal 用一個弱引用指向了我們ThrealLocalMap 的key,而用戶線程 則是一個強引用指向了 這個enrty的 value.現在我們就能理解為什么threadlocal 的副本是怎么回事了,線程拷貝一份值,用Threadlocal自身做key ,保存在一個map中,從而實現了每個線程之間變量的互相隔離。
五、內存泄露分析
提到Threadlocal 必然不得不說他的內存泄露問題,首先我們注意下,threadlocalmap 是一個弱引用,也就是說只要gc,我們上面這條 ThreadLocal->key 的虛線就回被打斷,但是如果我們的當前線程沒有結束,那么指向value的強引用自然不會斷開。于是當gc發生的時候,我們的map中就出現了一個,key為null的 entry,但是這個entry 由于value的強引用并不會被回收,那么map中就出現了一個永遠訪問不到的entry!內存泄漏由此產生。當然,threadLocal 對此還是有對策的,當我們調用 get set 和 remove 方法的時候,threadlocal都會去檢查是否有這樣的entry并把他們清除掉。但是注意,除了 remove方法,其他兩個方法都是不可靠的,他們并非及時同時不一定會去執行清除方法。
- 對象引用圖:
所以為了防止內存泄露,我們必須在使用完Threadlocal后及時的去調用remove方法!所以:ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。想要避免內存泄露就要手動remove()掉!
- 那么:為什么使用弱引用
說道這里可能很多人會問了,為什么threadlocal要采用弱引用,采用強引用不是就沒有這個訪問不到entry的毛病了嗎。那我們這里就假設一下,如果這里的key是強引用的話,如果我們給這個Threadlocal 賦值為null。那么情況就如下圖所示
請大家注意這里 指向我們value的線已經斷了,照理來說這個entry應該被回收,但是由于我們的key是強引用,所以這個entry就變得和 Threadlocal同生共死了,從而造成內存泄露!也即是:entry中的value置空,threadlocal置空,那么這個空的entry會因為key的強引用,變得和 Threadlocal同生共死,就內存泄露了
- 總結使用ThreadLocal時會發生內存泄漏的前提條件:
- ①ThreadLocal引用被設置為null,且后面沒有set,get,remove操作。
- ②線程一直運行,不停止。(線程池)
- ③觸發了垃圾回收。(Minor GC或Full GC)
我們看到ThreadLocal出現內存泄漏條件還是很苛刻的,所以我們只要破壞其中一個條件就可以避免內存泄漏,單但為了更好的避免這種情況的發生我們使用ThreadLocal時遵守以下兩個小原則
- ①ThreadLocal申明為private static final。
Private與final 盡可能不讓他人修改變更引用,
Static 表示為類屬性,只有在程序結束才會被回收。 - ②ThreadLocal使用后務必調用remove方法。
最簡單有效的方法是使用后將其移除。
六、流程圖
七、ThreadLocal的應用
- 管理Connection
最典型的是管理數據庫的Connection:當時在學JDBC的時候,為了方便操作寫了一個簡單數據庫連接池,需要數據庫連接池的理由也很簡單,頻繁創建和關閉Connection是一件非常耗費資源的操作,因此需要創建數據庫連接池,那么,數據庫連接池的連接怎么管理呢??我們交由ThreadLocal來進行管理。為什么交給它來管理呢??ThreadLocal能夠實現當前線程的操作都是用同一個Connection,保證了事務!
同樣的,Hibernate對Connection的管理也是采用了相同的手法(使用ThreadLocal,當然了Hibernate的實現是更強大的)
八、InheritableThreadLocal
InheritableThreadLocal類其實是重寫了ThreadLocal的3個函數:
/*** 該函數在父線程創建子線程,向子線程復制InheritableThreadLocal變量時使用*/protected T childValue(T parentValue) {return parentValue;}/*** 由于重寫了getMap,操作InheritableThreadLocal時,* 將只影響Thread類中的inheritableThreadLocals變量,* 與threadLocals變量不再有關系*/ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}/*** 類似于getMap,操作InheritableThreadLocal時,* 將只影響Thread類中的inheritableThreadLocals變量,* 與threadLocals變量不再有關系*/void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}- 作用:我們知道:當前線程的ThreadLocalMap,主要存儲該線程自身的ThreadLocal。而InheritableThreadLocal,是自父線程集成而來的ThreadLocalMap,主要用于父子線程間ThreadLocal變量的傳遞。 即主要存儲可自動向子線程中傳遞的ThreadLocal.ThreadLocalMap
九、inheritThreadLocals的使用
- thread的創建
可以看到,采用默認方式產生子線程時,inheritThreadLocals=true;若此時父線程inheritableThreadLocals不為空,則將父線程inheritableThreadLocals傳遞至子線程。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);} /*** 構建一個包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap,該函數只被 createInheritedMap() 調用.*/private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);// ThreadLocalMap 使用 Entry[] table 存儲ThreadLocaltable = new Entry[len];// 逐一復制 parentMap 的記錄for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// 可能會有同學好奇此處為何使用childValue,而不是直接賦值,// 畢竟childValue內部也是直接將e.value返回;// 個人理解,主要為了減輕閱讀代碼的難度Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}從ThreadLocalMap可知,子線程將parentMap中的所有記錄逐一復制至自身線程。
- 代碼例子
參考文章
參考文章
總結
以上是生活随笔為你收集整理的ThreadLocal 和 InheritableThreadLocal的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java对象的四种引用方式
- 下一篇: 2021中国家居行业洞察白皮书