日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JUC多线程:ThreadLocal 原理总结

發(fā)布時間:2024/9/30 编程问答 54 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JUC多线程:ThreadLocal 原理总结 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1、什么是 ThreadLocal:

????????ThreadLocal 提供了線程內(nèi)部的局部變量,當(dāng)在多線程環(huán)境中使用 ThreadLocal 維護變量時,會為每個線程生成該變量的副本,每個線程只操作自己線程中的變量副本,不同線程間的數(shù)據(jù)相互隔離、互不影響,從而保證了線程的安全。

????????ThreadLocal 適用于無狀態(tài),副本變量獨立后不影響業(yè)務(wù)邏輯的高并發(fā)場景,如果業(yè)務(wù)邏輯強依賴于變量副本,則不適合用 ThreadLocal 解決,需要另尋解決方案。

2、ThreadLocal 的數(shù)據(jù)結(jié)構(gòu):

????????在 JDK8 中,每個線程 Thread 內(nèi)部都維護了一個 ThreadLocalMap 的數(shù)據(jù)結(jié)構(gòu),ThreadLocalMap 中有一個由內(nèi)部類 Entry 組成的 table 數(shù)組,Entry 的 key 就是線程的本地化對象 ThreadLocal,而 value 則存放了當(dāng)前線程所操作的變量副本。每個 ThreadLocal 只能保存一個副本 value,并且各個線程的數(shù)據(jù)互不干擾,如果想要一個線程保存多個副本變量,就需要創(chuàng)建多個ThreadLocal。

????????一個 ThreadLocal 的值,會根據(jù)線程的不同,分散在 N 個線程中,所以獲取 ThreadLocal 的 value,有兩個步驟:

  • 第一步,根據(jù)線程獲取 ThreadLocalMap

  • 第二步,根據(jù)自身從 ThreadLocalMap 中獲取值,所以它的 this 就是 Map 的 Key

當(dāng)執(zhí)行 set() 方法時,其值是保存在當(dāng)前線程的 ThreadLocal 變量副本中,當(dāng)執(zhí)行g(shù)et() 方法中,是從當(dāng)前線程的 ThreadLocal 的變量副本獲取。所以對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當(dāng)前線程的副本值,形成了線程的隔離,互不干擾。

3、ThreadLocal 的核心方法:

ThreadLocal 對外暴露的方法有4個:

  • initialValue()方法:返回為當(dāng)前線程初始副本變量值。

  • get()方法:獲取當(dāng)前線程的副本變量值。

  • set()方法:保存當(dāng)前線程的副本變量值。

  • remove()方法:移除當(dāng)前前程的副本變量值

3.1、set()方法:

// 設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal值 public void set(T value) {Thread t = Thread.currentThread(); // 獲取當(dāng)前線程對象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)前對象沒有ThreadLocalMap 對象。// 創(chuàng)建一個對象 賦值給當(dāng)前線程 }// 獲取當(dāng)前線程對象維護的ThreadLocalMap ThreadLocalMap getMap(Thread t) {return t.threadLocals; } // 給傳入的線程 配置一個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)前線程對象ThreadLocalMap map = getMap(t);//線程對象對應(yīng)的mapif (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);// 以當(dāng)前threadlocal為key,嘗試獲得實體if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 如果當(dāng)前線程對應(yīng)map不存在// 如果map存在但是當(dāng)前threadlocal沒有關(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)前線程獲取對應(yīng)的 map

  • (2)如果獲得的 map 不為空,以當(dāng)前 threadlocal 為 key 嘗試獲得 entry

  • (3)如果 entry 不為空,返回值。

  • (4)如果 2 跟 3 出現(xiàn)無法獲得,則通過 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)如果沒有調(diào)用 set() 直接 get(),則會調(diào)用此方法,該方法只會被調(diào)用一次,

  • (2)默認返回一個缺省值null,如果不想返回null,可以O(shè)verride 進行覆蓋。

4、ThreadLocal 的哈希沖突的解決方法:線性探測

