1. 程式人生 > 實用技巧 >ThreadLocal原理,記憶體洩漏問題,怎麼解決

ThreadLocal原理,記憶體洩漏問題,怎麼解決

ThreadLocal的作用

ThreadLocal是線上程使用共享資源不是用來通訊的時候,即不是(生產者-消費者模式,通過一個訊息陣列來進行通訊),那就沒必要把它定義為共享資源(即成員變數),而是採用ThreadLocal來處理這個變數,使得它擁有成員變數的特性(類中甚至執行緒中全域性可用)。ThreadLocal的作用是提供執行緒內的區域性變數,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或者元件之間一些公共變數的傳遞的複雜度。ThreadLocal的設計本身就是為了能夠在當前執行緒中有屬於自己的變數,並不是為了解決併發或者共享變數的問題。因為自己的變數肯定不會有併發問題的。但是這樣確實是避免了這個變數使用過程中的執行緒安全問題。
把一個變數的使用範圍限制在一個執行緒內,其他執行緒訪問不到這個變數,這樣這個變數也就不會有執行緒安全問題。ThreadLocal 是以空間換取執行緒安全,而通過加鎖來實現執行緒安全,則是以時間為代價的。所以使用ThreadLocal 在某些情況下可能會獲得更好的效能。ThreadLocal為每個使用該變數的執行緒分配一個獨立的變數副本。所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其他執行緒所對應的副本

ThreadLocal 的應用場景

spring中的service, controller, dao都是單例的,全域性唯一,所有執行緒共用一個。但是這幾個單例物件的成員變數對於每個執行緒來說都是不一樣的,那既然sevice, controller, dao是單例的,那他們對應的成員變數應該也是全域性唯一的,為了解決併發問題,一定要對這個成員變數進行加鎖。有沒有不加鎖的方法呢,有,
1. 把上面三個物件都定義成多例的,每個執行緒都有一個例項物件,他們的成員變數也就不存線上程爭用問題了。2. 維持service, controller, dao 仍然是單例的,但是把他們的成員變數存在每個執行緒裡,而不是存在這三個例項裡面。把這個成員變數變數變成執行緒的變數,而不是例項的變數,這就要使用threadlocal了。threadlocal可以把一個變數變成執行緒的變數。

舉個栗子

spring中Dao物件必須包含一個數據庫的連線Connection, 因為不能給每個方法都new 一個connection物件,所以這個connection必須定義成一個dao的成員變數,讓所有dao方法都使用同一個connection, 但是問題來了,因為dao是單例的,所以dao的成員變數connection必定也是全域性唯一的,所以每個執行緒在使用connection的時候肯定會存在一個爭用問題,這樣這個connection就存在一個併發問題。有沒有解決辦法呢?有
1. 把dao物件宣告成多例的並connection宣告成dao的成員變數。這樣每個執行緒都能獲取到一個dao例項,使用的也是不同的connection,connection的使用就不會有執行緒安全問題2. 仍然保持dao物件是單例的,但是不把connection定義成dao的成員變數,而是定義成執行緒的變數。這樣同一個執行緒的dao物件的所有方法都可以使用同一個connection, 並且connection不會有執行緒安全問題。而把把變數定義成執行緒的變數就是靠threadlocal來完成的。Thread到底是什麼?他到底是怎麼實現把變數存到執行緒裡面的?這又會帶來哪些問題?下面一一揭曉

threadlocal是什麼

