中進階階段開發者出去面試,應該躲不開ThreadLocal相關問題,本文就常見問題做出一些解答,歡迎留言探讨。
ThreadLocal為Java并發提供了一個新的思路, 它用來存儲Thread的局部變量, 進而達到各個Thread之間的隔離運作。它被廣泛應用于架構之間的使用者資源隔離、事務隔離等。
但是用不好會導緻記憶體洩漏, 本文重點用于對它的使用過程的疑難解答, 相信仔細閱讀完後的朋友可以随心所欲的安全使用它。
一、記憶體洩漏原因探索
ThreadLocal操作不當會引發記憶體洩露,最主要的原因在于它的内部類ThreadLocalMap中的Entry的設計。
Entry繼承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用,是以key'會在垃圾回收的時候被回收掉, 而key對應的value則不會被回收, 這樣會導緻一種現象:key為null,value有值。
key為空的話value是無效資料,久而久之,value累加就會導緻記憶體洩漏。
二、怎麼解決這個記憶體洩漏問題
每次使用完ThreadLocal都調用它的remove()方法清除資料。因為它的remove方法會主動将目前的key和value(Entry)進行清除。
e.clear()用于清除Entry的key,它調用的是WeakReference中的方法:this.referent = null
expungeStaleEntry(i)用于清除Entry對應的value, 這個後面會詳細講。
三、JDK開發者是如何避免記憶體洩漏的
ThreadLocal的設計者也意識到了這一點(記憶體洩漏), 他們在一些方法中埋了對key=null的value擦除操作。
這裡拿ThreadLocal提供的get()方法舉例,它調用了ThreadLocalMap#getEntry()方法,對key進行了校驗和對null key進行擦除。
如果key為null, 則會調用getEntryAfterMiss()方法,在這個方法中,如果k == null , 則調用expungeStaleEntry(i);方法。
expungeStaleEntry(i)方法完成了對key=null 的key所對應的value進行賦空, 釋放了空間避免記憶體洩漏。
同時它周遊下一個key為空的entry, 并将value指派為null, 等待下次GC釋放掉其空間。
同理, set()方法最終也是調用該方法(expungeStaleEntry), 調用路徑:
set(T value)->
map.set(this, value)->
rehash()->
expungeStaleEntries()
remove方法
remove()->
ThreadLocalMap.remove(this)->
expungeStaleEntry(i)
這樣做, 也隻能說盡可能避免記憶體洩漏, 但并不會完全解決記憶體洩漏這個問題。比如極端情況下我們隻建立ThreadLocal但不調用set、get、remove方法等。是以最能解決問題的辦法就是用完ThreadLocal後手動調用remove().
四、手動釋放ThreadLocal遺留存儲?你怎麼去設計/實作?
這裡主要是強化一下手動remove的思想和必要性,設計思想與連接配接池類似。
包裝其父類remove方法為靜态方法,如果是spring項目, 可以借助于bean的聲明周期, 在攔截器的afterCompletion階段進行調用。
弱引用導緻記憶體洩漏,那為什麼key不設定為強引用
這個問題就比較有深度了,是你談薪的小小資本。
如果key設定為強引用, 當threadLocal執行個體釋放後, threadLocal=null, 但是threadLocal會有強引用指向threadLocalMap,threadLocalMap.Entry又強引用threadLocal, 這樣會導緻threadLocal不能正常被GC回收。
弱引用雖然會引起記憶體洩漏, 但是也有set、get、remove方法操作對null key進行擦除的補救措施, 方案上略勝一籌。
線程執行結束後會不會自動清空Entry的value
一并考察了你的gc基礎。
事實上,當currentThread執行結束後, threadLocalMap變得不可達進而被回收,Entry等也就都被回收了,但這個環境就要求不對Thread進行複用,但是我們項目中經常會複用線程來提高性能, 是以currentThread一般不會處于終止狀态。
五、Thread和ThreadLocal有什麼聯系呢
ThreadLocal的概念。
Thread和ThreadLocal是綁定的, ThreadLocal依賴于Thread去執行, Thread将需要隔離的資料存放到ThreadLocal(準确的講是ThreadLocalMap)中, 來實作多線程處理。
相關問題擴充
加分項來了。
六、Spring如何處理Bean多線程下的并發問題
ThreadLocal天生為解決相同變量的通路沖突問題, 是以這個對于spring的預設單例bean的多線程通路是一個完美的解決方案。spring也确實是用了ThreadLocal來處理多線程下相同變量并發的線程安全問題。
spring 如何保證資料庫事務在同一個連接配接下執行的
要想實作jdbc事務, 就必須是在同一個連接配接對象中操作, 多個連接配接下事務就會不可控, 需要借助分布式事務完成。那spring 如何保證資料庫事務在同一個連接配接下執行的呢?
DataSourceTransactionManager 是spring的資料源事務管理器, 它會在你調用getConnection()的時候從資料庫連接配接池中擷取一個connection, 然後将其與ThreadLocal綁定, 事務完成後解除綁定。這樣就保證了事務在同一連接配接下完成。
概要源碼:
1.事務開始階段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
2.事務結束階段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource