Java多線程之ThreadLocal原理總結(jié)
1、什么是 ThreadLocal:
ThreadLocal,即線程本地變量,如果你創(chuàng)建了一個(gè)變量,那么訪問這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的本地拷貝,多個(gè)線程操作這個(gè)變量的時(shí)候,實(shí)際操作自己本地內(nèi)存里面的變量,從而起到線程隔離的作用,避免了線程安全問題
ThreadLocal 適用于無狀態(tài),副本變量獨(dú)立后不影響業(yè)務(wù)邏輯的高并發(fā)場(chǎng)景,如果業(yè)務(wù)邏輯強(qiáng)依賴于變量副本,則不適合用 ThreadLocal 解決,需要另尋解決方案
應(yīng)用場(chǎng)景:
數(shù)據(jù)庫連接池會(huì)話管理中使用
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è):
1.initialValue()方法:返回為當(dāng)前線程初始副本變量值。
2.get()方法:獲取當(dāng)前線程的副本變量值。
3.set()方法:保存當(dāng)前線程的副本變量值。
4.remove()方法:移除當(dāng)前前程的副本變量值
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。 else createMap(t, value); // 如果當(dāng)前對(duì)象沒有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í)行流程:
1.獲得當(dāng)前線程,根據(jù)當(dāng)前線程獲得 map。
2.如果 map 不為空,則將參數(shù)設(shè)置到 map 中,當(dāng)前的 Threadlocal 作為 key。
3.如果 map 為空,則給該線程創(chuàng)建 map,設(shè)置初始值。
2、get()方法:
public T get() { Thread t = Thread.currentThread();//獲得當(dāng)前線程對(duì)象 ThreadLocalMap map = getMap(t);//線程對(duì)象對(duì)應(yīng)的map if (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沒有關(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); else createMap(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)無法獲得,則通過 initialValue 函數(shù)獲得初始值,然后給當(dāng)前線程創(chuàng)建新 map
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。
4、initialValue() 方法:
protected T initialValue() { return null; }
執(zhí)行流程:
(1)如果沒有調(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)中沒有 next 引用
ThreadLocalMap 中解決哈希沖突的方式并非鏈表的方式,而是采用線性探測(cè)的方式,當(dāng)發(fā)生哈希沖突時(shí)就將步長(zhǎng)加1或減1,尋找下一個(gè)相鄰的位置
流程說明:
1.根據(jù) ThreadLocal 對(duì)象的 hash 值,定位到 table 中的位置 i;
2.如果當(dāng)前位置是 null,就初始化一個(gè) Entry 對(duì)象放在位置 i 上;
3.如果位置 i 已經(jīng)有 Entry 對(duì)象了,如果這個(gè) Entry 對(duì)象的 key 與即將設(shè)置的 key 相同,那么重新設(shè)置 Entry 的 value;
4.如果位置 i 的 Entry 對(duì)象和 即將設(shè)置的 key 不同,那么尋找下一個(gè)空位置;
5、ThreadLocal 的內(nèi)存泄露:
在使用 ThreadLocal 時(shí),當(dāng)使用完變量后,必須手動(dòng)調(diào)用 remove() 方法刪除 entry 對(duì)象,否則會(huì)造成 value 的內(nèi)存泄露,嚴(yán)格來說,ThreadLocal 是沒有內(nèi)存泄漏問題,有的話,那也是忘記執(zhí)行 remove() 引起的
內(nèi)存泄露的根本原因在于 ThreadLocalMap 的生命周期與當(dāng)前線程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完沒有進(jìn)行手動(dòng)刪除導(dǎo)致的
ThreadLocal 的內(nèi)存泄露與強(qiáng)弱引用無關(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)該是無效了應(yīng)該設(shè)置為 null,也就是說對(duì)應(yīng)的 value 會(huì)在下次調(diào)用 ThreadLocal 的 set()、get() 方法時(shí),執(zhí)行底層 ThreadLocalMap 中的 expungeStaleEntry() 方法進(jìn)行清除無用的 value,從而避免內(nèi)存泄露。
6、ThreadLocal 的應(yīng)用場(chǎng)景:
(1)Hibernate 的 session 獲?。好總€(gè)線程訪問數(shù)據(jù)庫都應(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ù)庫連接上,Spring 采用 Threadlocal 的方式,來保證單個(gè)線程中的數(shù)據(jù)庫操作使用的是同一個(gè)數(shù)據(jù)庫連接,同時(shí)采用這種方式可以使業(yè)務(wù)層使用事務(wù)時(shí)不需要感知并管理 connection 對(duì)象,通過傳播級(jí)別,巧妙地管理多個(gè)事務(wù)配置之間的切換,掛起和恢復(fù)
7、如果想共享線程的 ThreadLocal 數(shù)據(jù)怎么辦 ?
使用 InheritableThreadLocal 可以實(shí)現(xiàn)多個(gè)線程訪問 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ǔ)空間就可以了,沒有必要作為成員變量多次被初始化。
到此這篇關(guān)于Java多線程之ThreadLocal原理總結(jié)的文章就介紹到這了,更多相關(guān)Java多線程ThreadLocal內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring-mvc/springboot使用MockMvc對(duì)controller進(jìn)行測(cè)試
這篇文章主要介紹了spring-mvc/springboot使用MockMvc對(duì)controller進(jìn)行測(cè)試,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11五種SpringBoot實(shí)現(xiàn)數(shù)據(jù)加密存儲(chǔ)的方式總結(jié)
這篇文章主要為大家詳細(xì)介紹了五種常見數(shù)據(jù)加密存儲(chǔ)的方法(結(jié)合SpringBoot和MyBatisPlus框架進(jìn)行實(shí)現(xiàn)),文中的示例代碼講解詳細(xì),需要的可以參考下2023-11-11SpringSecurity如何實(shí)現(xiàn)配置單個(gè)HttpSecurity
這篇文章主要介紹了SpringSecurity如何實(shí)現(xiàn)配置單個(gè)HttpSecurity,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java concurrency線程池之線程池原理(四)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java concurrency線程池之線程池原理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06