ThreadLocal記憶體洩漏問題

CodeWalker發表於2018-09-25
一、概述

ThreadLocal類用來提供執行緒內部的區域性變數。這些變數在多執行緒環境下訪問(通過get或set方法訪問)時能保證各個執行緒裡的變數相對獨立於其他執行緒內的變數,ThreadLocal例項通常來說都是private static型別。 總結:ThreadLocal不是為了解決多執行緒訪問共享變數,而是為每個執行緒建立一個單獨的變數副本,提供了保持物件的方法和避免引數傳遞的複雜性。

 ThreadLocal的主要應用場景為按執行緒多例項(每個執行緒對應一個例項)的物件的訪問,並且這個物件很多地方都要用到。例如:同一個網站登入使用者,每個使用者伺服器會為其開一個執行緒,每個執行緒中建立一個ThreadLocal,裡面存使用者基本資訊等,在很多頁面跳轉時,會顯示使用者資訊或者得到使用者的一些資訊等頻繁操作,這樣多執行緒之間並沒有聯絡而且當前執行緒也可以及時獲取想要的資料。


二、原理

ThreadLocal可以看做是一個容器,容器裡面存放著屬於當前執行緒的變數。ThreadLocal類提供了四個對外開放的介面方法,這也是使用者操作ThreadLocal類的基本方法:

 (1) void set(Object value)設定當前執行緒的執行緒區域性變數的值。

 (2) public Object get()該方法返回當前執行緒所對應的執行緒區域性變數。 

 (3) public void remove()將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度。 

(4) protected Object initialValue()返回該執行緒區域性變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲呼叫方法,線上程第1次呼叫get()或set(Object)時才執行,並且僅執行1次,ThreadLocal中的預設實現直接返回一個null。 可以通過上述的幾個方法實現ThreadLocal中變數的訪問,資料設定,初始化以及刪除區域性變數,那ThreadLocal內部是如何為每一個執行緒維護變數副本的呢? 其實在ThreadLocal類中有一個靜態內部類ThreadLocalMap(其類似於Map),用鍵值對的形式儲存每一個執行緒的變數副本,ThreadLocalMap中元素的key為當前ThreadLocal物件,而value對應執行緒的變數副本,每個執行緒可能存在多個ThreadLocal。


三、記憶體洩漏問題

每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收。所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的就可能出現記憶體洩露 。(在web應用中,每次http請求都是一個執行緒,tomcat容器配置使用執行緒池時會出現記憶體洩漏問題)

ThreadLocal記憶體洩漏問題

ThreadLocal只是操作Thread中的ThreadLocalMap,每個Thread都有一個map,ThreadLocalMap是執行緒內部屬性,ThreadLocalMap生命週期是和Thread一樣的,不依賴於ThreadMap。

ThreadLocal通過Entry儲存在map中,key為Thread的弱引用(GC時會自動回收),value為存入的變數副本,一個執行緒不管有多少個ThreadLocal,都是通過一個ThreadLocalMap來存放區域性變數的,可以再原始碼中看到,set值時先獲取map物件,如果不存在則建立,threadLocalMap初始大小為16,當容量超過2/3時會自動擴容。


四、總結

1.使用ThreadLocal,建議用static修飾 static ThreadLocal<HttpHeader> headerLocal = new ThreadLocal();

2.使用完ThreadLocal後,執行remove操作,避免出現記憶體溢位情況。


相關文章