JUC多线程:ThreadLocal 原理总结
1、什么是 ThreadLocal:
????????ThreadLocal 提供了線程內(nèi)部的局部變量,當(dāng)在多線程環(huán)境中使用 ThreadLocal 維護(hù)變量時(shí),會(huì)為每個(gè)線程生成該變量的副本,每個(gè)線程只操作自己線程中的變量副本,不同線程間的數(shù)據(jù)相互隔離、互不影響,從而保證了線程的安全。
????????ThreadLocal 適用于無(wú)狀態(tài),副本變量獨(dú)立后不影響業(yè)務(wù)邏輯的高并發(fā)場(chǎng)景,如果業(yè)務(wù)邏輯強(qiáng)依賴于變量副本,則不適合用 ThreadLocal 解決,需要另尋解決方案。
2、ThreadLocal 的數(shù)據(jù)結(jié)構(gòu):
????????在 JDK8 中,每個(gè)線程 Thread 內(nèi)部都維護(hù)了一個(gè) ThreadLocalMap 的數(shù)據(jù)結(jié)構(gòu),ThreadLocalMap 中有一個(gè)由內(nèi)部類 Entry 組成的 table 數(shù)組,Entry 的 key 就是線程的本地化對(duì)象 ThreadLocal,而 value 則存放了當(dāng)前線程所操作的變量副本。每個(gè) ThreadLocal 只能保存一個(gè)副本 value,并且各個(gè)線程的數(shù)據(jù)互不干擾,如果想要一個(gè)線程保存多個(gè)副本變量,就需要?jiǎng)?chuàng)建多個(gè)ThreadLocal。
????????一個(gè) ThreadLocal 的值,會(huì)根據(jù)線程的不同,分散在 N 個(gè)線程中,所以獲取 ThreadLocal 的 value,有兩個(gè)步驟:
-
第一步,根據(jù)線程獲取 ThreadLocalMap
-
第二步,根據(jù)自身從 ThreadLocalMap 中獲取值,所以它的 this 就是 Map 的 Key
當(dāng)執(zhí)行 set() 方法時(shí),其值是保存在當(dāng)前線程的 ThreadLocal 變量副本中,當(dāng)執(zhí)行g(shù)et() 方法中,是從當(dāng)前線程的 ThreadLocal 的變量副本獲取。所以對(duì)于不同的線程,每次獲取副本值時(shí),別的線程并不能獲取到當(dāng)前線程的副本值,形成了線程的隔離,互不干擾。
3、ThreadLocal 的核心方法:
ThreadLocal 對(duì)外暴露的方法有4個(gè):
-
initialValue()方法:返回為當(dāng)前線程初始副本變量值。
-
get()方法:獲取當(dāng)前線程的副本變量值。
-
set()方法:保存當(dāng)前線程的副本變量值。
-
remove()方法:移除當(dāng)前前程的副本變量值
3.1、set()方法:
// 設(shè)置當(dāng)前線程對(duì)應(yīng)的ThreadLocal值 public void set(T value) {Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對(duì)象ThreadLocalMap map = getMap(t);if (map != null) // 判斷map是否存在map.set(this, value); // 調(diào)用map.set 將當(dāng)前value賦值給當(dāng)前threadLocal。elsecreateMap(t, value);// 如果當(dāng)前對(duì)象沒(méi)有ThreadLocalMap 對(duì)象。// 創(chuàng)建一個(gè)對(duì)象 賦值給當(dāng)前線程 }// 獲取當(dāng)前線程對(duì)象維護(hù)的ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; } // 給傳入的線程 配置一個(gè)threadlocals void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue); }執(zhí)行流程:
-
獲得當(dāng)前線程,根據(jù)當(dāng)前線程獲得 map。
-
如果 map 不為空,則將參數(shù)設(shè)置到 map 中,當(dāng)前的 Threadlocal 作為 key。
-
如果 map 為空,則給該線程創(chuàng)建 map,設(shè)置初始值。
3.2、get()方法:
public T get() {Thread t = Thread.currentThread();//獲得當(dāng)前線程對(duì)象ThreadLocalMap map = getMap(t);//線程對(duì)象對(duì)應(yīng)的mapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);// 以當(dāng)前threadlocal為key,嘗試獲得實(shí)體if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 如果當(dāng)前線程對(duì)應(yīng)map不存在// 如果map存在但是當(dāng)前threadlocal沒(méi)有關(guān)連的entry。return setInitialValue(); }// 初始化 private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value; }執(zhí)行流程:
-
(1)先嘗試獲得當(dāng)前線程,再根據(jù)當(dāng)前線程獲取對(duì)應(yīng)的 map
-
(2)如果獲得的 map 不為空,以當(dāng)前 threadlocal 為 key 嘗試獲得 entry
-
(3)如果 entry 不為空,返回值。
-
(4)如果 2 跟 3 出現(xiàn)無(wú)法獲得,則通過(guò) initialValue 函數(shù)獲得初始值,然后給當(dāng)前線程創(chuàng)建新 map
3.3、remove()方法:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this); }執(zhí)行流程:
首先嘗試獲取當(dāng)前線程,然后根據(jù)當(dāng)前線程獲得map,從map中嘗試刪除enrty。
3.4、initialValue() 方法:
protected T initialValue() {return null; }執(zhí)行流程:
-
(1)如果沒(méi)有調(diào)用 set() 直接 get(),則會(huì)調(diào)用此方法,該方法只會(huì)被調(diào)用一次,
-
(2)默認(rèn)返回一個(gè)缺省值null,如果不想返回null,可以O(shè)verride 進(jìn)行覆蓋。
4、ThreadLocal 的哈希沖突的解決方法:線性探測(cè)
????????和 HashMap 不同,ThreadLocalMap 結(jié)構(gòu)中沒(méi)有 next 引用,也就是說(shuō) ThreadLocalMap 中解決哈希沖突的方式并非鏈表的方式,而是采用線性探測(cè)的方式,當(dāng)發(fā)生哈希沖突時(shí)就將步長(zhǎng)加1或減1,尋找下一個(gè)相鄰的位置。如下圖所示:
流程說(shuō)明:
-
根據(jù) ThreadLocal 對(duì)象的 hash 值,定位到 table 中的位置 i;
-
如果當(dāng)前位置是 null,就初始化一個(gè) Entry 對(duì)象放在位置 i 上;
-
如果位置 i 已經(jīng)有 Entry 對(duì)象了,如果這個(gè) Entry 對(duì)象的 key 與即將設(shè)置的 key 相同,那么重新設(shè)置 Entry 的 value;
-
如果位置 i 的 Entry 對(duì)象和 即將設(shè)置的 key 不同,那么尋找下一個(gè)空位置;
具體源碼如下:
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);//計(jì)算索引位置for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { // 開(kāi)放定址法解決哈希沖突ThreadLocal<?> k = e.get();//直接覆蓋if (k == key) {e.value = value;return;}if (k == null) {// 如果key不是空value是空,垃圾清除內(nèi)存泄漏防止。replaceStaleEntry(key, value, i);return;}}// 如果ThreadLocal對(duì)應(yīng)的key不存在并且沒(méi)找到舊元素,則在空元素位置創(chuàng)建個(gè)新Entrytab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash(); }// 環(huán)形數(shù)組 下一個(gè)索引 private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0); }5、ThreadLocal 的內(nèi)存泄露:
????????在使用 ThreadLocal 時(shí),當(dāng)使用完變量后,必須手動(dòng)調(diào)用 remove() 方法刪除 entry 對(duì)象,否則會(huì)造成 value 的內(nèi)存泄露,嚴(yán)格來(lái)說(shuō),ThreadLocal 是沒(méi)有內(nèi)存泄漏問(wèn)題,有的話,那也是忘記執(zhí)行 remove() 引起的,這是使用不規(guī)范導(dǎo)致的。
????????不過(guò)有些人認(rèn)為 ThreadLocal 的內(nèi)存泄漏是跟 Entry 中使用弱引用 key 有關(guān),這個(gè)結(jié)論是不對(duì)的。ThreadLocal 造成內(nèi)存泄露的根本原因并不是 key 使用弱引用,因?yàn)榧词?key 使用強(qiáng)引用,也會(huì)造成 Entry 對(duì)象的內(nèi)存泄露,內(nèi)存泄露的根本原因在于 ThreadLocalMap 的生命周期與當(dāng)前線程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完沒(méi)有進(jìn)行手動(dòng)刪除導(dǎo)致的。下面我們就針對(duì)兩種情況進(jìn)行分析:
5.1、如果 key 使用強(qiáng)引用:
如果在業(yè)務(wù)代碼中使用完 ThreadLocal,則此時(shí) Stack 中的 ThreadLocalRef 就會(huì)被回收了。
但是此時(shí) ThreadLocalMap 中的 Entry 中的 Key 是強(qiáng)引用 ThreadLocal 的,會(huì)造成 ThreadLocal 實(shí)例無(wú)法回收。
如果我們沒(méi)有刪除 Entry 并且 CurrentThread 依然運(yùn)行的情況下,強(qiáng)引用鏈如下圖紅色,會(huì)導(dǎo)致Entry內(nèi)存泄漏。
?所以結(jié)論就是:強(qiáng)引用無(wú)法避免內(nèi)存泄漏。
5.2、如果 key 使用弱引用
如果在業(yè)務(wù)代碼中使用完 ThreadLocal,則此時(shí) Stack 中的 ThreadLocalRef 就會(huì)被回收了。
但是此時(shí) ThreadLocalMap 中的 Entry 中的 Key 是弱引用 ThreadLocal 的,會(huì)造成 ThreadLocal 實(shí)例被回收,此時(shí) Entry 中的 key = null。
但是當(dāng)我們沒(méi)有手動(dòng)刪除 Entry 以及 CurrentThread 依然運(yùn)行的時(shí)候,還是存在強(qiáng)引用鏈,但因?yàn)?ThreadLocalRef 已經(jīng)被回收了,那么此時(shí)的 value 就無(wú)法訪問(wèn)到了,導(dǎo)致value內(nèi)存泄漏
?所以結(jié)論就是:弱引用也無(wú)法避免內(nèi)存泄漏
5.3、內(nèi)存泄露的原因:
從上面的分析知道內(nèi)存泄漏跟強(qiáng)弱引用無(wú)關(guān),內(nèi)存泄漏的前提有兩個(gè):
ThreadLocalRef 用完后 Entry 沒(méi)有手動(dòng)刪除。
ThreadLocalRef 用完后 CurrentThread 依然在運(yùn)行。
-
第一點(diǎn)表明當(dāng)我們?cè)谑褂猛?ThreadLocal 后,調(diào)用其對(duì)應(yīng)的 remove() 方法刪除對(duì)應(yīng)的 Entry 就可以避免內(nèi)存泄漏
-
第二點(diǎn)是由于 ThreadLocalMap 是 CurrentThread 的一個(gè)屬性,被當(dāng)前線程引用,生命周期跟 CurrentThread 一樣,如果當(dāng)前線程結(jié)束 ThreadLocalMap 被回收,自然里面的 Entry 也被回收了,但問(wèn)題是此時(shí)的線程不一定會(huì)被回收,比如線程是從線程池中獲取的,用完后就放回池子里了
所以,我們可以得出在這小節(jié)開(kāi)頭的結(jié)論:ThreadLocal 內(nèi)存泄漏根源是 ThreadLocalMap 的生命周期跟 Thread 一樣,如果用完 ThreadLocal 沒(méi)有手動(dòng)刪除就會(huì)內(nèi)存泄漏。
5.4、為什么使用弱引用:
????????前面講到 ThreadLocal 的內(nèi)存泄露與強(qiáng)弱引用無(wú)關(guān),那么為什么還要用弱引用呢?
(1)Entry 中的 key(Threadlocal)是弱引用,目的是將 ThreadLocal 對(duì)象的生命周期跟線程周期解綁,用 WeakReference 弱引用關(guān)聯(lián)的對(duì)象,只能生存到下一次垃圾回收之前,GC發(fā)生時(shí),不管內(nèi)存夠不夠,都會(huì)被回收。
(2)當(dāng)我們使用完 ThreadLocal,而 Thread 仍然運(yùn)行時(shí),即使忘記調(diào)用 remove() 方法, 弱引用也會(huì)比強(qiáng)引用多一層保障:當(dāng) GC 發(fā)生時(shí),弱引用的 ThreadLocal 被收回,那么 key 就為 null 了。而 ThreadLocalMap 中的 set()、get() 方法,會(huì)針對(duì) key == null (也就是 ThreadLocal 為 null) 的情況進(jìn)行處理,如果 key == null,則系統(tǒng)認(rèn)為 value 也應(yīng)該是無(wú)效了應(yīng)該設(shè)置為 null,也就是說(shuō)對(duì)應(yīng)的 value 會(huì)在下次調(diào)用 ThreadLocal 的 set()、get() 方法時(shí),執(zhí)行底層 ThreadLocalMap 中的 expungeStaleEntry() 方法進(jìn)行清除無(wú)用的 value,從而避免內(nèi)存泄露。
6、ThreadLocal 的應(yīng)用場(chǎng)景:
(1)Hibernate 的 session 獲取:每個(gè)線程訪問(wèn)數(shù)據(jù)庫(kù)都應(yīng)當(dāng)是一個(gè)獨(dú)立的 session 會(huì)話,如果多個(gè)線程共享同一個(gè) session 會(huì)話,有可能其他線程關(guān)閉連接了,當(dāng)前線程再執(zhí)行提交時(shí)就會(huì)出現(xiàn)會(huì)話已關(guān)閉的異常,導(dǎo)致系統(tǒng)異常。使用 ThreadLocal 的方式能避免線程爭(zhēng)搶session,提高并發(fā)安全性。
(2)Spring 的事務(wù)管理:事務(wù)需要保證一組操作同時(shí)成功或失敗,意味著一個(gè)事務(wù)的所有操作需要在同一個(gè)數(shù)據(jù)庫(kù)連接上,Spring 采用 Threadlocal 的方式,來(lái)保證單個(gè)線程中的數(shù)據(jù)庫(kù)操作使用的是同一個(gè)數(shù)據(jù)庫(kù)連接,同時(shí)采用這種方式可以使業(yè)務(wù)層使用事務(wù)時(shí)不需要感知并管理 connection 對(duì)象,通過(guò)傳播級(jí)別,巧妙地管理多個(gè)事務(wù)配置之間的切換,掛起和恢復(fù)。
7、如果想共享線程的?ThreadLocal 數(shù)據(jù)怎么辦 ?
????????使用 InheritableThreadLocal 可以實(shí)現(xiàn)多個(gè)線程訪問(wèn) ThreadLocal 的值,我們?cè)谥骶€程中創(chuàng)建一個(gè) InheritableThreadLocal 的實(shí)例,然后在子線程中得到這個(gè)InheritableThreadLocal實(shí)例設(shè)置的值。
private void test() { final ThreadLocal threadLocal = new InheritableThreadLocal(); threadLocal.set("主線程的ThreadLocal的值"); Thread t = new Thread() { @Override public void run() { super.run(); Log.i( "我是子線程,我要獲取其他線程的ThreadLocal的值 ==> " + threadLocal.get()); } }; t.start(); }8、為什么一般用 ThreadLocal 都要用 static?
????????ThreadLocal 能實(shí)現(xiàn)線程的數(shù)據(jù)隔離,不在于它自己本身,而在于 Thread 的 ThreadLocalMap,所以,ThreadLocal 可以只實(shí)例化一次,只分配一塊存儲(chǔ)空間就可以了,沒(méi)有必要作為成員變量多次被初始化。
參考文章:https://juejin.cn/post/6890446289411145741
與50位技術(shù)專家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的JUC多线程:ThreadLocal 原理总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JUC多线程:AQS抽象队列同步器原理
- 下一篇: JUC多线程:阻塞队列ArrayBloc