????????和 HashMap 不同,ThreadLocalMap 結(jié)構(gòu)中沒有 next 引用,也就是說 ThreadLocalMap 中解決哈希沖突的方式并非鏈表的方式,而是采用線性探測的方式,當(dāng)發(fā)生哈希沖突時就將步長加1或減1,尋找下一個相鄰的位置。如下圖所示:

流程說明:

  • 根據(jù) ThreadLocal 對象的 hash 值,定位到 table 中的位置 i;

  • 如果當(dāng)前位置是 null,就初始化一個 Entry 對象放在位置 i 上;

  • 如果位置 i 已經(jīng)有 Entry 對象了,如果這個 Entry 對象的 key 與即將設(shè)置的 key 相同,那么重新設(shè)置 Entry 的 value;

  • 如果位置 i 的 Entry 對象和 即將設(shè)置的 key 不同,那么尋找下一個空位置;

具體源碼如下:

private void set(ThreadLocal<?> key, Object 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) {e.value = value;return;}if (k == null) {// 如果key不是空value是空,垃圾清除內(nèi)存泄漏防止。replaceStaleEntry(key, value, i);return;}}// 如果ThreadLocal對應(yīng)的key不存在并且沒找到舊元素,則在空元素位置創(chuàng)建個新Entrytab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash(); }// 環(huán)形數(shù)組 下一個索引 private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0); }

5、ThreadLocal 的內(nèi)存泄露:

????????在使用 ThreadLocal 時,當(dāng)使用完變量后,必須手動調(diào)用 remove() 方法刪除 entry 對象,否則會造成 value 的內(nèi)存泄露,嚴格來說,ThreadLocal 是沒有內(nèi)存泄漏問題,有的話,那也是忘記執(zhí)行 remove() 引起的,這是使用不規(guī)范導(dǎo)致的。

????????不過有些人認為 ThreadLocal 的內(nèi)存泄漏是跟 Entry 中使用弱引用 key 有關(guān),這個結(jié)論是不對的。ThreadLocal 造成內(nèi)存泄露的根本原因并不是 key 使用弱引用,因為即使 key 使用強引用,也會造成 Entry 對象的內(nèi)存泄露,內(nèi)存泄露的根本原因在于 ThreadLocalMap 的生命周期與當(dāng)前線程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完沒有進行手動刪除導(dǎo)致的。下面我們就針對兩種情況進行分析:

5.1、如果 key 使用強引用:

如果在業(yè)務(wù)代碼中使用完 ThreadLocal,則此時 Stack 中的 ThreadLocalRef 就會被回收了。

但是此時 ThreadLocalMap 中的 Entry 中的 Key 是強引用 ThreadLocal 的,會造成 ThreadLocal 實例無法回收。

如果我們沒有刪除 Entry 并且 CurrentThread 依然運行的情況下,強引用鏈如下圖紅色,會導(dǎo)致Entry內(nèi)存泄漏。

?所以結(jié)論就是:強引用無法避免內(nèi)存泄漏。

5.2、如果 key 使用弱引用

如果在業(yè)務(wù)代碼中使用完 ThreadLocal,則此時 Stack 中的 ThreadLocalRef 就會被回收了。

但是此時 ThreadLocalMap 中的 Entry 中的 Key 是弱引用 ThreadLocal 的,會造成 ThreadLocal 實例被回收,此時 Entry 中的 key = null。

但是當(dāng)我們沒有手動刪除 Entry 以及 CurrentThread 依然運行的時候,還是存在強引用鏈,但因為 ThreadLocalRef 已經(jīng)被回收了,那么此時的 value 就無法訪問到了,導(dǎo)致value內(nèi)存泄漏

?所以結(jié)論就是:弱引用也無法避免內(nèi)存泄漏

5.3、內(nèi)存泄露的原因:

從上面的分析知道內(nèi)存泄漏跟強弱引用無關(guān),內(nèi)存泄漏的前提有兩個:

ThreadLocalRef 用完后 Entry 沒有手動刪除。

ThreadLocalRef 用完后 CurrentThread 依然在運行。

  • 第一點表明當(dāng)我們在使用完 ThreadLocal 后,調(diào)用其對應(yīng)的 remove() 方法刪除對應(yīng)的 Entry 就可以避免內(nèi)存泄漏

  • 第二點是由于 ThreadLocalMap 是 CurrentThread 的一個屬性,被當(dāng)前線程引用,生命周期跟 CurrentThread 一樣,如果當(dāng)前線程結(jié)束 ThreadLocalMap 被回收,自然里面的 Entry 也被回收了,但問題是此時的線程不一定會被回收,比如線程是從線程池中獲取的,用完后就放回池子里了

所以,我們可以得出在這小節(jié)開頭的結(jié)論:ThreadLocal 內(nèi)存泄漏根源是 ThreadLocalMap 的生命周期跟 Thread 一樣,如果用完 ThreadLocal 沒有手動刪除就會內(nèi)存泄漏。

5.4、為什么使用弱引用:

????????前面講到 ThreadLocal 的內(nèi)存泄露與強弱引用無關(guān),那么為什么還要用弱引用呢?

(1)Entry 中的 key(Threadlocal)是弱引用,目的是將 ThreadLocal 對象的生命周期跟線程周期解綁,用 WeakReference 弱引用關(guān)聯(lián)的對象,只能生存到下一次垃圾回收之前,GC發(fā)生時,不管內(nèi)存夠不夠,都會被回收。

(2)當(dāng)我們使用完 ThreadLocal,而 Thread 仍然運行時,即使忘記調(diào)用 remove() 方法, 弱引用也會比強引用多一層保障:當(dāng) GC 發(fā)生時,弱引用的 ThreadLocal 被收回,那么 key 就為 null 了。而 ThreadLocalMap 中的 set()、get() 方法,會針對 key == null (也就是 ThreadLocal 為 null) 的情況進行處理,如果 key == null,則系統(tǒng)認為 value 也應(yīng)該是無效了應(yīng)該設(shè)置為 null,也就是說對應(yīng)的 value 會在下次調(diào)用 ThreadLocal 的 set()、get() 方法時,執(zhí)行底層 ThreadLocalMap 中的 expungeStaleEntry() 方法進行清除無用的 value,從而避免內(nèi)存泄露。

6、ThreadLocal 的應(yīng)用場景:

(1)Hibernate 的 session 獲取:每個線程訪問數(shù)據(jù)庫都應(yīng)當(dāng)是一個獨立的 session 會話,如果多個線程共享同一個 session 會話,有可能其他線程關(guān)閉連接了,當(dāng)前線程再執(zhí)行提交時就會出現(xiàn)會話已關(guān)閉的異常,導(dǎo)致系統(tǒng)異常。使用 ThreadLocal 的方式能避免線程爭搶session,提高并發(fā)安全性。

(2)Spring 的事務(wù)管理:事務(wù)需要保證一組操作同時成功或失敗,意味著一個事務(wù)的所有操作需要在同一個數(shù)據(jù)庫連接上,Spring 采用 Threadlocal 的方式,來保證單個線程中的數(shù)據(jù)庫操作使用的是同一個數(shù)據(jù)庫連接,同時采用這種方式可以使業(yè)務(wù)層使用事務(wù)時不需要感知并管理 connection 對象,通過傳播級別,巧妙地管理多個事務(wù)配置之間的切換,掛起和恢復(fù)。

7、如果想共享線程的?ThreadLocal 數(shù)據(jù)怎么辦

????????使用 InheritableThreadLocal 可以實現(xiàn)多個線程訪問 ThreadLocal 的值,我們在主線程中創(chuàng)建一個 InheritableThreadLocal 的實例,然后在子線程中得到這個InheritableThreadLocal實例設(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 能實現(xiàn)線程的數(shù)據(jù)隔離,不在于它自己本身,而在于 Thread 的 ThreadLocalMap,所以,ThreadLocal 可以只實例化一次,只分配一塊存儲空間就可以了,沒有必要作為成員變量多次被初始化。

參考文章:https://juejin.cn/post/6890446289411145741

與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖

總結(jié)

以上是生活随笔為你收集整理的JUC多线程:ThreadLocal 原理总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。