ThreadLocal是一個關於創建線程局部變數的類。 通常情況下,我們創建的變數是可以被任何一個線程訪問並修改的。而使用ThreadLocal創建的變數只能被當前線程訪問,其他線程則無法訪問和修改。ThreadLocal在設計之初就是為解決併發問題而提供一種方案,每個線程維護一份自己的數據,達到線... ...
作者:京東物流 閆鵬勃
1 什麼是ThreadLocal?
ThreadLocal是一個關於創建線程局部變數的類。
通常情況下,我們創建的變數是可以被任何一個線程訪問並修改的。而使用ThreadLocal創建的變數只能被當前線程訪問,其他線程則無法訪問和修改。ThreadLocal在設計之初就是為解決併發問題而提供一種方案,每個線程維護一份自己的數據,達到線程隔離的效果。
2 有什麼作用?
2.1 set once,get everywhere
在現在的系統設計中,前後端分離已基本成為常態,分離之後如何獲取用戶信息就成了一件麻煩事,通常在用戶登錄後, 用戶信息會保存在Session或者Token中。這個時候,我們如果使用常規的手段去獲取用戶信息會很費勁,拿Session來說,我們要在介面參數中加上HttpServletRequest對象,然後調用 getSession方法,且每一個需要用戶信息的介面都要加上這個參數,才能獲取Session,這樣實現就很麻煩了。
在實際的系統設計中,我們肯定不會採用上面所說的這種方式,而是使用ThreadLocal,我們會選擇在攔截器的業務中, 獲取到保存的用戶信息,然後存入ThreadLocal,那麼當前線程在任何地方如果需要拿到用戶信息都可以使用ThreadLocal的get()方法 (非同步程式中ThreadLocal是不可靠的)
2.2 線程安全,空間換時間
在Spring的Web項目中,我們通常會將業務分為Controller層,Service層,Dao層, 我們都知道@Autowired註解預設使用單例模式,那麼不同請求線程進來之後,由於Dao層使用單例,那麼負責資料庫連接的Connection也只有一個, 如果每個請求線程都去連接資料庫,那麼就會造成線程不安全的問題,Spring是如何解決這個問題的呢?
在Spring項目中Dao層中裝配的Connection肯定是線程安全的,其解決方案就是採用ThreadLocal方法,當每個請求線程使用Connection的時候, 都會從ThreadLocal獲取一次,如果為null,說明沒有進行過資料庫連接,連接後存入ThreadLocal中,如此一來,每一個請求線程都保存有一份 自己的Connection。於是便解決了線程安全問題
3 ThreadLocal實戰應用
3.1 ehr中的使用
在登錄攔截器中將用戶信息寫入,後續使用時方便取值
3.2 分頁插件PageHelper中的應用
3.3 AopContext
4 源碼解讀
你是否有這樣的疑惑?為什麼可以直接拿到?對象存放在哪裡?存在什麼問題?
4.1 get方法
在 get() 方法中也會獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則把獲取 key 為當前 ThreadLocal 的值;否則調用 setInitialValue() 方法返回初始值,並保存到新創建的 ThreadLocalMap 中。
4.2 set方法
調用set時,直接調用set(T value) 方法中,首先獲取當前線程,然後在獲取到當前線程的 ThreadLocalMap,如果 ThreadLocalMap 不為 null,則將 value 保存到 ThreadLocalMap 中,並用當前 ThreadLocal 作為 key;否則創建一個 ThreadLocalMap 並給到當前線程,然後保存 value。
ThreadLocalMap 相當於一個 HashMap,是真正保存值的地方
map的set,如果map為空,則創建一個
4.3 initialValue() 方法
initialValue() 是 ThreadLocal 的初始值,預設返回 null,子類可以重寫改方法,用於設置 ThreadLocal 的初始值。
4.4 remove() 方法
ThreadLocal 還有一個 remove() 方法,用來移除當前 ThreadLocal 對應的值。同樣也是同過當前線程的 ThreadLocalMap 來移除相應的值。
getMap拿到了什麼?
在 set,get,initialValue 和 remove 方法中都會獲取到當前線程,然後通過當前線程獲取到 ThreadLocalMap,如果 ThreadLocalMap 為 null,則會創建一個 ThreadLocalMap,並給到當前線程
此處t是Thread,直接可以“點”拿到這個map
每個Thread對象內部都維護了一個ThreadLocalMap這樣一個ThreadLocal的Map,可以存放若幹個ThreadLocal
在使用 ThreadLocal 類型變數進行相關操作時,都會通過當前線程獲取到 ThreadLocalMap 來完成操作。每個線程的 ThreadLocalMap 是屬於線程自己的,ThreadLocalMap 中維護的值也是屬於線程自己的。這就保證了 ThreadLocal 類型的變數在每個線程中是獨立的,在多線程環境下不會相互影響。
5 使用註意事項
1)有可能導致記憶體泄漏,使用完畢後,需要remove
在 ThreadLocalMap 的 set(),get() 和 remove() 方法中,都有清除無效 Entry 的操作,這樣做是為了降低記憶體泄漏發生的可能。
Entry 中的 key 使用了弱引用的方式,這樣做是為了降低記憶體泄漏發生的概率,但不能完全避免記憶體泄漏。
假設 Entry 的 key 沒有使用弱引用的方式,而是使用了強引用:由於 ThreadLocalMap 的生命周期和當前線程一樣長,那麼當引用 ThreadLocal 的對象被回收後,由於 ThreadLocalMap 還持有 ThreadLocal 和對應 value 的強引用,ThreadLocal 和對應的 value 是不會被回收的,這就導致了記憶體泄漏。所以 Entry 以弱引用的方式避免了 ThreadLocal 沒有被回收而導致的記憶體泄漏,但是此時 value 仍然是無法回收的,依然會導致記憶體泄漏。
ThreadLocalMap 已經考慮到這種情況,並且有一些防護措施:在調用 ThreadLocal 的 get(),set() 和 remove() 的時候都會清除當前線程 ThreadLocalMap 中所有 key 為 null 的 value。這樣可以降低記憶體泄漏發生的概率。所以我們在使用 ThreadLocal 的時候,每次用完 ThreadLocal 都調用 remove() 方法,清除數據,防止記憶體泄漏。
2)使用線程池時,父子線程傳遞慎用,因為初始化時機為線程創建時
3)針對2有什麼方案可以解決?
TransmittableThreadLocal
源碼地址: https://github.com/alibaba/transmittable-thread-local
詳解:https://www.jianshu.com/p/e0774f965aa3