先看一張CyC2018大佬畫的一張 關於 Thread、ThreadLocalMap、ThreadLocal 三個物件的關係圖
可能你現在對這個圖不太理解,不太看得明白,沒關係,繼續往下看。
ThreadLocal 原理
每個Thread類中有一個成員變數 threadlocals , 這個成員變數是一個 ThreadLocalMap的型別物件,我們按住 Ctrl 鍵滑鼠點選 ThreadLocalMap , 進入檢視ThreadLocalMap 的底層實現可以看到一個 Entry[] 陣列,每個Entry的底層實現為可以看出,每個 Entry 鍵值對的鍵是 一個ThreadLocal 物件,值是一個 Object 物件。這個ThreadLocalMap 類提供了set() 方來設定新增鍵值對, 提供了get()方法來獲取鍵值對,提供了remove()方法來刪除鍵值對,看到這些方法,是不是覺得和 ThreadLocal 物件很像,因為ThreadLocal 類也提供了這個三個方法,分別用來儲存物件值,獲取物件值,以及移除物件值。我們來看ThreadLocal 類中這三個方法的底層實現當我用呼叫ThreadLcoal物件的set()方法時, ThreadLocal物件會獲取到當前當前執行緒的引用,根據這個引用獲取到執行緒的成員ThreadLocalMap物件,然後後呼叫ThreadLocalMap物件的set方法儲存到這個Map中。看似我們是把資料儲存在了ThreadLcoal物件中,但是實際上我們是把資料儲存在當前執行緒的ThreadLocalMap中。而threadlocal只是用來線上程中查詢這個物件而已
ThreadLocal的get()方法也是類似,先獲取當前執行緒物件引用,然後獲取這個執行緒的成員物件ThreadLocalMap,以 ThreadLocal 引用為鍵,取出這個鍵值對中的值。remove方法也是先獲取當前執行緒物件引用,然後獲取這個執行緒的成員物件ThreadLocalMap,最後移除以 ThreadLocal 引用為鍵的鍵值對。看到這裡,應該很清楚,ThreadLocal 的執行緒安全原理了。ThreadLocal 的set(), get(), remove()方法實際上在操作當前執行緒成員變數 threadlocals, 這個變數的型別是一個ThreadLocalMap, 所以當我們往ThreadLocal中新增值實際上是把值新增到了當前執行緒中,從 ThreadLocal 物件中取值實際上是從當前執行緒中取值,從ThreadLocal 物件中移除值實際上是從把這個值從當前執行緒中移除,所以一切操作都是在操作當前執行緒中的值,threadlocal在這裡只是相當於一個索引作用。那麼對 ThreadLocal 中儲存的物件進行操作當然就是執行緒安全的了,因為始終都是操作的當前執行緒,不涉及到其他執行緒,當然就不會執行緒不安全了。現在再回過頭去看文章開頭的關係圖,是不是覺得豁然開朗了~~

注意:

因為每個健值在ThreadMap中是唯一的,它唯一標識了一個健值對,所以我們在ThreadLocalMap中不能儲存多個健值相等的鍵值對,而因為這個ThreadLocalMap是以ThreadLocal物件引用為健值,所以一個ThreadLocalMap物件只能儲存一個以同一個ThreadLocal物件引用為鍵的鍵值對,也就是每個執行緒對同一個ThreadLocal物件,只能儲存一個數據物件。

ThreadLocal的記憶體洩漏問題

再次檢視一下 Entry 類的定義可以看到,Entry繼承自WeakReference<ThreadLocal<?>>Entry的 key是ThreadLocal物件引用,這個引用是一個弱引用。當沒指向 key 的強引用後,該key就會被垃圾收集器回收。

在ThreadLocalMap中,entry的key是弱引用,value仍然是一個強引用。當某一條執行緒中的ThreadLocal使用完畢,沒有強引用指向它的時候,這個key指向的物件就會被垃圾收集器回收,從而這個key就變成了null;所以entry就變成了(null, value), 而entry 和 value 都是強引用,並且只要entry還在,value就一直存在。所以如果我們不手動清理掉這些鍵為空的entry, 線上程執行完畢之前,這個entry就一直處於記憶體洩漏的狀態。執行緒生命週期越長,記憶體洩漏的就越多。

解決辦法:

不過不用擔心,ThreadLocal提供了這個問題的解決方案。

每次操作set、get、remove操作時,會相應呼叫 ThreadLocalMap 的三個方法,ThreadLocalMap的三個方法在每次被呼叫時 都會直接或間接呼叫一個expungeStaleEntry()方法,這個方法會將key為null的 Entry 刪除,從而避免記憶體洩漏。

那麼問題又來了,如果一個執行緒執行週期較長,而且將一個大物件放入LocalThreadMap後便不再呼叫set、get、remove方法仍然有可能key的弱引用被回收後,引用沒有被回收,此時該仍然可能會導致記憶體洩漏。

這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程式設計師在完成ThreadLocal的使用後要養成手動呼叫remove的習慣,從而避免記憶體洩漏。

既然弱引用會導致記憶體洩漏,那ThreadLocalMap為什麼對ThreadLocal的引用要設定成弱引用?

為了儘快回收這個執行緒變數,因為這個執行緒變數可能使用場景不是特別多,所以希望使用完後能儘快被釋放掉。因為執行緒擁有的資源越多,就越臃腫,執行緒切換的開銷就越大,所以希望儘量降低執行緒擁有的資源量。

參考:

ThreadLocal記憶體洩漏原因以及避免方案

揭祕ThreadLocal
手撕面試題ThreadLocal!!!
深入理解ThreadLocalThreadLocal的原理以及在Spring中的應用(好文,建議閱讀完上面幾遍文章後再來看此文)