Java多線程:ThreadLocal


一、ThreadLocal基礎知識

  ThreadLocal是線程的一個本地化對象,或者說是局部變量。當工作於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程分配一個獨立的變量副本。所以每一個線程都可以獨立地改變自己的副本,而不會影響其他線程所對應的副本。

  ThreadLocal不是用來解決對象共享訪問問題的,而主要是提供了線程保持對象的方法和避免參數傳遞的方便的對象訪問方式 

  ThreadLocal的應用場合,最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。

   概括起來說,對於多線程資源共享的問題,同步機制synchronized采用了“以時間換空間”的方式,比如定義一個static變量,同步訪問,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響

 

二:如何存放線程本地變量?

       在ThreadLocal類中有一個ThreadLocalMap, 用於存放每一個線程的變量副本,Map中元素的key為線程對象,value為對應線程的變量副本。

    另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,並不是通過ThreadLocal.set()來實現的,而是通過每個線程中的new 對象 的操作來創建的對象,每個線程創建一個,不是什么對象的拷貝或副本。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。 

  如果ThreadLocal.set()進去的東西本來就是多個線程共享的同一個對象,那么多個線程的ThreadLocal.get()取得的還是這個共享對象本身,還是有並發訪問問題。

 

歸納了兩點: 
    1。每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。 
    2。將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然后在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
 

 
    當然如果要把本來線程共享的對象通過ThreadLocal.set()放到線程中也可以,可以實現避免參數傳遞的訪問方式,但是要注意get()到的是那同一個共享對象,並發訪問問題要靠其他手段來解決。但一般來說線程共享的對象通過設置為某類的靜態變量就可以實現方便的訪問了,似乎沒必要放到線程中。 

    ThreadLocal的應用場合,我覺得最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。

三、源碼解讀

   很多人對ThreadLocal存在一定的誤解,說ThreadLocal中有一個全局的Map,set時執行map.put(Thread.currentThread(), value),get和remove時也同理

首先看一下ThreadLocal的API:

  • get():返回此線程局部變量的當前線程副本中的值。
  • protected T initialValue(): 返回此線程局部變量的當前線程的“初始值”。
  • void remove(): 移除此線程局部變量當前線程的值。
  • void set(T value): 將此線程局部變量的當前線程副本中的值設置為指定值。 

   set方法:

/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */  
public void set(T value) { // 獲取當前線程對象 
    Thread t = Thread.currentThread(); // 獲取當前線程本地變量Map 
    ThreadLocalMap map = getMap(t); // map不為空 
    if (map != null) // 存值 
        map.set(this, value); else  
        // 創建一個當前線程本地變量Map 
 createMap(t, value); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { // 獲取當前線程的本地變量Map 
    return t.threadLocals; } 

 這里注意,ThreadLocal中是有一個Map,但這個Map不是我們平時使用的Map,而是ThreadLocalMap,ThreadLocalMap是ThreadLocal的一個內部類,不對外使用的。當使用ThreadLocal存值時,首先是獲取到當前線程對象,然后獲取到當前線程本地變量Map,最后將當前使用的ThreadLocal和傳入的值放到Map中,也就是說ThreadLocalMap中存的值是[ThreadLocal對象, 存放的值],這樣做的好處是,每個線程都對應一個本地變量的Map,所以一個線程可以存在多個線程本地變量。

 


  get方法:

/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } // 如果值為空,則返回初始值  return setInitialValue(); }  

看了之前set方法的分析,get方法也同理,需要說明的是,如果沒有進行過set操作,那從ThreadLocalMap中拿到的值就是null

 四、使用場景

  ThreadLocal對象通常用於防止對可變的單實例變量或全局變量進行共享

  當一個類中使用了static成員變量的時候,一定要多問問自己,這個static成員變量需要考慮線程安全嗎?也就是說,多個線程需要獨享自己的static成員變量嗎?如果需要考慮,不妨使用ThreadLocal。

  ThreadLocal的主要應用場景為多線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。例如:同一個網站登錄用戶,每個用戶服務器會為其開一個線程,每個線程中創建一個ThreadLocal,里面存用戶基本信息等,在很多頁面跳轉時,會顯示用戶信息或者得到用戶的一些信息等頻繁操作,這樣多線程之間並沒有聯系而且當前線程也可以及時獲取想要的數據。

  

五、注意事項

  

  ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現keynullEntry,就沒有辦法訪問這些keynullEntryvalue,如果當前線程再遲遲不結束的話,這些keynullEntryvalue就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。

其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocalget(),set(),remove()的時候都會清除線程ThreadLocalMap里所有keynullvalue

但是這些被動的預防措施並不能保證不會內存泄漏:

  • 使用staticThreadLocal,延長了ThreadLocal的生命周期,可能導致的內存泄漏(參考ThreadLocal 內存泄露的實例分析)。
  • 分配使用了ThreadLocal又不再調用get(),set(),remove()方法,那么就會導致內存泄漏。

為什么使用弱引用

從表面上看內存泄漏的根源在於使用了弱引用。網上的文章大多着重分析ThreadLocal使用了弱引用會導致內存泄漏,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強引用?

我們先來看看官方文檔的說法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了應對非常大和長時間的用途,哈希表使用弱引用的 key。

下面我們分兩種情況討論:

  • key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
  • key 使用弱引用:引用的ThreadLocal的對象被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,getremove的時候會被清除。

比較兩種情況,我們可以發現:由於ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除

因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。

 

綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因后果,那么怎么避免內存泄漏呢? 

   ①  使用結束以后進行remove操作,避免ThreadLocal對象越來越大。
  ②  高並發的場景:由於ThreadLocal內部使用HashMap的原理,key=currentThread,因為HashMap是非線程安全的,一定要注意hashmap.resize的時候,可能會導致某幾個CPU 100%的問題,進而導致應用出現資源耗盡等不可預知的問題。

在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

 

參考資料:

    ThreadLocal


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM