咱們知道在Java併發編程中咱們通常會使用synchronized關鍵字或者CAS操做類來進行共享資源的同步。ThreadLocal類爲併發編程提供了另一種思路,它將共享資源做爲每個線程的副本,這樣在某些場景下,咱們就能夠無需同步完成併發程序的設計與開發,Andoid中Looper類的實現裏面就使用到了ThreadLocal。
ThreadLocal本質是本地線程副本工具類,將線程與線程私有變量作一個映射,各個線程之間的變量互不干擾。 java
從上面的類全覽圖咱們能夠看到,ThreadLocal類提供的主要方法有set(T)、get()、remove()、initialValue()等,下面咱們就從這些經常使用的方法開始分析。編程
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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
複製代碼
從源碼中咱們看到第一行就是獲取當前線程對象,而後調用getMap獲取ThreadLocalMap對象,咱們繼續往下跟,看getMap是什麼邏輯。bash
/**
* 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;
}
複製代碼
咱們看到這個方法直接返回的就是線程對象的threadLocals成員變量,咱們繼續往下跟,在Thread.java文件中有以下定義併發
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼
通過上面的分析,咱們能夠梳理出get()方法的相關操做步驟:
less
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
複製代碼
/**
* 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中。ide
/**
* 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();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製代碼
set()方法的邏輯以下:
函數
/**
* 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
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼
該方法是用來設置默認值的,即map中找不到數據時返回的默認值,咱們在初始化ThreadLocal的時候能夠自定義這個默認值,源碼以下:工具
/**
* Returns the current thread's "initial value" for this * thread-local variable. This method will be invoked the first * time a thread accesses the variable with the {@link #get} * method, unless the thread previously invoked the {@link #set} * method, in which case the {@code initialValue} method will not * be invoked for the thread. Normally, this method is invoked at * most once per thread, but it may be invoked again in case of * subsequent invocations of {@link #remove} followed by {@link #get}. * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; } 複製代碼
咱們能夠像下面這樣,自定義默認值:oop
ThreadLocal<String> threadLocal=new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "default";
}
};
複製代碼
/**
* Removes the current thread's value for this thread-local * variable. If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim. This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } 複製代碼
remove()方法比較簡單,就是移除map中的副本。
ThreadLocalMap類圖:
在ThreadLocalMap中,是使用Entry結構來存儲K-V數據的,key是ThreadLocal對象,這個在構造函數中已經指定了,源碼以下:/**
* 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. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 複製代碼
Entry繼承WeakReference(弱引用,生命週期只能存活到下次GC前),但只有Key是弱引用類型的,Value並不是弱引用。
static class ThreadLocalMap {
/**
* 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.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
複製代碼
從源碼作咱們看到,ThreadLocalMap類結構很簡單,主要結構是一個Entry[]數組,來存放每個線程存放的變量副本。Entry結構和HashMap的Node結構仍是有很大不一樣的,Node結構須要存儲next節點,發生衝突後會造成鏈表結構,而Entry結構只是單純的K-V結構。那麼若是發生衝突如何解決?咱們去set方法裏面尋找答案。
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not.'
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製代碼
從set()方法中咱們能夠看到,先根據key的hashCode來肯定元素在table列表中的位置,而後判斷是否存在數據,若是存在數據就更新操做。而後e = tab[i = nextIndex(i, len)])這一句很關鍵,咱們看到會調用nextIndex方法獲取下一個節點。源碼以下:
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
複製代碼
這裏使用的是固定步長來查找下一個能夠存放的位置。如今咱們能夠總結ThreadLocalMap解決Hash衝突時經過固定步長,查找上一個或者下一個位置來存放Entry數據。
若是有大量的ThreadLocal對象存入ThreadLocalMap對象中,會致使衝突,甚至是二次衝突,這就必定程度上下降了效率。那咱們如何解決呢? 良好建議:
每一個線程只存一個變量,這樣全部的線程存放到map中的Key都是相同的ThreadLocal,若是一個線程要保存多個變量,就須要建立多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增長Hash衝突的可能。若是須要存儲多份數據,可使用包裝類進行包裝下。
不少文章都會提到不正確的使用ThreadLocal會致使內存泄漏的發生,這一節咱們來好好研究下內存泄漏時如何發生的。 咱們來看一下在使用ThreadLocal對象時,對象的內存分佈:
ThreadLocal的原理:每一個Thread內部維護着一個ThreadLocalMap,它是一個Map。這個映射表的Key是一個弱引用,其實就是ThreadLocal自己,Value是真正存的線程變量Object。也就是說ThreadLocal自己並不真正存儲線程的變量值,它只是一個工具,用來維護Thread內部的Map,幫助存和取。注意上圖的虛線,它表明一個弱引用類型,而弱引用的生命週期只能存活到下次GC前。
ThreadLocal對象在ThreadLocalMap對象中是使用一個弱引用進行被Entry中的Key進行引用的,所以若是ThreadLocal對象沒有外部強引用來引用它,那麼ThreadLocal對象會在下次GC的時候被回收(注意:若是ThreadLocal對象還有強引用引用,GC事後,WeakReference仍是不會被回收的)。這時候Entry結構中就會出現Null Key的狀況。外部讀取ThreadLocal是沒法 使用Null Key來找到Value的。所以若是當前線程執行時間足夠長的話,就會造成一條強引用的鏈。Thread-->ThreadLocalMap對象-->Entry(Key爲Null,Value還引用其餘對象)-->Object對象。這就致使了Entry對象不會被回收,固然Object對象也不會回收。
ThradLocal類的設計者也考慮到這種狀況了,因此在ThreadLocal類的相關操做方法中,例如:get()、set()、remove()等方法中都會尋找Key爲Null的Entry節點,將Entry的Key和Value結構都設置爲Null,利於GC回收。下面咱們就來看相關方法。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code * @param e the entry at table[i] * @return the entry associated with key, or null if no such'
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
複製代碼
從上面的源碼咱們看到,在get方法中會調用getEntry()方法,getEntry()方法內部會根據entry對象和key是不是Null執行getEntryAfterMiss()方法。咱們在getEntryAfterMiss()方法內部能夠看到k==null時執行的是expungeStaleEntry方法(即刪除、擦除)。咱們繼續看這個方法。
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
複製代碼
從源碼中咱們看到,把當前的Entry刪除後,會繼續循環往下尋找是否存在Key爲Null的節點,若是存在的話也刪除,防止內存泄漏發生。
有不少人以爲之因此發生內存泄漏是由於ThreadLocal中Entry結構Key是弱引用致使的。實際上是由於Entry中Key爲Null以後,沒有主動清除Value所致使的。那Entry結構爲何使用弱引用呢?官方的註釋是這樣的:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
爲了處理很是大和運行週期很是長的線程。哈希表使用弱引用。
複製代碼
下面咱們分兩種狀況來看:
每次使用完ThreadLocal,都調用它的remove()方法,清除數據。 在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。