1. 程式人生 > >ThreadLocal原理、使用場景及存在記憶體洩漏的原因

ThreadLocal原理、使用場景及存在記憶體洩漏的原因

什麼是執行緒封閉

當多執行緒訪問共享變數時,往往需要加鎖來保證共享變數的執行緒安全(資料同步)。一種避免使用加鎖方式就是不共享資料,而是讓執行緒獨享資料。由於資料本身就是執行緒私有的,這樣,如果僅在單執行緒內訪問資料就不需要同步,這種避免共享資料的技術稱為執行緒封閉。在Java語言中,提供了一些類庫和機制來維護執行緒的封閉性,例如區域性變數(存放線上程棧中)和ThreadLocal類,本文首先介紹ThreadLocal類保證執行緒封閉的原理,再講一下如何使用ThreadLocal類來保證執行緒封閉。

以下內容全部來自 ThreadLocal的設計理念與作用 -《 Java基礎 》一文,連結

http://blog.qianxuefeng.com/article/153

ThreadLocal是什麼

ThreadLocal是一個關於建立執行緒區域性變數的類。通常情況下,我們建立的變數是可以被任何一個執行緒訪問並修改的。ThreadLocal類允許我們建立只能被同一個執行緒讀寫的變數。因此,如果一段程式碼含有一個ThreadLocal變數的引用,即使兩個執行緒同時執行這段程式碼,它們也無法訪問到對方的ThreadLocal變數。

如何建立ThreadLocal

我們可以看到,通過程式碼例項化了一個ThreadLocal物件。我們只需要例項化物件一次,並且也不需要知道它是被哪個執行緒例項化。雖然所有的執行緒都能訪問到這個ThreadLocal例項,但是每個執行緒卻只能訪問到自己通過呼叫ThreadLocal的set()方法設定的值。即使是兩個不同的執行緒在同一個ThreadLocal物件上設定了不同的值,他們仍然無法訪問到對方的值。

建立ThreadLocal物件時,我們可以指定泛型,這樣我們就不需要每次對使用get()方法返回的值作強制型別轉換了;並且我們也可以設定初始值。

如何訪問ThreadLocal變數

測試程式碼

從上述程式碼的執行結果,我們可以看出,兩個執行緒的ThreadLocal值並未互相干擾。

實現原理

2.ThreadLocal原始碼中初始化、get、set都會通過最後的getMap、createMap兩個方法獲取ThreadLocalMap物件。通過這兩個方法可以看出,最終儲存的地方實際上是上述Thread原始碼中預留的threadLocals變數,而這個變數是執行緒例項化後每個物件獨立的變數。

以上為ThreadLocal的設計理念與作用 -《 Java基礎 》一文內容,這裡是轉載作者的內容,圖片也是原作者的,這裡尷尬打上了我的水印,還請原作者見諒。

使用場景

1.實現單個執行緒單例以及單個執行緒上下文資訊儲存,比如交易id等。

2.實現執行緒安全,非執行緒安全的物件使用ThreadLocal之後就會變得執行緒安全,因為每個執行緒都會有一個對應的例項。

3.承載一些執行緒相關的資料,避免在方法中來回傳遞引數。

 

ThreadLocal會導致記憶體洩露嗎?

會。我們先看一下ThreadLocalMap類的結構及實現。

如上圖ThreadLocalMap內部是一個Entry陣列,Entry繼承自WeakReference,Entry內部的key就是ThreadLocal本身,value是ThreadLocal的set方法傳遞的值(ThreadLocal的value)。ThreadLocal作為key被傳遞到了WeakReference的建構函式裡面(super(k)),也就是說ThreadLocalMap裡面的key為ThreadLocal物件的弱引用,value為具體呼叫ThreadLocal的set方法傳遞的值。

當一個執行緒呼叫ThreadLocal的set方法設定變數時候,當前執行緒的ThreadLocalMap(Thread類的ThreadLocal.ThreadLocalMap threadLocals成員變數)裡面就會存放一個記錄,這個記錄的key為ThreadLocal的引用,value則為設定的值。如果當前執行緒一直存在而沒有呼叫ThreadLocal的remove方法,並且這時候其它地方還是有對ThreadLocal的引用,則當前執行緒的ThreadLocalMap變數裡存在的ThreadLocal變數的引用和value物件的引用是不會被釋放的,這就會造成記憶體洩露的。但是考慮如果這個ThreadLocal變數沒有了其他強引用,而當前執行緒還存在的情況下,由於執行緒的ThreadLocalMap裡面的key是弱引用,則當前執行緒的ThreadLocalMap裡面的ThreadLocal變數的弱引用會在gc的時候被回收,但是對應value還是會造成記憶體洩露,這時候ThreadLocalMap中就會出現key為null但是value不為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收(由於key為null無法訪問到value,但是Entry還引用著value,無法對value進行GC),造成記憶體洩漏。

因此導致記憶體洩漏的原因是作為key的ThreadLocal為弱引用。

其實在ThreadLocal的set和get和remove方法裡面有一些時機是會對這些key為null,value不為null的entry進行清理的,但是這些清理不是必須發生的。呼叫ThreadLocal的remove()方法會確保清理這些key為null的entry。下面看一下ThreadLocal的remove()方法:

會呼叫ThreadLocalMap的remove()方法:

e.clear()清除對ThreadLocal的弱引用

避免記憶體洩漏的方法是,必須呼叫ThreadLocal的remove()方法。

既然弱引用導致了記憶體洩漏,為什麼還使用弱引用?


我們先來看看官方文件的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了應對非常大和長時間的用途,雜湊表使用弱引用的 key。


下面我們分兩種情況討論:
        key 使用強引用:引用的ThreadLocal的物件被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal的引用不會被回收,導致記憶體洩漏(引用的物件被回收,但引用還在)。
        key 使用弱引用:引用的ThreadLocal的物件被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收(引用及引用的物件均被回收,保證ThreadLocal的引用被回收,是一個進步)。

ThreadLocal記憶體洩漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key(主動呼叫ThreadLocal的remove()方法)就會導致記憶體洩漏,而不是因為弱引用。

避免記憶體洩漏的方法是,必須呼叫ThreadLocal的remove()方法。