有關 ThreadLocal 的一切

七淅在學Java發表於2022-05-10

早上好,各位新老讀者們,我是七淅(xī)。

今天和大家分享的是面試常駐嘉賓:ThreadLocal

當初鵝廠一面就有問到它,問題的答案在下面正文的第 2 點。

1. 底層結構

ThreadLocal 底層有一個預設容量為 16 的陣列組成,k 是 ThreadLocal 物件的引用,v 是要放到 TheadLocal 的值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

陣列類似為 HashMap,對雜湊衝突的處理不是用連結串列/紅黑樹處理,而是使用鏈地址法,即嘗試順序放到雜湊衝突下標的下一個下標位置。

該陣列也可以進行擴容。

2. 工作原理

一個 ThreadLocal 物件維護一個 ThreadLocalMap 內部類物件,ThreadLocalMap 物件才是儲存鍵值的地方。

更準確的說,是 ThreadLocalMap 的 Entry 內部類是儲存鍵值的地方

見原始碼 set(),createMap() 可知。

因為一個 Thread 物件維護了一個 ThreadLocal.ThreadLocalMap 成員變數,且 ThreadLocal 設定值時,獲取的 ThreadLocalMap 正是當前執行緒物件的 ThreadLocalMap

// 獲取 ThreadLocalMap 原始碼
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

所以每個執行緒對 ThreadLocal 的操作互不干擾,即 ThreadLocal 能實現執行緒隔離

3. 使用

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學Java");
Integer i = threadLocal.get()
// i = 七淅在學Java

4. 為什麼 ThreadLocal.ThreadLocalMap 底層是長度 16 的陣列呢?

對 ThreadLocal 的操作見第 3 點,可以看到 ThreadLocal 每次 set 方法都是對同個 key(因為是同個 ThreadLocal 物件,所以 key 肯定都是一樣的)進行操作。

如此操作,看似對 ThreadLocal 的操作永遠只會存 1 個值,那用長度為 1 的陣列它不香嗎?為什麼還要用 16 長度呢?

好了,其實這裡有個需要注意的地方,ThreadLocal 是可以存多個值的

那怎麼存多個值呢?看如下程式碼:

// 在主執行緒執行以下程式碼:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在學Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("七淅在學Java2");

按程式碼執行後,看著是 new 了 2 個 ThreadLocal 物件,但實際上,資料的儲存都是在同一個 ThreadLocal.ThreadLocalMap 上操作的

再次強調:ThreadLocal.ThreadLocalMap 才是資料存取的地方,ThreadLocal 只是 api 呼叫入口)。真相在 ThreadLocal 類原始碼的 getMap()

因此上述程式碼最終結果就是一個 ThreadLocalMap 存了 2 個不同 ThreadLocal 物件作為 key,對應 value 為 七淅在學Java、七淅在學Java2。

我們再看下 ThreadLocal 的 set 方法

public void set(T value) {
    Thread t = Thread.currentThread();
    // 這裡每次 set 之前,都會呼叫 getMap(t) 方法,t 是當前呼叫 set 方法的執行緒
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 重點:返回撥用 set 方法的執行緒(例子是主執行緒)的 ThreadLocal 物件。  
// 所以不管 api 呼叫方 new 多少個 ThreadLocal 物件,它永遠都是返回撥用執行緒(例子是主執行緒)的 ThreadLocal.ThreadLocalMap 物件供呼叫執行緒去存取資料。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// t.threadLocals 的宣告如下
ThreadLocal.ThreadLocalMap threadLocals = null;

// 僅有一個構造方法
public ThreadLocal() {
}

5. 資料存放在陣列中,那如何解決 hash 衝突問題

使用鏈地址法解決。

具體怎麼解決呢?看看執行 get、set 方法的時候:

  • set:

    • 根據 ThreadLocal 物件的 hash 值,定位到 ThreadLocalMap 陣列中的位置。
    • 如果位置無元素則直接放到該位置
    • 如果有元素

      • 且陣列的 key 等於該 ThreadLocal,則覆蓋該位置元素
      • 否則就找下一個空位置,直到找到空或者 key 相等為止。
  • get:

    • 根據 ThreadLocal 物件的 hash 值,定位到 ThreadLocalMap 陣列中的位置。
    • 如果不一致,就判斷下一個位置
    • 否則則直接取出
// 陣列元素結構
Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
}

