微信公眾號:I am CR7
如有問題或建議,請在下方留言;
最近更新:2019-01-12
前言
繼上一篇文章《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》中提到的RequestContext使用的兩大神器之一:ThreadLocal,本文特此進行深入分析,為大家掃清知識障礙。
Hello World
在展開深入分析之前,我們們先來看一個官方示例:
出處來源於ThreadLocal類上的註釋,其中main方法是筆者加上的。
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class ThreadId {
4 // Atomic integer containing the next thread ID to be assigned
5 private static final AtomicInteger nextId = new AtomicInteger(0);
6
7 // Thread local variable containing each thread`s ID
8 private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
9 @Override
10 protected Integer initialValue() {
11 return nextId.getAndIncrement();
12 }
13 };
14
15 // Returns the current thread`s unique ID, assigning it if necessary
16 public static int get() {
17 return threadId.get();
18 }
19
20 public static void main(String[] args) {
21 for (int i = 0; i < 5; i++) {
22 new Thread(new Runnable() {
23 @Override
24 public void run() {
25 System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26 }
27 }).start();
28 }
29 }
30}
複製程式碼
執行結果如下:
1threadName=Thread-0,threadId=0
2threadName=Thread-1,threadId=1
3threadName=Thread-2,threadId=2
4threadName=Thread-3,threadId=3
5threadName=Thread-4,threadId=4
複製程式碼
我問:看完這個例子,您知道ThreadLocal是幹什麼的了嗎?
您答:不知道,沒感覺,一個hello world的例子,完全激發不了我的興趣。
您問:那個誰,你敢不敢舉一個生產級的、工作中真實能用的例子?
我答:得,您是”爺”,您說啥我就做啥。還記得《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》中提到的RequestContext嗎?這就是一個生產級的運用啊。Zuul核心原理是什麼?就是將請求放入過濾器鏈中經過一個個過濾器的處理,過濾器之間沒有直接的呼叫關係,處理的結果都是存放在RequestContext裡傳遞的,而這個RequestContext就是一個ThreadLocal型別的物件啊!!!
1public class RequestContext extends ConcurrentHashMap<String, Object> {
2
3 protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
4 @Override
5 protected RequestContext initialValue() {
6 try {
7 return contextClass.newInstance();
8 } catch (Throwable e) {
9 throw new RuntimeException(e);
10 }
11 }
12 };
13
14 public static RequestContext getCurrentContext() {
15 if (testContext != null) return testContext;
16
17 RequestContext context = threadLocal.get();
18 return context;
19 }
20}
複製程式碼
以Zuul中前置過濾器DebugFilter為例:
1public class DebugFilter extends ZuulFilter {
2
3 @Override
4 public Object run() {
5 // 獲取ThreadLocal物件RequestContext
6 RequestContext ctx = RequestContext.getCurrentContext();
7 // 它是一個map,可以放入資料,給後面的過濾器使用
8 ctx.setDebugRouting(true);
9 ctx.setDebugRequest(true);
10 return null;
11 }
12
13}
複製程式碼
您問:那說了半天,它到底是什麼,有什麼用,能不能給個概念?
我答:能!必須能!!!
What is this
它是啥?它是一個支援泛型的java類啊,拋開裡面的靜態內部類ThreadLocalMap不說,其實它沒幾行程式碼,不信,您自己去看看。它用來幹啥?類上註釋說的很明白:
- 它能讓執行緒擁有了自己內部獨享的變數
- 每一個執行緒可以通過get、set方法去進行操作
- 可以覆蓋initialValue方法指定執行緒獨享的值
- 通常會用來修飾類裡private static final的屬性,為執行緒設定一些狀態資訊,例如user ID或者Transaction ID
- 每一個執行緒都有一個指向threadLocal例項的弱引用,只要執行緒一直存活或者該threadLocal例項能被訪問到,都不會被垃圾回收清理掉
愛提問的您,一定會有疑惑,demo裡只是呼叫了ThreadLocal.get()方法,它如何實現這偉大的一切呢?這就是筆者下面要講的內容,走著~~~
我有我的map
話不多說,我們來看get方法內部實現:
get()原始碼
1public 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;
9 return result;
10 }
11 }
12 return setInitialValue();
13}
複製程式碼
邏輯很簡單:
- 獲取當前執行緒內部的ThreadLocalMap
- map存在則獲取當前ThreadLocal對應的value值
- map不存在或者找不到value值,則呼叫setInitialValue,進行初始化
setInitialValue()原始碼
1private T setInitialValue() {
2 T value = initialValue();
3 Thread t = Thread.currentThread();
4 ThreadLocalMap map = getMap(t);
5 if (map != null)
6 map.set(this, value);
7 else
8 createMap(t, value);
9 return value;
10}
複製程式碼
邏輯也很簡單:
- 呼叫initialValue方法,獲取初始化值【呼叫者通過覆蓋該方法,設定自己的初始化值】
- 獲取當前執行緒內部的ThreadLocalMap
- map存在則把當前ThreadLocal和value新增到map中
- map不存在則建立一個ThreadLocalMap,儲存到當前執行緒內部
時序圖
為了便於理解,筆者特地畫了一個時序圖,請看:
小結
至此,您能回答ThreadLocal的實現原理了嗎?沒錯,map,一個叫做ThreadLocalMap的map,這是關鍵。每一個執行緒都有一個私有變數,是ThreadLocalMap型別。當為執行緒新增ThreadLocal物件時,就是儲存到這個map中,所以執行緒與執行緒間不會互相干擾。總結起來,一句話:我有我的young,哦,不對,是我有我的map。弄清楚了這些,是不是使用的時候就自信了很多。但是,這是不是就意味著可以大膽的去使用了呢?其實,不盡然,有一個“大坑”在等著你。
神奇的remove
那個“大坑”指的就是因為ThreadLocal使用不當,會引發記憶體洩露的問題。筆者給出兩段示例程式碼,來說明這個問題。
程式碼出處來源於Stack Overflow:stackoverflow.com/questions/1…
示例一:
1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t = null;
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}
複製程式碼
執行結果:
10
21
32
43
5...省略...
6440
7441
8442
9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12 at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13 at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14 at java.lang.Thread.run(Thread.java:745)
複製程式碼
對上述程式碼稍作修改,請看:
1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t.threadLocal.remove();
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}
複製程式碼
執行結果:
10
21
32
43
5...省略...
6996
7997
8998
9999
複製程式碼
一個記憶體洩漏,一個正常完成,對比程式碼只有一處不同:t = null改為了t.threadLocal.remove(); 哇,神奇的remove!!!筆者先留個懸念,暫且不去分析原因。我們先來看看上述示例中涉及到的兩個方法:set()和remove()。
set(T value)原始碼
1public 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}
複製程式碼
邏輯很簡單:
- 獲取當前執行緒內部的ThreadLocalMap
- map存在則把當前ThreadLocal和value新增到map中
- map不存在則建立一個ThreadLocalMap,儲存到當前執行緒內部
remove原始碼
1public void remove() {
2 ThreadLocalMap m = getMap(Thread.currentThread());
3 if (m != null)
4 m.remove(this);
5}
複製程式碼
就一句話,獲取當前執行緒內部的ThreadLocalMap,存在則從map中刪除這個ThreadLocal物件。
小結
講到這裡,ThreadLocal最常用的四種方法都已經說完了,細心的您是不是已經發現,每一個方法都離不開一個類,那就是ThreadLocalMap。所以,要更好的理解ThreadLocal,就有必要深入的去學習這個map。
無處不在的ThreadLocalMap
還是老規矩,先來看看類上的註釋,翻譯過來就是這麼幾點:
- ThreadLocalMap是一個自定義的hash map,專門用來儲存執行緒的thread local變數
- 它的操作僅限於ThreadLocal類中,不對外暴露
- 這個類被用在Thread類的私有變數threadLocals和inheritableThreadLocals上
- 為了能夠儲存大量且存活時間較長的threadLocal例項,hash table entries採用了WeakReferences作為key的型別
- 一旦hash table執行空間不足時,key為null的entry就會被清理掉
我們來看下類的宣告資訊:
1static class ThreadLocalMap {
2
3 // hash map中的entry繼承自弱引用WeakReference,指向threadLocal物件
4 // 對於key為null的entry,說明不再需要訪問,會從table表中清理掉
5 // 這種entry被成為“stale entries”
6 static class Entry extends WeakReference<ThreadLocal<?>> {
7 /** The value associated with this ThreadLocal. */
8 Object value;
9
10 Entry(ThreadLocal<?> k, Object v) {
11 super(k);
12 value = v;
13 }
14 }
15
16 /**
17 * The initial capacity -- MUST be a power of two.
18 */
19 private static final int INITIAL_CAPACITY = 16;
20
21 /**
22 * The table, resized as necessary.
23 * table.length MUST always be a power of two.
24 */
25 private Entry[] table;
26
27 /**
28 * The number of entries in the table.
29 */
30 private int size = 0;
31
32 /**
33 * The next size value at which to resize.
34 */
35 private int threshold; // Default to 0
36
37 /**
38 * Set the resize threshold to maintain at worst a 2/3 load factor.
39 */
40 private void setThreshold(int len) {
41 threshold = len * 2 / 3;
42 }
43
44 /**
45 * Construct a new map initially containing (firstKey, firstValue).
46 * ThreadLocalMaps are constructed lazily, so we only create
47 * one when we have at least one entry to put in it.
48 */
49 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
50 table = new Entry[INITIAL_CAPACITY];
51 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
52 table[i] = new Entry(firstKey, firstValue);
53 size = 1;
54 setThreshold(INITIAL_CAPACITY);
55 }
56}
複製程式碼
當建立一個ThreadLocalMap時,實際上內部是構建了一個Entry型別的陣列,初始化大小為16,閾值threshold為陣列長度的2/3,Entry型別為WeakReference,有一個弱引用指向ThreadLocal物件。
為什麼Entry採用WeakReference型別?
Java垃圾回收時,看一個物件需不需要回收,就是看這個物件是否可達。什麼是可達,就是能不能通過引用去訪問到這個物件。(當然,垃圾回收的策略遠比這個複雜,這裡為了便於理解,簡單給大家說一下)。
jdk1.2以後,引用就被分為四種型別:強引用、弱引用、軟引用和虛引用。強引用就是我們常用的Object obj = new Object(),obj就是一個強引用,指向了物件記憶體空間。當記憶體空間不足時,Java垃圾回收程式發現物件有一個強引用,寧願丟擲OutofMemory錯誤,也不會去回收一個強引用的記憶體空間。而弱引用,即WeakReference,意思就是當一個物件只有弱引用指向它時,垃圾回收器不管當前記憶體是否足夠,都會進行回收。反過來說,這個物件是否要被垃圾回收掉,取決於是否有強引用指向。ThreadLocalMap這麼做,是不想因為自己儲存了ThreadLocal物件,而影響到它的垃圾回收,而是把這個主動權完全交給了呼叫方,一旦呼叫方不想使用,設定ThreadLocal物件為null,記憶體就可以被回收掉。
記憶體溢位問題解答
至此,該做的鋪墊都已經完成了,此時,我們可以來看看上面那個記憶體洩漏的例子。示例中執行一次for迴圈裡的程式碼後,對應的記憶體狀態:
- t為建立TestClass物件返回的引用,臨時變數,在一次for迴圈後就執行出棧了
- thread為建立Thread物件返回的引用,run方法在執行過程中,暫時不會執行出棧
呼叫t=null後,雖然無法再通過t訪問記憶體地址MemoryLeak
1不能識別此Latex公式:
2TestClass@538,但是當前執行緒依舊存活,可以通過thread指向的記憶體地址,訪問到Thread物件,從而訪問到ThreadLocalMap物件,訪問到value指向的記憶體空間,訪問到arr指向的記憶體空間,從而導致Java垃圾回收並不會回收int[1000000]@541這一片空間。那麼隨著迴圈多次之後,不被回收的堆空間越來越大,最後丟擲java.lang.OutOfMemoryError: Java heap space。
3
4您問:那為什麼呼叫t.threadLocal.remove()就可以呢?
5
6我答:這就得看remove方法裡究竟做了什麼了,請看:
7
8是不是恍然大悟?來看下呼叫remove方法之後的記憶體狀態:
9
10因為remove方法將referent和value都被設定為null,所以ThreadLocal@540和Memory
複製程式碼
TestClass@538對應的記憶體地址都變成不可達,Java垃圾回收自然就會回收這片記憶體,從而不會出現記憶體洩漏的錯誤。
小結
呼應文章開頭提到的《Spring Cloud Netflix Zuul原始碼分析之請求處理篇》,其中就有一個非常重要的類:ZuulServlet,它就是典型的ThreadLocal在實際場景中的運用案例。請看:
1public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
2 try {
3 init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
4 RequestContext context = RequestContext.getCurrentContext();
5 context.setZuulEngineRan();
6
7 try {
8 preRoute();
9 } catch (ZuulException e) {
10 error(e);
11 postRoute();
12 return;
13 }
14 try {
15 route();
16 } catch (ZuulException e) {
17 error(e);
18 postRoute();
19 return;
20 }
21 try {
22 postRoute();
23 } catch (ZuulException e) {
24 error(e);
25 return;
26 }
27
28 } catch (Throwable e) {
29 error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
30 } finally {
31 RequestContext.getCurrentContext().unset();
32 }
33}
複製程式碼
您有沒有發現,一次HTTP請求經由前置過濾器、路由過濾器、後置過濾器處理完成之後,都會呼叫一個方法,沒錯,就是在finally裡,RequestContext.getCurrentContext().unset()。走進RequestContext一看:
1public void unset() {
2 threadLocal.remove();
3}
複製程式碼
看到沒有,神器的remove又出現了。講到這裡,您是否get到ThreadLocal正確的使用”姿勢”呢?
ThreadLocalMap之番外篇
筆者之前寫過關於TreeMap和HashMap的文章,凡是Map的實現,都有自己降低雜湊衝突和解決雜湊衝突的方法。在這裡,ThreadLocalMap是如何處理的呢?請往下看。
如何降低雜湊衝突
回顧ThreadLocalMap新增元素的原始碼:
- 方式一:構造方法
1ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
2 table = new Entry[INITIAL_CAPACITY];
3 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
4 table[i] = new Entry(firstKey, firstValue);
5 size = 1;
6 setThreshold(INITIAL_CAPACITY);
7}
複製程式碼
- 方式二:set方法
1private 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}
複製程式碼
其中i就是ThreadLocal在ThreadLocalMap中存放的索引,計算方式為:key.threadLocalHashCode & (len-1)。我們先來看threadLocalHashCode是什麼?
1private final int threadLocalHashCode = nextHashCode();
複製程式碼
也就是說,每一個ThreadLocal都會根據nextHashCode生成一個int值,作為雜湊值,然後根據這個雜湊值&(陣列長度-1),從而獲取到雜湊值的低N位(以len為16,16-1保證低四位都是1,從而獲取雜湊值本身的低四位值),從而獲取到在陣列中的索引位置。那它是如何降低雜湊衝突的呢?玄機就在於這個nextHashCode方法。
1private static AtomicInteger nextHashCode = new AtomicInteger();
2
3private static final int HASH_INCREMENT = 0x61c88647;
4
5private static int nextHashCode() {
6 return nextHashCode.getAndAdd(HASH_INCREMENT);
7}
複製程式碼
0x61c88647是什麼?轉化為十進位制是1640531527。2654435769轉換成int型別就是-1640531527。2654435769等於(根號5-1)/2乘以2的32次方。(根號5-1)/2是什麼?是黃金分割數,近似為0.618。也就是說0x61c88647理解為一個黃金分割數乘以2的32次方。有什麼好處?它可以神奇的保證nextHashCode生成的雜湊值,均勻的分佈在2的冪次方上,且小於2的32次方。來看例子:
1public class ThreadLocalHashCodeTest {
2
3 private static AtomicInteger nextHashCode =
4 new AtomicInteger();
5
6 private static final int HASH_INCREMENT = 0x61c88647;
7
8 private static int nextHashCode() {
9 return nextHashCode.getAndAdd(HASH_INCREMENT);
10 }
11
12 public static void main(String[] args){
13 for (int i = 0; i < 16; i++) {
14 System.out.print(nextHashCode() & 15);
15 System.out.print(" ");
16 }
17 System.out.println();
18 for (int i = 0; i < 32; i++) {
19 System.out.print(nextHashCode() & 31);
20 System.out.print(" ");
21 }
22 System.out.println();
23 for (int i = 0; i < 64; i++) {
24 System.out.print(nextHashCode() & 63);
25 System.out.print(" ");
26 }
27 }
28}
複製程式碼
輸出結果:
10 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
216 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9
316 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9
複製程式碼
看見沒有,元素索引值完美的雜湊在陣列當中,並沒有出現衝突。
如何解決雜湊衝突
ThreadLocalMap採用黃金分割數的方式,大大降低了雜湊衝突的情況,但是這種情況還是存在的,那如果出現,它是怎麼解決的呢?請看:
1private 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 // 出現雜湊衝突
8 for (Entry e = tab[i];
9 e != null;
10 e = tab[i = nextIndex(i, len)]) {
11 ThreadLocal<?> k = e.get();
12
13 // 如果是同一個物件,則覆蓋value值
14 if (k == key) {
15 e.value = value;
16 return;
17 }
18
19 // 如果key為null,則替換它的位置
20 if (k == null) {
21 replaceStaleEntry(key, value, i);
22 return;
23 }
24
25 // 否則往後一個位置找,直到找到空的位置
26 }
27
28 tab[i] = new Entry(key, value);
29 int sz = ++size;
30 if (!cleanSomeSlots(i, sz) && sz >= threshold)
31 rehash();
32}
複製程式碼
當出現雜湊衝突時,它的做法看是否是同一個物件或者是是否可以替換,否則往後移動一位,繼續判斷。
1private static int nextIndex(int i, int len) {
2 return ((i + 1 < len) ? i + 1 : 0);
3}
複製程式碼
擴容
通過set方法裡的程式碼,我們知道ThreadLocalMap擴容有兩個前提:
- !cleanSomeSlots(i, sz)
- size >= threshold
元素個數大於閾值進行擴容,這個很好理解,那麼還有一個前提是什麼意思呢?我們來看cleanSomeSlots()做了什麼:
1private boolean cleanSomeSlots(int i, int n) {
2 boolean removed = false;
3 Entry[] tab = table;
4 int len = tab.length;
5 do {
6 i = nextIndex(i, len);
7 Entry e = tab[i];
8 if (e != null && e.get() == null) {
9 n = len;
10 removed = true;
11 i = expungeStaleEntry(i);
12 }
13 } while ( (n >>>= 1) != 0);
14 return removed;
15}
複製程式碼
方法上註釋寫的很明白,從當前插入元素位置,往後掃描陣列中的元素,判斷是否是“stale entry”。在前面將ThreadLocalMap類宣告資訊的時候講過,“stale entry”表示的是那些key為null的entry。cleanSomeSlots方法就是找到他們,呼叫expungeStaleEntry方法進行清理。如果找到,則返回true,否則返回false。
您問:為什麼擴容要看它的返回值呢?
我答:因為一旦找到,就呼叫expungeStaleEntry方法進行清理。
1private int expungeStaleEntry(int staleSlot) {
2 Entry[] tab = table;
3 int len = tab.length;
4
5 // expunge entry at staleSlot
6 tab[staleSlot].value = null;
7 tab[staleSlot] = null;
8 size--;
9
10 // 省略
11}
複製程式碼
看到沒有,size會減一,那麼新增元素導致size加1,cleanSomeSlots一旦找到,則會清理一個或者多個元素,size減去的最少為1,所以返回true,自然就沒有必要再判斷size是否大於等於閾值了。
好了,前提條件一旦滿足,則呼叫rehash方法,此時還未擴容:
1private void rehash() {
2 // 先清理stale entry,會導致size變化
3 expungeStaleEntries();
4
5 // 如果size大於等於3/4閾值,則擴容
6 if (size >= threshold - threshold / 4)
7 resize();
8}
複製程式碼
哈哈,這裡才是真正的擴容,要進行擴容:
- 當前插入元素的位置,往後沒有需要清理的stale entry
- size大於等於閾值
- 清理掉stale entry之後,size大於等於3/4閾值
既然搞清楚了條件,那麼滿足後,又是如何擴容的呢?
1private void resize() {
2 Entry[] oldTab = table;
3 int oldLen = oldTab.length;
4 int newLen = oldLen * 2;
5 // 新建一個陣列,按照2倍長度擴容
6 Entry[] newTab = new Entry[newLen];
7 int count = 0;
8
9 for (int j = 0; j < oldLen; ++j) {
10 Entry e = oldTab[j];
11 if (e != null) {
12 ThreadLocal<?> k = e.get();
13 if (k == null) {
14 e.value = null; // Help the GC
15 } else {
16 // key不為null,重新計算索引位置
17 int h = k.threadLocalHashCode & (newLen - 1);
18 while (newTab[h] != null)
19 h = nextIndex(h, newLen);
20 // 插入新的陣列中索引位置
21 newTab[h] = e;
22 count++;
23 }
24 }
25 }
26
27 // 閾值為長度的2/3
28 setThreshold(newLen);
29 size = count;
30 table = newTab;
31}
複製程式碼
兩倍長度擴容,重新計算索引,擴容的同時也順便清理了key為null的元素,即stale entry,不再存入擴容後的陣列中。
補充
不知您有沒有注意到,ThreadLocalMap中出現雜湊衝突時,它是線性探測,直到找到空的位置。這種效率是非常低的,那為什麼Java大神們寫程式碼時還要這麼做呢?筆者認為取決於它採用的雜湊演算法,正因為nextHashCode(),保證了衝突出現的可能性很低。而且ThreadLocalMap在處理過程中非常注意清理”stale entry”,及時釋放出空餘位置,從而降低了線性探測帶來的低效。
總結
本文講了這麼多,主要是為了讓大家明白ThreadLocal應該如何正確的使用,以及使用它背後的原理。後面番外篇,純屬興趣部分,您可以對比之前筆者《HashMap之元素插入》裡面的內容,發散思考。筆者深知水平有限,如有任何意見建議,還請您留言指出,感激不盡!!!最後,感謝大家一如既往的支援,祝近安,祁琛,2019年1月12日。