java 並發(七)--- ThreadLocal
文章部分圖片來自參考資料
ThreadLocal 概述
ThreadLocal 線程本地變量 ,是一個工具,可以讓多個線程保持一個變量的副本,那麽每個線程可以訪問自己內部的副本變量。
ReentranReadWriteLock中。
ThreadLocal 結構圖裏面看到有兩個內部類,一個 SuppliedThreadLocal , 一個ThreadLocalMap 。下面用一張圖來說明線程使用的示意圖。可以看到每個Thread有個 ThreadLocalMap ,然後裏面由hash值分列的的數組 Entry[] 。Entry 數據結構就是圖中淡綠色框內所示。
ThreadLocal 源碼分析
ThreadLocal 下文簡稱 TL, TL最常見的方法就是 get 和 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}
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9return result; 10 } 11 } 12 return setInitialValue(); 13 }
1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 }
1 ThreadLocal.ThreadLocalMap threadLocals = null;
可以看到thread 內部中持有TL的內部類變量。我們來看一下 ThreadLocalMap, threadLocalMap 內部定義一個類,Entry 類。這是threadLocalMap 內的變量
1 static class ThreadLocalMap { 2 /** 3 * The initial capacity -- MUST be a power of two. 4 */ 5 private static final int INITIAL_CAPACITY = 16; 6 7 /** 8 * The table, resized as necessary. 9 * table.length MUST always be a power of two. 10 */ 11 private Entry[] table; 12 13 /** 14 * The number of entries in the table. 15 */ 16 private int size = 0; 17 18 /** 19 * The next size value at which to resize. 20 */ 21 private int threshold; // Default to 0 22 } 23
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 }
我們看到 TL 的set 方法實際就是調用了 ThreadLocalMap 的set 方法。
1 private void set(ThreadLocal<?> key, Object value) { 2 3 // We don‘t use a fast path as with get() because it is at 4 // least as common to use set() to create new entries as 5 // it is to replace existing ones, in which case, a fast 6 // path would fail more often than not. 7 8 Entry[] tab = table; 9 int len = tab.length; 10 int i = key.threadLocalHashCode & (len-1); 11 12 for (Entry e = tab[i]; 13 e != null; 14 e = tab[i = nextIndex(i, len)]) { 15 ThreadLocal<?> k = e.get(); 16 17 //找到相同的 key 18 if (k == key) { 19 e.value = value; 20 return; 21 } 22 23 //某個key失效 24 if (k == null) { 25 replaceStaleEntry(key, value, i); 26 return; 27 } 28 } 29 30 //走到這裏必定是退出了循環,即是遇到空的 entry ,直接放在空的地方,檢查是否需要擴容,重新 hash 31 tab[i] = new Entry(key, value); 32 int sz = ++size; 33 if (!cleanSomeSlots(i, sz) && sz >= threshold) 34 rehash(); 35 } 36 37 38 // 這個方法是替代某些失效的entry ,最終的值會放在 table[staleSlot] 39 // slotToExpunge 這個變量從名字上可以看出就是需要擦洗的 slot (指的是某個位置) 40 private void replaceStaleEntry(ThreadLocal<?> key, Object value, 41 int staleSlot) { 42 Entry[] tab = table; 43 int len = tab.length; 44 Entry e; 45 46 // Back up to check for prior stale entry in current run. 47 // We clean out whole runs at a time to avoid continual 48 // incremental rehashing due to garbage collector freeing 49 // up refs in bunches (i.e., whenever the collector runs). 50 // 向前找是否有失效節點,如果有做一下標記,即是為 slotToExpunge 賦值 51 int slotToExpunge = staleSlot; 52 for (int i = prevIndex(staleSlot, len); 53 (e = tab[i]) != null; 54 i = prevIndex(i, len)) 55 if (e.get() == null) 56 slotToExpunge = i; 57 58 // Find either the key or trailing null slot of run, whichever 59 // occurs first 60 // 向後尋找是否有相同的 key 61 for (int i = nextIndex(staleSlot, len); 62 (e = tab[i]) != null; 63 i = nextIndex(i, len)) { 64 ThreadLocal<?> k = e.get(); 65 66 // If we find key, then we need to swap it 67 // with the stale entry to maintain hash table order. 68 // The newly stale slot, or any other stale slot 69 // encountered above it, can then be sent to expungeStaleEntry 70 // to remove or rehash all of the other entries in run. 71 // 找到相同的值,交換位置到 tab[staleSlot] 72 if (k == key) { 73 e.value = value; 74 75 tab[i] = tab[staleSlot]; 76 tab[staleSlot] = e; 77 78 // Start expunge at preceding stale entry if it exists 79 // 擦洗失效值 80 if (slotToExpunge == staleSlot) 81 slotToExpunge = i; 82 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 83 return; 84 } 85 86 // If we didn‘t find stale entry on backward scan, the 87 // first stale entry seen while scanning for key is the 88 // first still present in the run. 89 if (k == null && slotToExpunge == staleSlot) 90 slotToExpunge = i; 91 } 92 93 // If key not found, put new entry in stale slot 94 //找不到值會放在 tab[staleSlot] ,即原來失效值的位置上 95 tab[staleSlot].value = null; 96 tab[staleSlot] = new Entry(key, value); 97 98 // If there are any other stale entries in run, expunge them 99 // 擦洗失效值 100 if (slotToExpunge != staleSlot) 101 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 102 } 103
可以看到我們在 set 的時候,TL內會檢查是否存在失效值。也可以看到 ThreadLocalMap 的Hash 中解決沖突的方式只是簡單的向下尋找空的位置,即線性探測,這樣的效率比較低,所以建議 :
每個線程只存一個變量,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal,如果一個線程要保存多個變量,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash沖突的可能。
下面看一下 get 方法,不難。
1 // ThreadLocalMap 2 private Entry getEntry(ThreadLocal<?> key) { 3 int i = key.threadLocalHashCode & (table.length - 1); 4 Entry e = table[i]; 5 if (e != null && e.get() == key) 6 return e; 7 else 8 return getEntryAfterMiss(key, i, e); 9 }
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 //獲取的時候出現失效的entry 8 return getEntryAfterMiss(key, i, e); 9 } 10 11 12 // 往後找,失效的值擦洗掉,沒有就返回 Null 13 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 14 Entry[] tab = table; 15 int len = tab.length; 16 17 while (e != null) { 18 ThreadLocal<?> k = e.get(); 19 if (k == key) 20 return e; 21 if (k == null) 22 expungeStaleEntry(i); 23 else 24 i = nextIndex(i, len); 25 e = tab[i]; 26 } 27 return null; 28 }
ThreadLocalMap 的 key 失效
ThreadLocalMap下文簡稱 TLM 。
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 }
可以看到 Entry 繼承 WeakReference (弱引用)。ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那麽這個Entry對象中的value就有可能一直得不到回收,於是這就存在一條強引用鏈的關系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。
我們從源碼中也可以看到在 get 和 set 等方法都有檢查失效值的操作,同時當我們使用TL時,某個線程不再需要某個值的時候應該調用 remove 方法,下面代碼中 e.clear() 這一句實際是調用了弱引用的 clear 方法,實現對對象的回收。
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 }
1 /** 2 * Clears this reference object. Invoking this method will not cause this 3 * object to be enqueued. 4 * 5 * <p> This method is invoked only by Java code; when the garbage collector 6 * clears references it does so directly, without invoking this method. 7 */ 8 public void clear() { 9 this.referent = null; 10 }
我們來看一下weakReference 表示弱引用,java中有四種引用類型,強引用,弱引用,軟引用,虛引用。
在Java語言中, 當一個對象o被創建時, 它被放在Heap裏. 當GC運行的時候, 如果發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須滿足兩個條件:
-
沒有任何引用指向它
-
GC被運行.
1 DemoA a=new DemoA(); 2 DemoB b=new DemoB(a);
假如有下面代碼,如果我們增加一行代碼來將a對象的引用設置為null,當一個對象不再被其他對象引用的時候,是會被GC回收的,但是對於這個場景來說,即時是a=null,也不可能被回收,因為DemoB依賴DemoA,這個時候是可能造成內存泄漏的。
1 DemoA a=new DemoA(); 2 DemoB b=new DemoB(a); 3 a=null;
通過弱引用,有兩個方法可以避免這樣的問題。
1 //方法1 2 DemoA a=new DemoA(); 3 DemoB b=new DemoB(a); 4 a=null; 5 b=null; 6 //方法2 7 DemoA a=new DemoA(); 8 WeakReference b=new WeakReference(a); 9 a=null; 10
對於方法2來說,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然後把這個弱可達對象標記為可終結(finalizable)的,這樣它隨後就會被回收。
我們可以設想b就是ThreadLocal ,試想一下如果這裏沒有使用弱引用,意味著ThreadLocal的生命周期和線程是強綁定,只要線程沒有銷毀,那麽ThreadLocal一直無法回收。而使用弱引用以後,當ThreadLocal被回收時,由於Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap本身的垃圾清理會用到這一個好處,方便對無效的Entry進行回收。
其實我們從源碼分析可以看到,ThreadLocalMap是做了防護措施的
首先從ThreadLocal的直接索引位置(通過
ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不為null並且key相同則返回e
如果e為null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續向下一個位置查詢
Entry 的 Hash 值
如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢?
1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap(this, firstValue); 3 }
1 static class ThreadLocalMap { 2 static class Entry extends WeakReference<ThreadLocal<?>> { 3 4 /** The value associated with this ThreadLocal. */ 5 Object value; 6 7 Entry(ThreadLocal<?> k, Object v) { 8 super(k); 9 value = v; 10 } 11 } 12 13 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { 14 //構造一個Entry數組,並設置初始大小 15 table = new Entry[INITIAL_CAPACITY]; 16 //計算Entry數據下標 17 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 18 //將`firstValue`存入到指定的table下標中 19 table[i] = new Entry(firstKey, firstValue); 20 size = 1;//設置節點長度為1 21 setThreshold(INITIAL_CAPACITY); //設置擴容的閾值 22 } 23 //...省略部分代碼 24 } 25 26
1 private final int threadLocalHashCode = nextHashCode(); 2 private static AtomicInteger nextHashCode = new AtomicInteger(); 3 private static final int HASH_INCREMENT = 0x61c88647; 4 5 private static int nextHashCode() { 6 return nextHashCode.getAndAdd(HASH_INCREMENT); 7 }
那為什麽要使用到 0x61c88647 這個值呢? 我們首先要明白一點,散列的目的是使數據分布更加均勻。那麽這個數字的使用必定會達到這個目的。
魔數0x61c88647的選取和斐波那契散列有關,0x61c88647對應的十進制為1640531527。而斐波那契散列的乘數可以用 (long)((1L<<31)*(Math.sqrt(5)-1)); 如果把這個值給轉為帶符號的int,則會得到-1640531527。也就是說(long)((1L<<31)*(Math.sqrt(5)-1));得到的結果就是1640531527,也就是魔數0x61c88647
建議
-
將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命周期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露
-
每次使用完ThreadLocal,都調用它的remove()方法,清除數據。
參考資料 :
- ThreadLocal-面試必問深度解析
- JAVA高級架構 微信公眾號的 “ThreadLocal的使用及原理分析”
- 從 ThreadLocal 的實現看散列算法
java 並發(七)--- ThreadLocal