ThreadLocal在不少地方被用到,下面來看一下具體ThreadLocal是如何實現的。首先看ThreadLocal的註釋:html
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID). For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls. Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
翻譯過來意思是:該類提供線程局部變量.。這些變量與它們的正常對應變量不一樣,由於每一個訪問一個線程這些變量在多線程環境下訪問(經過get或set方法訪問)時能保證各個線程裏的變量相對獨立於其餘線程內的變量,都有它本身的、獨立初始化的變量副本。ThreadLocal實例一般是類中但願將狀態與線程相關聯的私有靜態字段(例如,用戶ID或事務ID)。java
每一個線程都包含對線程局部變量副本的隱式引用,只要線程是活動的,而且線程本地實例是可訪問的;線程消失後,線程本地實例的全部副本都會受到垃圾收集(除非存在對這些副本的其餘引用)。數組
例子如JDK代碼註釋所示。多線程
import java.util.concurrent.atomic.AtomicInteger; public class ThreadId { // Atomic integer containing the next thread ID to be assigned private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return nextId.getAndIncrement(); } }; // Returns the current thread's unique ID, assigning it if necessary public static int get() { return threadId.get(); } }
ThreadLocal能夠看作是一個容器,容器裏面存放着屬於當前線程的變量。ThreadLocal類提供了四個對外開放的接口方法,這也是用戶操做ThreadLocal類的基本方法: less
//該方法返回當前線程所對應的線程局部變量 public T get() {} //設置當前線程的線程局部變量的值 public void set(T value) {} //將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。 public void remove() {} //返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,而且僅執行1次,ThreadLocal中的缺省實現直接返回一個null。 private T setInitialValue() {}
(1)get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本ide
(2)set()用來設置當前線程中變量的副本,ui
(3)remove()用來移除當前線程中變量的副本,this
(4)initialValue()是一個protected方法,通常是用來在使用時進行重寫的,它是一個延遲加載方法。atom
先看下get方法的具體實現:spa
/** * 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(); }
第一句是取得當前線程,而後經過getMap(t)方法獲取到一個map,以下:
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
map的類型爲ThreadLocalMap。在getMap中,是調用當前線程t,返回當前線程t中的一個成員變量threadLocals。查看Thread類能夠看到:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
會發現Thread中有ThreadLocal.ThreadLocalMap類型的成員變量threadLocals
而後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。
若是獲取成功,則返回value值。
若是map爲空,則調用setInitialValue方法返回value。
咱們上面的ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,咱們繼續取看ThreadLocalMap的實現:
/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */ static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ //map中的每一個節點Entry,其鍵key是ThreadLocal而且仍是弱引用,這也致使了後續會產生內存泄漏問題的緣由。 static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. * 真正用於存儲線程的每一個ThreadLocal的數組,將ThreadLocal和其對應的值包裝爲一個Entry。 */ */ private Entry[] table;
能夠看到ThreadLocalMap的Entry繼承了WeakReference,而且使用ThreadLocal做爲鍵值。
而後再繼續看setInitialValue方法的具體實現:
/** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
很容易瞭解,就是若是map不爲空,就設置鍵值對,爲空,再建立Map,看一下createMap的實現:
/** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map * @param map the map to store. */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
至此,咱們明白了ThreadLocal是如何爲每一個線程建立變量的副本的:
首先,在每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。
初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。
而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找。
下面上一個簡單的例子:
package com.jason.threadlocal; public class Context { private String traceId; private String ordNo; public String getTraceId() { return traceId; } public void setTraceId(String traceId) { this.traceId = traceId; } public String getOrdNo() { return ordNo; } public void setOrdNo(String ordNo) { this.ordNo = ordNo; } @Override public String toString() { return "Context{" + "traceId='" + traceId + '\'' + ", ordNo='" + ordNo + '\'' + '}'; } }
package com.jason.threadlocal; public class TestThreadLocal { private static final ThreadLocal<Context> contextThreadLocal = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { Context context = new Context(); context.setTraceId(Thread.currentThread().getName()); context.setOrdNo(Thread.currentThread().getId() + ""); contextThreadLocal.set(context); System.out.println(context); Thread thread1 = new Thread(new Runnable() { @Override public void run() { Context context = new Context(); context.setTraceId(Thread.currentThread().getName()); context.setOrdNo(Thread.currentThread().getId() + ""); contextThreadLocal.set(context); System.out.println(context); } }); thread1.start(); //等待thread1終止 thread1.join(); System.out.println(context); } }
會打印結果以下:
Context{traceId='main', ordNo='1'}
Context{traceId='Thread-0', ordNo='9'}
Context{traceId='main', ordNo='1'}
從上面的例子能夠看到main線程和thread1線程在contextThreadLocal各自保存的值是不同的,之因此後面又打印一次main線程的值是爲了證實共享變量contextThreadLocal中保存的確實是main線程本身的那一份副本。
綜上所述,能夠得出
(1)ThreadLocal建立的副本其實是經過每一個線程的threadLocals中保存:
(2)爲什麼threadLocals的類型ThreadLocalMap的鍵值爲ThreadLocal對象,由於每一個線程中可有多個threadLocal變量。
ThreadLocal
ThreadLocal
的實現是這樣的:每一個Thread
維護一個 ThreadLocalMap
映射表,這個映射表的 key
是 ThreadLocal
實例自己,value
是真正須要存儲的 Object
。
也就是說 ThreadLocal
自己並不存儲值,它只是做爲一個 key
來讓線程從 ThreadLocalMap
獲取 value
。值得注意的是圖中的虛線,表示 ThreadLocalMap
是使用 ThreadLocal
的弱引用做爲 Key
的,弱引用的對象在 GC 時會被回收。
ThreadLocal
爲何會內存泄漏ThreadLocalMap
使用ThreadLocal
的弱引用做爲key
,若是一個ThreadLocal
沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal
勢必會被回收,這樣一來,ThreadLocalMap
中就會出現key
爲null
的Entry
,就沒有辦法訪問這些key
爲null
的Entry
的value
,若是當前線程再遲遲不結束的話,這些key
爲null
的Entry
的value
就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠沒法回收,形成內存泄漏。
其實,ThreadLocalMap
的設計中已經考慮到這種狀況,也加上了一些防禦措施:在ThreadLocal
的get()
,set()
,remove()
的時候都會清除線程ThreadLocalMap
裏全部key
爲null
的value
。
可是這些被動的預防措施並不能保證不會內存泄漏:
static
的ThreadLocal
,延長了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。
下面咱們分兩種狀況討論:
ThreadLocal
的對象被回收了,可是ThreadLocalMap
還持有ThreadLocal
的強引用,若是沒有手動刪除,ThreadLocal
不會被回收,致使Entry
內存泄漏。ThreadLocal
的對象被回收了,因爲ThreadLocalMap
持有ThreadLocal
的弱引用,即便沒有手動刪除,ThreadLocal
也會被回收。value
在下一次ThreadLocalMap
調用set
,get
,remove
的時候會被清除。比較兩種狀況,咱們能夠發現:因爲ThreadLocalMap
的生命週期跟Thread
同樣長,若是都沒有手動刪除對應key
,都會致使內存泄漏,可是使用弱引用能夠多一層保障:弱引用ThreadLocal
不會內存泄漏,對應的value
在下一次ThreadLocalMap
調用set
,get
,remove
的時候會被清除。
所以,ThreadLocal
內存泄漏的根源是:因爲ThreadLocalMap
的生命週期跟Thread
同樣長,若是沒有手動刪除對應key
就會致使內存泄漏,而不是由於弱引用。
綜合上面的分析,咱們能夠理解ThreadLocal
內存泄漏的來龍去脈,那麼怎麼避免內存泄漏呢?
ThreadLocal
,都調用它的remove()
方法,清除數據。在使用線程池的狀況下,沒有及時清理ThreadLocal
,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal
就跟加鎖完要解鎖同樣,用完就清理。
參考文獻:
https://www.cnblogs.com/dolphin0520/p/3920407.html
http://blog.csdn.net/lhqj1992/article/details/52451136
深刻分析ThreadLocal內存泄露問題:
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/