1,最近在做一個需求的時候需要對外部暴露一個值得應用 ,一般來說直接寫個單例,將這個成員變量的值暴露出去就ok了,但是當時突然靈機一動(現在回想是個多余的想法),想到handle源碼里面有使用過ThreadLocal這個類,想了想為什么不想直接用ThreadLocal保存數據源然后使用靜態方法暴露出去呢,結果發現使用ThreadLocal有時候會獲取不到值,查了下原因原來同事是在子線程中調用的(捂臉哭泣),所以還是要來看一波源碼,看看ThreadLocal底層實現,適用於哪些場景
2,我們現在網上搜索一下前人對於ThreadLocal這個類的一些總結
ThreadLocal特性及使用場景: 1、方便同一個線程使用某一對象,避免不必要的參數傳遞; 2、線程間數據隔離(每個線程在自己線程里使用自己的局部變量,各線程間的ThreadLocal對象互不影響); 3、獲取數據庫連接、Session、關聯ID(比如日志的uniqueID,方便串起多個日志);
從上面的總結來看,主要是用來線程間的數據隔離的,即ThreadLocal 對象可以在多個線程中共享, 但每個線程只能讀寫其中自己的數據副本。
ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>(); mBooleanThreadLocal.set(true); Boolean result = mBooleanThreadLocal.get();
主要就是這三個方法 ,那咱們就一個一個來看
2.1 構造函數
/** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and * inheritableThreadLocals). The ThreadLocal objects act as keys, * searched via threadLocalHashCode. This is a custom hash code * (useful only within ThreadLocalMaps) that eliminates collisions * in the common case where consecutively constructed ThreadLocals * are used by the same threads, while remaining well-behaved in * less common cases. */ private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647; public ThreadLocal() { }
ThreadLocal的構造方法是一個空方法 ,但是有三個參數,nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜態變量,真正變量只有 threadLocalHashCode 這一個,這三個參數都不是善茬啊。
HASH_INCREMENT 英文注釋解釋是“連續生成的哈希碼之間的差異——將隱式順序線程本地id轉換為接近最優擴散的乘法哈希值,用於大小為2的冪的表。” 這句解釋看得我們一臉蒙蔽啊,不過我記得看HashMap源碼的時候 有解決哈希沖突這一說,我們先不探究這么多,先將0x61c88647這個奇怪的值記在心里一下。
nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值。
threadLocalHashCode 見名知意 這個ThreadLocal對象的hashcode
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
調用的是nextHashCode方法 ,就是將ThreadLocal類的下一個hashCode值即nextHashCode的值賦給實例的threadLocalHashCode,然后nextHashCode的值增加HASH_INCREMENT這個值。
我們先不管這些參數的生產方式,先知道有這三個參數就行,繼續往下面看流程
2,2 set方法
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 } 9 10 ThreadLocalMap getMap(Thread t) { 11 return t.threadLocals; 12 } 13 14 void createMap(Thread t, T firstValue) { 15 t.threadLocals = new ThreadLocalMap(this, firstValue); 16 }
我們可以看到,首先通過當前調用的線程獲取到線程中對應的threadLocals變量(這里我一臉懵逼 線程中竟然使用到ThreadLocal了,趕緊去看看線程的源碼)
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }
果然Thread中擁有變量threadLocals,孤陋寡聞啊,各位老鐵,咋們繼續往下看代碼,判斷一下從線程中獲取到的ThreadLocalMap,如果不為空則,調用ThreadLocalMap類的set方法保存一下,,如果為空,則new一個ThreadLocalMap對象出來 這里涉及到了ThreadLocalMap這個類,我們來詳細的看一下這個類
2.3 ThreadLocalMap類
2.3.1 構造函數
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 } 10 11 /** 12 * The initial capacity -- MUST be a power of two. 13 */ 14 private static final int INITIAL_CAPACITY = 16; 15 16 /** 17 * The table, resized as necessary. 18 * table.length MUST always be a power of two. 19 */ 20 private Entry[] table; 21 22 /** 23 * The number of entries in the table. 24 */ 25 private int size = 0; 26 27 /** 28 * The next size value at which to resize. 29 */ 30 private int threshold; // Default to 0 31 32 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 33 table = new Entry[INITIAL_CAPACITY]; 34 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 35 table[i] = new Entry(firstKey, firstValue); 36 size = 1; 37 setThreshold(INITIAL_CAPACITY); 38 } 39 40 /** 41 * Set the resize threshold to maintain at worst a 2/3 load factor. 42 */ 43 private void setThreshold(int len) { 44 threshold = len * 2 / 3; 45 } 46 /** 47 * The next size value at which to resize. 48 */ 49 private int threshold; // Default to 0
第32-33行 : 我們看到ThreadLocalMap是ThreadLocal的匿名內部類,且構造方法也就是將該ThreadLocal實例作為key,要保持的對象作為值。變量table用來用來存放存放數據,我們可以看到table數組的初始大小是INITIAL_CAPACITY = 16 英文注釋是“初始容量——必須是2的冪。” 這里我們又碰到了“2的冪”關鍵字了,我們繼續往下看
第34行: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 這行代碼完全有些看不懂 ,為了不理解流程,我們可以先不用懂,就模糊的理解為通過ThreadLoad的threadLocalHashCode變量哈希碼來生成一個下標位置,繼續往下看
第35 - 49行:將ThreadLocal對象和value值保存到Entry對象中再保存到table數組中,設置初始的size,設置threshold閾值,這個閾值適用於擴容,當發現存入的數據大小打到了當前長度size的二分之三,就會觸發擴容,將當前table數組的大小擴充到原來的兩倍。
然后我們可以看到Entry對象中對我們的ThreadLocal參數是采用弱引用的,這點對我們后續分析ThreadLocal內存泄漏這款有所幫助,先提醒一下大家。
然后我們全面來看一下int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 這行代碼所帶來的的意思,我們再和上面有可能有聯系的一些代碼全部給粘貼在一起
private static final int HASH_INCREMENT = 0x61c88647; private static AtomicInteger nextHashCode = new AtomicInteger(); private final int threadLocalHashCode = nextHashCode(); int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16;
在看代碼之前我們先來了解一個小的知識點,看過HashMap源碼的同學一定知道“哈希沖突”這個問題
我們使用同一個哈希函數來計算不止一個的待存放的數據在表中的存放位置, 總是會有一些數據通過這個轉換函數計算出來的存放位置是相同的,這就是哈希沖突。 也就是說,不同的關鍵字通過同一哈希轉換函數計算出相同的哈希地址。
通過ThreadLocal和ThreadLocalMap的源碼可以知道,里面存值table的下標是通過ThreadLocal的哈希碼生成的,那么在ThreadLocalMap同樣的存在這個哈希沖突問題,那我們來看看ThreadLocal是怎么來解決這個問題的呢?
我們知道ThreadLocalMap的初始長度為16,每次擴容都增長為原來的2倍,即它的長度始終是2的n次方,大小必須是2的N次方呀(len = 2^N),那 len-1 的二進制表示就是低位連續的N個1,以16為例,16-1的二進制是15(十進制) = 1111(二進制),而 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位(&運算符我就不給大家進行解釋了,大家自己百度一下位運算符),而這樣做的目的是能均勻的產生哈希碼的分布,我一臉懵逼,那讓我們來看一下
public static int HASH_INCREMENT = 0x61c88647 ; public static void range(int value){ for (int i = 0; i < value; i++) { int nextHashCode = i*HASH_INCREMENT + HASH_INCREMENT; System.out.println((nextHashCode & (value - 1))+","); } } range(16); //輸出結果 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
卧槽,真的可以均勻的產生,難道0x61c88647這個數值這么神奇?下面是網上搜到的關於這個數字的節選,我反正是看不懂,還是給大家貼出來吧
①這個魔數的選取與斐波那契散列有關,0x61c88647對應的十進制為1640531527。 ②斐波那契散列的乘數可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把這個值給轉為帶符號的int,則會得到-1640531527。 換句話說 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的結果就是 1640531527也就是0x61c88647 。 ③通過理論與實踐,當我們用0x61c88647作為魔數累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結果分布很均勻。
理解了生成方式我們繼續往下看ThreadLocalMap的源碼吧
2.3.2 set函數
1 private void set(ThreadLocal<?> key, Object value) { 2 3 Entry[] tab = table; 4 int len = tab.length; 5 int i = key.threadLocalHashCode & (len-1); 6 7 for (Entry e = tab[i]; 8 e != null; 9 e = tab[i = nextIndex(i, len)]) { 10 ThreadLocal<?> k = e.get(); 11 12 if (k == key) { 13 e.value = value; 14 return; 15 } 16 17 if (k == null) { 18 replaceStaleEntry(key, value, i); 19 return; 20 } 21 } 22 23 tab[i] = new Entry(key, value); 24 int sz = ++size; 25 if (!cleanSomeSlots(i, sz) && sz >= threshold) 26 rehash(); 27 }
set方法也很簡單,就是去table中獲取第i位的數據,如果發現當前第i為的ThreadLocal等於當前傳入的ThreadLocal,就更新i位的value,如果發現第i為的ThreadLocal為空,由於我們的Entry對ThreadLocal是弱引用,就表示之前保存的ThreadLocal已經被回收了,replaceStaleEntry()方法就不和大家細看了,就是首先清理掉空的Entry,然后將后面的 Entry 進行 rehash 填補空洞,25-27行就是對閾值的判斷,如果超過了就進行擴容。
2.3.3 getEntry函數
1 private Entry getEntry(ThreadLocal<?> key) { 2 int i = key.threadLocalHashCode & (table.length - 1); 3 Entry e = table[i]; 4 if (e != null && e.get() == key) 5 return e; 6 else 7 return getEntryAfterMiss(key, i, e); 8 } 9 10 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 11 Entry[] tab = table; 12 int len = tab.length; 13 14 while (e != null) { 15 ThreadLocal<?> k = e.get(); 16 if (k == key) 17 return e; 18 if (k == null) 19 expungeStaleEntry(i); 20 else 21 i = nextIndex(i, len); 22 e = tab[i]; 23 } 24 return null; 25 }
1 private static int nextIndex(int i, int len) { 2 return ((i + 1 < len) ? i + 1 : 0); 3 }
getEntry函數也很簡單,使用位運算找到哈希槽。若哈希槽中為空或 key 不是當前 ThreadLocal 對象則會調用getEntryAfterMiss方法,可以看到getEntryAfterMiss 方法會循環查找直到找到或遍歷所有可能的哈希槽, 在循環過程中可能遇到4種情況:
①哈希槽中是當前ThreadLocal, 說明找到了目標 ②哈希槽中為其它ThreadLocal, 需要繼續查找 ③哈希槽中為null, 說明搜索結束未找到目標 ④哈希槽中存在Entry, 但是 Entry 中沒有 ThreadLocal 對象。因為 Entry 使用弱引用, 這種情況說明 ThreadLocal 被GC回收。 為了處理GC造成的空洞(stale entry), 需要調用expungeStaleEntry方法進行清理。
2.3.3 remove函數
1 private void remove(ThreadLocal<?> key) { 2 Entry[] tab = table; 3 int len = tab.length; 4 int i = key.threadLocalHashCode & (len-1); 5 for (Entry e = tab[i]; 6 e != null; 7 e = tab[i = nextIndex(i, len)]) { 8 if (e.get() == key) { 9 e.clear(); 10 expungeStaleEntry(i); 11 return; 12 } 13 } 14 }
remove方法和get方法類似,也是找出對應的Entry對象,然后調用其clean方法清理
ok,這里我們就把ThreadLocalMap的源碼看完了,其實類似ThreadLocal的源碼一樣,來我們繼續往下看
2.4 get函數
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(); } 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; } protected T initialValue() { return null; }
都很簡單,首先去獲取當前線程的ThreadLocalMap中的Entry對象,如果不為空直接返回Entry的value值,如果為空,則調用initialValue方法,這個方法可以被重寫 ,默認返回為null,將當前線程的ThreadLocal對象保存在ThreadLocalMap中,返回上層null。
2.5 remove函數
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
也很簡單,這里就不在啰嗦了 ,ok,到這里我們已經看完了ThreadLocal源碼 ,真正的功能都是在ThreadLocalMap中進行操作的,這里我有一個疑問,為什么不適用HashMap來替代ThreadLocalMap呢?如一下代碼:
class ThreadLocal { private Map values = Collections.synchronizedMap(new HashMap()); public Object get() { Thread curThread = Thread.currentThread(); Object o = values.get(curThread); if (o == null && !values.containsKey(curThread)) { o = initialValue(); values.put(curThread, o); } return o; } public void set(Object newValue) { values.put(Thread.currentThread(), newValue); } }
這樣貌似也沒問題啊,乍一看的確沒毛病,但是我們知道ThreadLocal本意是避免並發,用一個全局Map顯然違背了這一初衷,且會導致內存泄漏,用Thread當key,除非手動調用remove,否則即使線程退出了會導致:1)該Thread對象無法回收;2)該線程在所有ThreadLocal中對應的value也無法回收。
這時候會有同學提出疑問了,你使用ThreadLocalMap的話就不會導致內存泄漏嗎?
很多人認為:threadlocal里面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用以后,map里面的value卻沒有被回收.而這塊value永遠不會被訪問到了。在這種情況下我們的確會存在value的泄漏。
我們看上圖,每個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回收。
我們從之前的ThreadLocalMap源碼可以知道,弱引用只存在於key上,所以key會被回收,但當線程還在運行的情況下,value還是在被線程強引用而無法釋放,只有當線程結束之后或者我們調用set、get方法的時候回去移除已被回收的Entry( replaceStaleEntry這個方法),給出的建議是,當ThreadLocal使用完成的時候,調用remove方法將value移除掉,這樣就不會存在內存泄漏了。
3,總結
我們代碼看完了,需要對ThreadLocal的使用場景進行總結一下 :
ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。
ok,感覺好久都沒有些博客了,思路和語言表達都有些生疏,爭取后面一直堅持寫一寫,加油