6. ThreadLocal 的記憶體洩露隱患

三個前置知識:

  • ThreadLocal 物件維護一個 ThreadLocalMap 內部類
  • ThreadLocalMap 物件又維護一個 Entry 內部類,並且該類繼承弱引用 WeakReference<ThreadLocal<?>>,用來存放作為 key 的 ThreadLocal 物件(可見最下方的 Entry 構造方法原始碼),可見最後的原始碼部分。
  • 不管當前記憶體空間足夠與否,GC 時 JVM 會回收弱引用的記憶體

因為 ThreadLocal 作為弱引用被 Entry 中的 Key 變數引用,所以如果 ThreadLocal 沒有外部強引用來引用它,那麼 ThreadLocal 會在下次 JVM 垃圾收集時被回收。

這個時候 Entry 中的 key 已經被回收,但 value 因為是強引用,所以不會被垃圾收集器回收。這樣 ThreadLocal 的執行緒如果一直持續執行,value 就一直得不到回收,導致發生記憶體洩露。

如果想要避免記憶體洩漏,可以使用 ThreadLocal 物件的 remove() 方法

7. 為什麼 ThreadLocalMap 的 key 是弱引用

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

為什麼要這樣設計,這樣分為兩種情況來討論:

  • key 使用強引用:只有建立 ThreadLocal 的執行緒還在執行,那麼 ThreadLocalMap 的鍵值就都會記憶體洩漏,因為 ThreadLocalMap 的生命週期同建立它的 Thread 物件。
  • key 使用弱引用:是一種挽救措施,起碼弱引用的值可以被及時 GC,減輕記憶體洩漏。另外,即使沒有手動刪除,作為鍵的 ThreadLocal 也會被回收。因為 ThreadLocalMap 呼叫 set、get、remove 時,都會先判斷之前該 value 對應的 key 是否和當前呼叫的 key 相等。如果不相等,說明之前的 key 已經被回收了,此時 value 也會被回收。因此 key 使用弱引用是最優的解決方案。

8. (父子執行緒)如何共享 ThreadLocal 資料

  1. 主執行緒建立 InheritableThreadLocal 物件時,會為 t.inheritableThreadLocals 變數建立 ThreadLocalMap,使其初始化。其中 t 是當前執行緒,即主執行緒
  2. 建立子執行緒時,在 Thread 的構造方法,會檢查其父執行緒的 inheritableThreadLocals 是否為 null。從第 1 步可知不為 null,接著 將父執行緒的 inheritableThreadLocals 變數值複製給這個子執行緒。
  3. InheritableThreadLocal 重寫了 getMap, createMap, 使用的都是 Thread.inheritableThreadLocals 變數

如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> 

關鍵原始碼:

第 1 步:對 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

第 2 步:建立子執行緒時,判斷父執行緒的 inheritableThreadLocals 是否為空。非空進行復制
// Thread 構造方法中,一定會執行下面邏輯
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

第 3 步:使用物件為第 1 步建立的 inheritableThreadLocals 物件
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
}

示例:
// 結果:能夠輸出「父執行緒-七淅在學Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父執行緒-七淅在學Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();

// 結果:null,不能夠輸出「子執行緒-七淅在學Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
    threadLocal2.set("子執行緒-七淅在學Java");
});
t2.start();
System.out.println(threadLocal2.get());

文章首發公眾號:七淅在學Java ,持續原創輸出 Java 後端乾貨。

如果對你有幫助的話,可以給個贊再走嗎

相關文章