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