ThreadLocal之深度解讀

SevenBlue發表於2019-02-28

微信公眾號: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<StringObject{
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 != nullreturn 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(thisvalue);
7    else
8        createMap(t, value);
9    return value;
10}
複製程式碼

邏輯也很簡單:

  • 呼叫initialValue方法,獲取初始化值【呼叫者通過覆蓋該方法,設定自己的初始化值】
  • 獲取當前執行緒內部的ThreadLocalMap
  • map存在則把當前ThreadLocal和value新增到map中
  • map不存在則建立一個ThreadLocalMap,儲存到當前執行緒內部
時序圖

為了便於理解,筆者特地畫了一個時序圖,請看:

get方法時序圖
get方法時序圖
小結

至此,您能回答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(value{
2    Thread t = Thread.currentThread();
3    ThreadLocalMap map = getMap(t);
4    if (map != null)
5        map.set(thisvalue);
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
image

8是不是恍然大悟?來看下呼叫remove方法之後的記憶體狀態:
9
image

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}
複製程式碼

哈哈,這裡才是真正的擴容,要進行擴容:

  1. 當前插入元素的位置,往後沒有需要清理的stale entry
  2. size大於等於閾值
  3. 清理掉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日。

ThreadLocal之深度解讀

相關文章