淺談ThreadLocal----每個線程一個小書包
ThreadLocal是什么?
thread是線程,local是本地的意思
字面意思是線程本地。
其實更通俗的理解是給每個線程設置一個緩存。這個緩存用來存儲當前線程在未來的業務邏輯中需要執行到的變量。
我們先來看怎么用:
首先創建全局變量ThreadLocal,
各自啟動一個線程任務:
線程任務將變量設置到緩存中。
線程任務需要用到緩存中的變量時,直接從緩存中取即可。
1 import java.util.concurrent.TimeUnit; 2 3 /** 4 * @discription 5 */ 6 public class ThreadLocalLearn { 7 static ThreadLocal<String> threadLocal = new ThreadLocal<>(); 8 9 public static void main(String[] args) { 10 Runnable r = new Runnable() { 11 @Override 12 public void run() { 13 threadLocal.set(Thread.currentThread().getName()); 14 sayMyName(); 15 threadLocal.remove(); 16 } 17 18 public void sayMyName() { 19 for (int i = 0; i < 3; i++) { 20 String name = threadLocal.get(); 21 System.out.println(Thread.currentThread().getName() + " say: im a thread, name:" + name); 22 try { 23 TimeUnit.SECONDS.sleep(3); 24 } catch (Exception e) { 25 //... 26 } 27 } 28 } 29 }; 30 Thread t1 = new Thread(r); 31 t1.start(); 32 Thread t2 = new Thread(r); 33 t2.start(); 34 } 35 }
它的使用非常簡單,
(1)先set()存儲值;
(2)使用時get()取出值;
(3)用完了使用remove()清理掉;
輸出如下:
Connected to the target VM, address: '127.0.0.1:56863', transport: 'socket' Thread-0 say: im a thread, name:Thread-0 Thread-1 say: im a thread, name:Thread-1 Thread-0 say: im a thread, name:Thread-0 Thread-1 say: im a thread, name:Thread-1 Thread-1 say: im a thread, name:Thread-1 Thread-0 say: im a thread, name:Thread-0 Disconnected from the target VM, address: '127.0.0.1:56863', transport: 'socket'
很多人第一次見到ThreadLocal,第一直覺它的實現是用Map<Thread,Object> 。(防盜連接:本文首發自http://www.rzrgm.cn/jilodream/ )但是深入研究之后,你會發現threadLocal的實現要比這樣一個map 精妙的多,也好用的多。
我們通過查看java源碼,可以依次探索ThreadLocal是如何實現緩存的:
類整體的關系大概是這樣的:

查看源碼,我們可以發現如下特性:
1、ThreadLocal本身并不是緩存,它只是起到一個緩存的key 的作用。我們每次創建一個ThreadLocal 并不是真正的創建了一個緩存,其實只是創建了一個緩存的標識。
源碼如下:this 就是ThreadLocal實例
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 map.set(this, value); 6 } else { 7 createMap(t, value); 8 } 9 }
2、真正的緩存保存在Thread中,緩存被定義為:
ThreadLocal.ThreadLocalMap threadLocals;
從名字可以發現,這個緩存的類型是在ThreadLocal 中定義的一個靜態內部類。這個類就是用來真正存放緩存的地方。這就像是thread小書包一樣,每個線程有一個自己的獨立的存儲空間。
設計疑問:它(ThreadLocalMap)為什么沒有定義在Thread類中,畢竟它是Thread的緩存。
源碼如下:Thread.java
1 /* ThreadLocal values pertaining to this thread. This map is maintained 2 * by the ThreadLocal class. */ 3 ThreadLocal.ThreadLocalMap threadLocals = null;
3、查看ThreadLocalMap的源碼,我們發現它并沒有實現Map接口,就像其他map一樣,ThreadLocalMap實現了常用的Map中的set,get,getEntry,setThreshold,,remove 等方法。
并且它內部使用了線性探測法來解決哈希沖突。
設計疑問:它(ThreadLocalMap)為什么沒有實現Map接口?
源碼如下:ThreadLocal.Java
1 static class ThreadLocalMap { 2 3 //... 4 5 private static final int INITIAL_CAPACITY = 16; 6 7 8 private Entry[] table; 9 10 11 private int size = 0; 12 13 14 private int threshold; // Default to 0 15 16 17 private void setThreshold(int len) { 18 threshold = len * 2 / 3; 19 } 20 21 22 private Entry getEntry(ThreadLocal<?> key) { 23 ... 24 } 25 26 27 28 private void set(ThreadLocal<?> key, Object value) { 29 ... 30 } 31 32 33 private void remove(ThreadLocal<?> key) { 34 ... 35 } 36 37 38 private void rehash() { 39 ... 40 } 41 42 private void resize() { 43 ... 44 } 45 .... 46 }
4、繼續看源碼,我們發現ThreadLocalMap類像其他Map實現一樣,在內部定義了Entry。并且這個Entry居然繼承了弱引用,弱引用被定義在Entry的key上,而且key的類型是ThreadLocal。
至于什么是弱引用,我以前的文章中介紹過,請看(淺談Java中的引用 http://www.rzrgm.cn/jilodream/p/6181762.html),一定要對弱引用了解,否則ThreadLocal的核心實現以及它會存在的問題,就無法更深理解了。
這里又會有疑問,為什么要使用弱引用,使用強引用不好嗎?弱引用萬一被回收導致空引用等問題怎么辦?
源碼如下:ThreadLocal.Java
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
我們依次回答這幾個問題:
(1)設計疑問:它(ThreadLocalMap)為什么沒有定義在Thread類中,畢竟它是Thread的緩存
這恰恰是Thread符合開閉原則的優秀設計。如果是將ThreadLocalMap添加到Thread中,那么Thread類就太重了,以后只要和線程相關的業務都要將代碼添加到Thread中,那Thread就無限膨脹了,變成超級類了,試想什么業務和線程能脫離關系呢?
況且他們只是類依賴關系而不是組合關系(對類關系不了解的同學可以看我的這篇文章:統一建模語言UML---類圖 http://www.rzrgm.cn/jilodream/p/16693511.html)。
Map怎么實現,緩存怎么維護,這些都是Thread不需要考慮的,我們就是需要用到你的特性。
(2)設計疑問:它(ThreadLocalMap)為什么沒有實現Map接口?
實現接口是為了統一化提供接口,讓外界可以只依賴接口,而不是接口的實現。但是ThreadLocalMap并不是給外界使用的,并不需要暴露出來。他就是為了給ThreadLocal業務使用的。只要完成最核心的Map能力,用空間換時間,將理論時間復雜度推向O(1)即可。因此完全沒有必要實現Map接口。實現了Map接口反而要將內部方法暴露為public,這也不符合最少知道原則。一句話就是沒必要,還添亂。
(3)為什么要使用弱引用,使用強引用不好嗎?弱引用萬一被回收導致空引用等問題怎么辦?
我們需要先了解弱引用的特性:當一個變量只有弱引用關聯時,那么在下次GC回收時,不論我們內存是否足夠,都將回收掉該內存。
第一眼感覺這很危險,畢竟我們非常擔心就是一個變量用著用著突然不能用了,出現空引用了,漫天的空引用這太不可控了。
其實這完全多慮了,注意看:我們是如何使用緩存的,是通過threadlocal.get(),也就是說我們想要使用緩存就一定要使用threadlocal的實例,也就是強引用,
有了強引用,使用時就一定不會被回收。因此完全不用擔心使用緩存中,弱引用key突然變為null的情況了。
那什么時候弱引用key會被回收呢?
這就是當外界的強引用被手動設置為null時,(防盜連接:本文首發自http://www.rzrgm.cn/jilodream/ )或者是作為局部變量跳出了方法棧,超出生命周期被回收掉了。
試想一下,真要是發生這兩種情況,那么其實這個緩存也就根本無法再用到了同時,key被盡快回收,反而對內存更有利。
那么弱引用這么好用,為什么value不設置為弱引用呢?
其實細想一下就會發現value一定不能設置為弱引用,為什么呢?
key設置為弱引用,是因為想要使用這個緩存,key就一定要有強引用關聯。而value則不一定有外界強引用關聯,它在外界的強引用可能早就消失了。比如下面這個例子:
1 import java.util.concurrent.TimeUnit; 2 3 /** 4 * @discription 5 */ 6 public class ThreadLocalLearn { 7 static ThreadLocal<UserInfo> userContext = new ThreadLocal<>(); 8 9 public static void main(String[] args) { 10 Runnable r = new Runnable() { 11 @Override 12 public void run() { 13 setUserInfo(); 14 handle(); 15 userContext.remove(); 16 } 17 18 public void handle() { 19 UserInfo user = userContext.get(); 20 //注意倘若map中的value被定義為弱引用,則此處的user可能為null 21 System.out.println(" i am:" + user.toString()); 22 //do sth 23 try { 24 TimeUnit.SECONDS.sleep(3); 25 } catch (Exception e) { 26 //... 27 } 28 } 29 }; 30 Thread t1 = new Thread(r); 31 t1.start(); 32 Thread t2 = new Thread(r); 33 t2.start(); 34 } 35 36 private static void setUserInfo() { 37 UserInfo user = new UserInfo();// 假裝是從db中獲取的 38 userContext.set(user); 39 //跳出該方法后,userInfo的在外部的直接強引用就被回收了 40 } 41 } 42 43 class UserInfo { 44 private String name; 45 private int age; 46 47 //.... 48 }
我們在A方法中設置了緩存 currentUserId,跳出A方法,currentUserId在外界的引用被斷開,倘若此時value也被定義為弱引用,value就隨時可能被回收。而我們又可以通過
(key)Threadlocal --> threadLocals(ThreadLocalMap) --> entry --> value
這樣的調用關系來拿到緩存value。這樣緩存的使用就不可控了。
那么value一定不能設置為弱引用或及時回收么?
并不是,
其實我們只要在key回收時,順手對value也做一個回收,但是這是GC完成的,再key消失時,聯動對所有線程中關聯的Map都進行一遍清理。(實現過于復雜)
亦或者清理key(threadlocal)的強引用時,將value的強引用也一并被清理。
可行,也是ThreadLocal推薦的方式,需要手動調用ThreadLocal.remove 方法。
在調用remove方法后,ThreadLocalMap會對所有垃圾數據進行清理,還會壓縮哈希表。
為了解決ThreadLocalMap的value 延遲清理的情況,ThreadLocalMap在set get remove等方法中,都會對ThreadLocalMap存在的這種<null,Object> 垃圾數據進行一定程度的清理(注意這里要分各種情況,具體只能詳細分析源碼了,一篇博文很難說清)。
(4)這樣又會有一個新的問題,如果key 被回收了,但是value沒有被回收,因此value就常駐內存了,那么value不就會導致內存泄露嗎?
很不幸,這樣的確是會導致內存的泄露。(這里簡單提一下,java中的內存泄露是指,可以通過強引用關聯到他,gc無法回收掉它。與此同時,業務按照正常邏輯又無法使用到它。也就是又用不到,又回收不掉,就稱之為內存泄露)
但是這種內存泄露出現的概率非常低。
它需要同時滿足以下三個條件才可以:
1、需要線程的生命周期永遠不會結束。如果線程生命周期結束了,那么ThreadLocalMap就會被回收,里邊出現的無其他關聯的key value 也都會被回收。
這種一般是守護線程或者線程池(線程復用出現)
2、ThreadLocal在設置為null時,沒有手動調動remove方法
3、線程中的ThreadLocalMap在后續使用中,沒有再調用任何get set remove方法,也就是線程沒再使用ThreadLocal
概率低,是不是代表不太需要關注,當然不是。
因為內存泄露不僅僅是減少了可用內存,還增加了GC負擔,系統性能就會收到影響,這就說的遠了。
其實ThreadLocal最大的問題,并不是泄露的問題,而是被濫用的問題,不規范使用的問題。很多人把ThreadLocal當成是線程的私有倉庫,所有變量參數都往里邊塞,
導致寫代碼和維護時,非常不方便,出現問題也給維護人員造成很大的困擾。
接下來我們簡單說下ThreadLocal的使用(后邊我會再寫一篇,如何使用ThreadLocal,畢竟我們學習技術目的是能夠駕馭它,而不僅僅是知其所以然):
我們一般是將上下文信息,或者當前需要頻繁使用的,與實際業務直接關系不大的系統數據方便攜帶。放置到thread的小書包中。
(1)上下文信息
如我們在controller層,將用戶的上下文信息傳入,如traceId(方便鏈路追蹤),如用戶token,后續可能調用其他鑒權接口等
(2)解耦數據庫連接等連接池信息,
比如Springboot運行事務時,我們每次getconnection(),就只使用ThreadLocal中貯存好的這個連接,整個方法使用的是同一個數據庫連接。
以上場景不使用ThreadLocal可以嗎?
也可以,他并不是一定要使用。但是你這樣就要把很多的參數傳來傳去,暴露很多的問題。
甚至在很多第三方實現的框架中,他不支持你傳這些參數,他就是要用通過ThreadLocal來回傳值。
(3)為線程安全提供了方案,減少了鎖競爭:
如果說鎖是從資源競爭的角度,解決了數據安全的問題。
ThreadLocal則是在每個線程中,只保存(只隔離)出與自己當前業務相關的數據。
注意他只是保證了數據的獨立性,并不是獨立創建了一份副本,(防盜連接:本文首發自http://www.rzrgm.cn/jilodream/ )所以如果使用全局數據放置到value中時,一樣可能會有數據安全問題。(當然這也是不推薦的用法)
比如有一份UserCache的全局緩存,多線程使用時,
我可以在全局中對UserCache進行加鎖處理,也可以每個線程獨立引用自己的UserInfo,線程之間互不干擾。結構就像這個樣子:
全局加鎖:

線程各自引用:

不知講到這里大家還有沒有最初的直覺了,為啥不設計一個全局的 Map<Thread,Object>。這樣不是更簡單,也更好定位問題:
細想一下,就會發現這樣并不好:
方案1,全局只有一個Map,value是當前線程的所有緩存數據。那么Object就是一個非常復雜的數據,每次對Object進行讀取都要解析的特別復雜。
方案2,全局定義的很多個Map,每個map是一個業務的緩存,比如User,就有userMap,token就有tokenMap。先不論Map本來就會有競爭的問題,對于管理大量的Map就是一件頭痛的事情。
當然還是要根據具體業務來看,不能一概而論,并不能說任何時候使用ThreadLocal更好,使用全局Map更弱
如果你覺得寫的不錯,歡迎轉載和點贊。 轉載時請保留作者署名jilodream/王若伊_恩賜解脫(博客鏈接:http://www.rzrgm.cn/jilodream/

浙公網安備 33010602011771號