ThreadLocal介紹以及原始碼分析
ThreadLocal 執行緒主變數
前面部分引用其他優秀部落格,後面原始碼自己分析的,如有冒犯請私聊我。
用Java語言開發的同學對 ThreadLocal 應該都不會陌生,這個類的使用場景很多,特別是在一些框架中經常用到,比如資料庫事務操作,還有MVC框架中資料跨層傳遞。這裡我們簡要探討下 ThreadLocal 的內部實現及可能存在的問題。
首先問自己一個問題,讓自己實現一個這個的功能類的話怎麼去做?第一反應就是簡單構造一個 Map<Thread, T> 資料結構,key是 Thread ,value就是我們要儲存的執行緒變數 T。我們看下這種設計有哪些問題:
- 隨著執行時間越久,存在Map裡的Thread越多,當Thread退出時,資源也沒有釋放,存在記憶體洩漏問題
- Map資料因為會被多執行緒訪問,存在資源競爭,所以還必需對Map做同步安全操作,效率低下
JDK中的 ThreadLocal 精妙的設計來解決問題上述兩個問題。首先每個Thread(執行緒)內部都有一個Map結構資料 ThreadLocalMap<ThreadLocal, T> ,當我們對執行緒變數賦值時 ThreadLocal.set(T value) 時,其實是先獲取當前執行緒 Thread.currentThread()) 的內部屬性欄位 ThreadLocalMap ,然後以當前 ThreadLocal 為key設定執行緒變數值T。這種設計的精髓是,每個 Thread 執行緒都維護一份自己的 ThreadLocalMap資料結構,這樣就解決了上面所述問題中的第二個,不存在競爭條件。
既然每個 Thread 內部都維護一個 ThreadLocalMap 字典資料結構,字典的Key值是 ThreadLocal ,那麼當某個 ThreadLocal 物件不再使用(沒有其它地方再引用)時,每個已經關聯了此 ThreadLocal 的執行緒怎麼在其內部的 ThreadLocalMap 裡做清除此資源呢?JDK中的 ThreadLocalMap 又做了一次精彩的表演,它沒有繼承 java.util.Map 類,而是自己實現了一套專門用來定時清理無效資源的字典結構。其內部儲存實體結構 Entry<ThreadLocal, T> 繼承自 java.lan.ref. WeakReference ,這樣當 ThreadLocal 不再被引用時,因為弱引用機制原因,當jvm發現記憶體不足時,會自動回收弱引用指向的例項記憶體,即其執行緒內部的 ThreadLocalMap 會釋放其對 ThreadLocal 的引用從而讓jvm回收 ThreadLocal物件。這裡是重點強調下,是回收對
問題真的都解決了嗎,好像都解決了。因為即沒有競爭資源操作,也不會存在記憶體洩漏。但是細想一下,總感覺哪裡不對勁,真的不會存在記憶體溢位(OOM)問題嗎?上面一段的分析中,強調 ThreadLocalMap 會 定期 清理內部的無效 Entry 物件,觸發的條件就是對 TrheadLocal 執行 set,get,remove()等操作時會觸發,但是如果存在這樣的場景,當我們在某個執行緒上下文中執行 ThreadLocal.set(T) 設定了一個很大記憶體的資料結構,然後該 ThreadLocal 被清除引用回收,之前的執行緒又一直存活著,則這個大記憶體資料物件 T是一直不回收的,這裡很容易寫個程式碼測試出OOM來。怎麼解決這個問題呢?
Lucene中的 org.apache.lucene.util.CloseableThreadLocal 類解決了上述特殊場景引起的問題:即解決JDK中因為定期才執行無效物件回收的問題。 CloseableThreadLocal 在內部維護了一個 ThreadLocal ,當執行 CloseableThreadLocal.set(T) 時,內部其實只是代理的把值賦給內部的 ThreadLocal 物件,即執行 ThreadLocal.set(new WeakReference(T)) 。看到這裡應該明白了,這裡不是直接儲存 T ,則是包裝成弱引用物件,目的就是當記憶體不足時,jvm可以回收此物件。但是細心的你會發現會引入一個新的問題,即當前執行緒還存活著的時候,因為記憶體不足而回收了弱引用物件,這樣會在下次呼叫 get() 時取不到值返回null,這是不可接受的。所以 CloseableThreadLocal 在內部還建立了一個數據, WeakHashMap<Thread, T> ,當執行緒只要存活時,則T就至少有一個引用存在,所以不會被提前回收。但是又引入的第2個問題,對 WeakHashMap 的操作要做同步 synchronized限制。你看,所有的東西都不是十全十美的,我們掌握那個平衡點就行了。
ThreadLocal原始碼分析
原始碼介紹
1、每個執行緒訪問自己維護的的一份副本
2、ThreadLocal 類內部維護了一個私有的欄位去跟對應的執行緒聯絡(例如 userid 或者TranslationId) 這個欄位第一次呼叫時生成後面呼叫不會改變
3、只要這個執行緒活著而且例項可以被訪問,這個執行緒會持有這個變數副本的隱性引用,直到執行緒消亡,被垃圾回收
ThreadLocal get方法
1、根據當前thread獲取對應的ThreadLocalMap 如果沒有就初始化設定一個,如果有就返回 ThreadLocalMap 裡面維護的Entry儲存的值
1 public T get() { 2Thread t = Thread.currentThread(); 3ThreadLocalMap map = getMap(t); 4if (map != null) { 5ThreadLocalMap.Entry e = map.getEntry(this); 6if (e != null) { 7@SuppressWarnings("unchecked") 8T result = (T)e.value; 9return result; 10} 11} 12return setInitialValue(); 13 }
ThreadLocal 初始化value
倆個步驟 1、在原有的map上設定值 2、建立一個ThreadLocalMap
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; }
ThreadLocal rehash 1、先刪除陳舊的Entriy ,如果不能有效的收縮table的長度,而且長度已經大於threshold 的0.75(裝載因子)倍了,就直接擴充套件一倍長度
1 2 /** 3* Re-pack and/or re-size the table. First scan the entire 4* table removing stale entries. If this doesn't sufficiently 5* shrink the size of the table, double the table size. 6*/ 7 private void rehash() { 8expungeStaleEntries(); 9 10// Use lower threshold for doubling to avoid hysteresis 11if (size >= threshold - threshold / 4) 12resize(); 13 }
如果你也有此類問題,可以一起探討(私聊或者評論),一起不斷完善自己的理解,如果覺得可以歡迎關注我。