面試題 - ThreadLocal詳解

2020-10-01 13:00:11

目錄(jdk1.8)

  • 一、什麼是ThreadLocal
  • 二、ThreadLocal怎麼用
  • 三、ThreadLocal的原理
  • 四、ThreadLocal原始碼分析
    • 1.ThreadLocal的內部屬性
    • 2.ThreadLocal 之 set() 方法
    • 3.ThreadLocal 之 get() 方法
    • 4.TreadLocal的remove方法
    • 5.內部類ThreadLocalMap的基本結構和原始碼分析
      • 5.1先看成員和結構部分
      • 5.2接著看ThreadLocalMap的建構函式
      • 5.3ThreadLocalMap 之 set() 方法
      • 5.4ThreadLocalMap 之 getEntry() 方法
      • 5.5ThreadLocalMap 之 rehash() 方法
      • 5.6ThreadLocalMap 之 remove(key) 方法
  • 五、什麼情況下ThreadLocal的使用會導致記憶體漏失
  • 六、ThreadLocal的最佳實踐
  • 七、總結

一、什麼是ThreadLocal

ThreadLocal 是 JDK java.lang 包下的一個類,是天然的執行緒安全的類,

1.ThreadLoca 是執行緒區域性變數,這個變數與普通變數的區別,在於每個存取該變數的執行緒,線上程內部都會
初始化一個獨立的變數副本,只有該執行緒可以存取【get() or set()】該變數,ThreadLocal範例通常宣告
為 private static2.執行緒在存活並且ThreadLocal範例可被存取時,每個執行緒隱含持有一個執行緒區域性變數副本,當執行緒生命週期
結束時,ThreadLocal的範例的副本跟著執行緒一起消失,被GC垃圾回收(除非存在對這些副本的其他參照)

JDK 原始碼中解析:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 * /

稍微翻譯一下:ThreadLocal提供執行緒區域性變數。這些變數與正常的變數不同,因為每一個執行緒在存取ThreadLocal範例的時候(通過其get或set方法)都有自己的、獨立初始化的變數副本。ThreadLocal範例通常是類中的私有靜態欄位,使用它的目的是希望將狀態(例如,使用者ID或事務ID)與執行緒關聯起來。

二、ThreadLocal怎麼用

討論ThreadLocal用在什麼地方前,我們先明確下,如果僅僅就一個執行緒,那麼都不用談ThreadLocal的,ThreadLocal是用在多執行緒的場景的!!!

ThreadLocal歸納下來就3類用途:

  1. 儲存執行緒上下文資訊,在任意需要的地方可以獲取!!!
  2. 執行緒安全的,避免某些情況需要考慮執行緒安全必須同步帶來的效能損失!!!
  3. 執行緒間資料隔離

1.儲存執行緒上下文資訊,在任意需要的地方可以獲取!!!
由於ThreadLocal的特性,同一執行緒在某地方進行設定,在隨後的任意地方都可以獲取到。從而可以用來儲存執行緒上下文資訊。

常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄紀錄檔的方法裡面進行get獲取到請求id,從而把整個請求串起來。

還有比如Spring的事務管理,用ThreadLocal儲存Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。

2.執行緒安全的,避免某些情況需要考慮執行緒安全必須同步帶來的效能損失!!!
由於不需要共用資訊,自然就不存在競爭問題了,從而保證了某些情況下執行緒的安全,以及避免了某些情況需要考慮執行緒安全必須同步帶來的效能損失!!!

ThreadLocal侷限性
ThreadLocal為解決多執行緒程式的並行問題提供了一種新的思路。但是ThreadLocal也有侷限性,我們來看看阿里規範:
在這裡插入圖片描述
這類場景阿里規範裡面也提到了:
在這裡插入圖片描述
ThreadLocal用法

public class MyThreadLocalDemo {

	private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) throws InterruptedException {
        int threads = 9;
        MyThreadLocalDemo demo = new MyThreadLocalDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                threadLocal.set(Thread.currentThread().getName());
                System.out.println("threadLocal.get()================>" + threadLocal.get());
                countDownLatch.countDown();
            }, "執行執行緒 - " + i);
            thread.start();
        }
        countDownLatch.await();
    }

}

程式碼執行結果:

threadLocal.get()================>執行執行緒 - 1
threadLocal.get()================>執行執行緒 - 0
threadLocal.get()================>執行執行緒 - 3
threadLocal.get()================>執行執行緒 - 4
threadLocal.get()================>執行執行緒 - 5
threadLocal.get()================>執行執行緒 - 8
threadLocal.get()================>執行執行緒 - 7
threadLocal.get()================>執行執行緒 - 2
threadLocal.get()================>執行執行緒 - 6

Process finished with exit code 0

三、ThreadLocal的原理

在這裡插入圖片描述

以兩個執行緒為例:

ThreadLocal雖然叫執行緒區域性變數,但是實際上它並不存放任何的資訊,可以這樣理解:它是執行緒(Thread)操作ThreadLocalMap中存放的變數的橋樑。它主要提供了初始化、set()、get()、remove()幾個方法。這樣說可能有點抽象,下面畫個圖說明一下線上程中使用ThreadLocal範例的set()和get()方法的簡單流程圖。

假設我們有如下的程式碼,主執行緒的執行緒名字是main(也有可能不是main):

public class Main {

    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception{
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}

在這裡插入圖片描述
上面只描述了單執行緒的情況並且因為是主執行緒忽略了Thread t = new Thread()這一步,如果有多個執行緒會稍微複雜一些,但是原理是不變的,ThreadLocal範例總是通過Thread.currentThread()獲取到當前操作執行緒範例,然後去操作執行緒範例中的ThreadLocalMap型別的成員變數,因此它是一個橋樑,本身不具備儲存功能

四、ThreadLocal原始碼分析

從Thread原始碼入手:

public class Thread implements Runnable {
......
//與此執行緒有關的ThreadLocal值。該對映由ThreadLocal類維護。
ThreadLocal.ThreadLocalMap threadLocals = null;
//與此執行緒有關的InheritableThreadLocal值。該Map由InheritableThreadLocal類維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

從上面Thread類原始碼可以看出Thread類中有一個threadLocals和一個inheritableThreadLocals 變數,它們都是ThreadLocalMap型別的變數,預設情況下這兩個變數都是null,只有當前執行緒呼叫ThreadLocal類的Iset或get方法時才建立它們,實際上呼叫這兩個方法的時候,我們呼叫的是ThreadLocalMap類對應的get()、set()方法。
在這裡插入圖片描述

在這裡插入圖片描述

1.ThreadLocal的內部屬性

ThreadLocalMap 的 key 是 ThreadLocal,但它不會傳統的呼叫 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是呼叫 nextHashCode() ,具體運算如下:

public class ThreadLocal<T> {
	//獲取下一個ThreadLocal範例的雜湊魔數
	private final int threadLocalHashCode = nextHashCode();
	
	//原子計數器,主要到它被定義為靜態
	private static AtomicInteger nextHashCode = new AtomicInteger();
	
	//雜湊魔數(增長數),也是帶符號的32位元整型值黃金分割值的取正
	private static final int HASH_INCREMENT = 0x61c88647;
	
	//生成下一個雜湊魔數
	private static int nextHashCode() {
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

這裡需要注意一點,threadLocalHashCode是一個final的屬性,而原子計數器變數nextHashCode和生成下一個雜湊魔數的方法nextHashCode()是靜態變數和靜態方法,靜態變數只會初始化一次。換而言之,每新建一個ThreadLocal範例,它內部的threadLocalHashCode就會增加0x61c88647。舉個例子:

//t1中的threadLocalHashCode變數為0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode變數為0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode變數為0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap結構中使用的雜湊演演算法的核心變數,對於每個ThreadLocal範例,它的threadLocalHashCode是唯一的。

這裡寫個demo看一下基於魔數 1640531527 方式產生的hash分佈多均勻:


public class ThreadLocalTest {
    public static void main(String[] args) {
        printAllSlot(8);
        printAllSlot(16);
        printAllSlot(32);
    }

    static void printAllSlot(int len) {
        System.out.println("********** len = " + len + " ************");
        for (int i = 1; i <= 64; i++) {
            ThreadLocal<String> t = new ThreadLocal<>();
            int slot = getSlot(t, len);
            System.out.print(slot + " ");
            if (i % len == 0) {
                System.out.println(); // 分組換行
            }
        }
    }

    /**
     * 獲取槽位
     *
     * @param t   ThreadLocal
     * @param len 模擬map的table的length
     * @throws Exception
     */
    static int getSlot(ThreadLocal<?> t, int len) {
        int hash = getHashCode(t);
        return hash & (len - 1);
    }

    /**
     * 反射獲取 threadLocalHashCode 欄位,因為其為private的
     */
    static int getHashCode(ThreadLocal<?> t) {
        Field field;
        try {
            field = t.getClass().getDeclaredField("threadLocalHashCode");
            field.setAccessible(true);
            return (int) field.get(t);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

上述程式碼模擬了 ThreadLocal 做為 key 的hashCode產生,看看完美槽位分配:

********** len = 8 ************
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 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 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 

Process finished with exit code 0

2. ThreadLocal 之 set() 方法

ThreadLocal中set()方法的原始碼如下:


  protected T initialValue() {
        return null;
    }
    
   /**
    * 將此執行緒區域性變數的當前執行緒副本設定為指定值。大多數子類將不需要
    * 重寫此方法,而僅依靠{@link #initialValue} 
    * 方法來設定執行緒區域性變數的值。
    *
    * @param value 要儲存在此執行緒的thread-local副本中的值
    */
   public void set(T value) {
    //設定值前總是獲取當前執行緒範例
    Thread t = Thread.currentThread();
    //從當前執行緒範例中獲取threadLocals屬性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals屬性不為null則覆蓋key為當前的ThreadLocal範例,值為value
         map.set(this, value);
    else
    //threadLocals屬性為null,則建立ThreadLocalMap,第一個項的Key為當前的ThreadLocal範例,值為value
        createMap(t, value);
	}
	
	//這裡看到獲取ThreadLocalMap範例時候總是從執行緒範例的成員變數獲取
 	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    //建立ThreadLocalMap範例的時候,會把新範例賦值到執行緒範例的threadLocals成員
 	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上面的過程原始碼很簡單,設定值的時候總是先獲取當前執行緒範例並且操作它的變數threadLocals。步驟是:

  1. 獲取當前執行執行緒的範例。
  2. 通過執行緒範例獲取執行緒範例成員threadLocals(ThreadLocalMap),如果為null,則建立一個新的ThreadLocalMap範例賦值到threadLocals。
  3. 通過threadLocals設定值value,如果原來的雜湊槽已經存在值,則進行覆蓋。

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

3.ThreadLocal 之 get() 方法

ThreadLocal中get()方法的原始碼如下:


 	/**
     * 返回此執行緒區域性變數的當前執行緒副本中的值。如果該變數沒有當前執行緒的值,
     * 則首先通過呼叫{@link #initialValue}方法將其初始化為*返回的值。
     *
     * @return 當前執行緒區域性變數中的值
     */
     public T get() {
	    //獲取當前執行緒的範例
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
	    //根據當前的ThreadLocal範例獲取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
	    //執行緒範例中的threadLocals為null,則呼叫initialValue方法,並且建立ThreadLocalMap賦值到threadLocals
	    return setInitialValue();
	}
	
	private T setInitialValue() {
	    // 呼叫initialValue方法獲取值
	    T value = initialValue();
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // ThreadLocalMap如果未初始化則進行一次建立,已初始化則直接設定值
	    if (map != null)
	        map.set(this, value);
	    else
	        createMap(t, value);
	    return value;
	}
	
	protected T initialValue() {
       return null;
    }

initialValue()方法預設返回null,如果ThreadLocal範例沒有使用過set()方法直接使用get()方法,那麼ThreadLocalMap中的此ThreadLocal為Key的項會把值設定為initialValue()方法的返回值。如果想改變這個邏輯可以對initialValue()方法進行覆蓋。
在這裡插入圖片描述

4.TreadLocal的remove方法

ThreadLocal中remove()方法的原始碼如下:

public void remove() {
    //獲取Thread範例中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根據當前ThreadLocal作為Key對ThreadLocalMap的元素進行移除
       m.remove(this);
}

在這裡插入圖片描述

這裡羅列了 ThreadLocal 的幾個public方法,其實所有工作最終都落到了 ThreadLocalMap 的頭上,ThreadLocal 僅僅是從當前執行緒取到 ThreadLocalMap 而已,具體執行,請看下面對 ThreadLocalMap 的分析。

5.內部類ThreadLocalMap的基本結構和原始碼分析

ThreadLocalMap 是ThreadLocal 內部的一個Map實現,然而它並沒有實現任何集合的介面規範,因為它僅供內部使用,資料結構採用 陣列 + 開方地址法,Entry 繼承 WeakReference,是基於 ThreadLocal 這種特殊場景實現的 Map,它的實現方式很值得研究。

ThreadLocal內部類ThreadLocalMap使用了預設修飾符,也就是包(包私有)可存取的。ThreadLocalMap內部定義了一個靜態類Entry。我們重點看下ThreadLocalMap的原始碼,

5.1先看成員和結構部分

/**
 * ThreadLocalMap是一個客製化的雜湊對映,僅適用於維護執行緒本地變數。
 * 它的所有方法都是定義在ThreadLocal類之內。
 * 它是包私有的,所以在Thread類中可以定義ThreadLocalMap作為變數。
 * 為了處理非常大(指的是值)和長時間的用途,雜湊表的Key使用了弱參照(WeakReferences)。
 * 參照的佇列(弱參照)不再被使用的時候,對應的過期的條目就能通過主動刪除移出雜湊表。
 */
static class ThreadLocalMap {

    //注意這裡的Entry的Key為WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {

        //這個是真正的存放的值
        Object value;
        // Entry的Key就是ThreadLocal範例本身,Value就是輸入的值
        Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
        }
    }
    //初始化容量,必須是2的冪次方
    private static final int INITIAL_CAPACITY = 16;

    //雜湊(Entry)表,必須時擴容,長度必須為2的冪次方
    private Entry[] table;

    //雜湊表中元素(Entry)的個數
    private int size = 0;

    //下一次需要擴容的閾值,預設值為0
    private int threshold;

    //設定下一次需要擴容的閾值,設定值為輸入值len的三分之二
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    // 以len為模增加i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    // 以len為模減少i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}
  1. 這裡注意到十分重要的一點:ThreadLocalMap$Entry是WeakReference(弱參照),並且鍵值Key為ThreadLocal<?>範例本身,這裡使用了無限定的泛型萬用字元。
  2. ThreadLocalMap 的 key 是 ThreadLocal,但它不會傳統的呼叫 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是呼叫 nextHashCode()

5.2接著看ThreadLocalMap的建構函式

// 構造ThreadLocal時候使用,對應ThreadLocal的實體方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 雜湊表預設容量為16
    table = new Entry[INITIAL_CAPACITY];
    // 計算第一個元素的雜湊碼
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 構造InheritableThreadLocal時候使用,基於父執行緒的ThreadLocalMap裡面的內容進行
// 提取放入新的ThreadLocalMap的雜湊表中
// 對應ThreadLocal的靜態方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基於父ThreadLocalMap的雜湊表進行拷貝
    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

這裡注意一下,ThreadLocal的set()方法呼叫的時候會懶初始化一個ThreadLocalMap並且放入第一個元素。而ThreadLocalMap的私有構造是提供給靜態方法ThreadLocal#createInheritedMap()使用的。

5.3ThreadLocalMap 之 set() 方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算槽位
    // hash衝突時,使用開放地址法
    // 因為獨特和hash演演算法,導致hash衝突很少,一般不會走進這個for迴圈
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同,則覆蓋value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,說明 key 已經被回收了,進入替換方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的值,並判斷是否需要擴容
        rehash(); // 擴容
}

這個 set 方法涵蓋了很多關鍵點:

  1. 開放地址法:與我們常用的Map不同,java裡大部分Map都是用連結串列發解決hash衝突的,而 ThreadLocalMap 採用的是開發地址法。
  2. hash演演算法:hash值演演算法的精妙之處上面已經講了,均勻的 hash 演演算法使其可以很好的配合開方地址法使用;
  3. 過期值清理

下面對 set 方法裡面的幾個關鍵方法展開:

1.replaceStaleEntry()
因為開發地址發的使用,導致 replaceStaleEntry 這個方法有些複雜,它的清理工作會涉及到slot前後的非null的slot。

//這裡個方法比較長,作用是替換雜湊碼為staleSlot的雜湊槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前尋找過期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才終止迴圈
    // 遍歷staleSlot之後的雜湊槽,如果Key匹配則用輸入值替換
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了key,那麼需要將它與過期的 slot 交換來維護雜湊表的順序。
        // 然後可以將新過期的 slot 或其上面遇到的任何其他過期的 slot 
        // 給 expungeStaleEntry 以清除或 rehash 這個 run 中的所有其他entries。

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果存在,則開始清除前面過期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我們沒有在向前掃描中找到過期的條目,
        // 那麼在掃描 key 時看到的第一個過期 entry 是仍然存在於 run 中的條目。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果沒有找到 key,那麼在 slot 中建立新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果還有其他過期的entries存在 run 中,則清除他們
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中的 run 不好翻譯,理解為開放地址中一個slot中前後不為null的連續entry

2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是不是有點模糊,到底是哪些?)

//清理第i個雜湊槽之後的n個雜湊槽,如果遍歷的時候發現Entry的Key為null,則n會重置為雜湊表的長度,
//expungeStaleEntry有可能會重雜湊使得雜湊表長度發生變化
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i); // 清除方法 
        }
    } while ( (n >>>= 1) != 0);  // n = n / 2, 對數控制迴圈 
    return removed;
}

當新元素被新增時,或者另一個過期元素已被刪除時,會呼叫cleanSomeSlots。該方法會試探性地掃描一些 entry 尋找過期的條目。它執行 對數 數量的掃描,是一種 基於不掃描(快速但保留垃圾)和 所有元素掃描之間的平衡。

上面說到的對數數量是多少?迴圈次數 = log2(N) (log以2為底N的對數),此處N是map的size,如:

log2(4= 2
log2(5= 2
log2(18= 4

因此,此方法並沒有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 裡面

3.expungeStaleEntry(int staleSlot)

這裡是真正的清除,並且不要被方法名迷惑,不僅僅會清除當前過期的slot,還回往後查詢直到遇到null的slot為止。開放地址法的清除也較難理解,清除當前slot後還有往後進行rehash。

//對當前雜湊表中所有的Key為null的Entry呼叫expungeStaleEntry
// 1.清空staleSlot對應雜湊槽的Key和Value
// 2.對staleSlot到下一個空的雜湊槽之間的所有可能衝突的雜湊表部分槽進行重雜湊,置空Key為null的槽
// 3.注意返回值是staleSlot之後的下一個空的雜湊槽的雜湊碼
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清空staleSlot對應雜湊槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 null 的 slot
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {//空key直接清除
            e.value = null;
            tab[i] = null;
            size--;
        } else {//key非空,則Rehash
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

5.4ThreadLocalMap 之 getEntry() 方法

getEntry() 主要是在 ThreadLocal 的 get() 方法裡被呼叫

/**
 * 這個方法主要給`ThreadLocal#get()`呼叫,通過當前ThreadLocal範例獲取雜湊表中對應的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 計算Entry的雜湊值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)//無hash衝突情況
        return e;
    else  // 注意這裡,如果e為null或者Key對不上,表示:有hash衝突情況,會呼叫getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 如果Key在雜湊表中找不到雜湊槽的時候會呼叫此方法
// 這個方法是在遇到 hash 衝突時往後繼續查詢,並且會清除查詢路上遇到的過期slot。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 這裡會通過nextIndex嘗試遍歷整個雜湊表,如果找到匹配的Key則返回Entry
    // 如果雜湊表中存在Key == null的情況,呼叫expungeStaleEntry進行清理
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

5.5ThreadLocalMap 之 rehash() 方法

// 重雜湊,必要時進行擴容
private void rehash() {
    // 清理所有空的雜湊槽,並且進行重雜湊
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 在上面的清除過程中,size會減小,在此處重新計算是否需要擴容
    // 並沒有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發resize
    if (size >= threshold - threshold / 4)
        resize();
}

// 對當前雜湊表中所有的Key為null的Entry呼叫expungeStaleEntry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 擴容,簡單的擴大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                     h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

PS :ThreadLocalMap 沒有 影響因子 的欄位,是採用直接設定 threshold 的方式,threshold = len * 2 / 3,相當於不可修改的影響因子為 2/3,比 HashMap 的預設 0.75 要低。這也是減少hash衝突的方式。

5.6ThreadLocalMap 之 remove(key) 方法

	/**
	 * Remove the entry for key.
	 */
	private void remove(ThreadLocal<?> key) {
	    Entry[] tab = table;
	    int len = tab.length;
	    int i = key.threadLocalHashCode & (len-1);
	    for (Entry e = tab[i];
	         e != null;
	         e = tab[i = nextIndex(i, len)]) {
	        if (e.get() == key) {
	            e.clear();
	            expungeStaleEntry(i);
	            return;
	        }
	    }
	}

remove 方法是刪除特定的 ThreadLocal,建議在 ThreadLocal 使用完後一定要執行此方法。

五、什麼情況下ThreadLocal的使用會導致記憶體漏失

其實ThreadLocal本身不存放任何的資料,而ThreadLocal中的資料實際上是存放線上程範例中,從實際來看是執行緒記憶體漏失,底層來看是Thread物件中的成員變數threadLocals持有大量的K-V結構,並且執行緒一直處於活躍狀態導致變數threadLocals無法釋放被回收。threadLocals持有大量的K-V結構這一點的前提是要存在大量的ThreadLocal範例的定義,一般來說,一個應用不可能定義大量的ThreadLocal,所以一般的洩漏源是執行緒一直處於活躍狀態導致變數threadLocals無法釋放被回收。但是我們知道,·ThreadLocalMap·中的Entry結構的Key用到了弱參照(·WeakReference<ThreadLocal<?>>·),當沒有強參照來參照ThreadLocal範例的時候,JVM的GC會回收ThreadLocalMap中的這些Key,此時,ThreadLocalMap中會出現一些Key為null,但是Value不為null的Entry項,這些Entry項如果不主動清理,就會一直駐留在ThreadLocalMap中。也就是為什麼ThreadLocal中get()、set()、remove()這些方法中都存在清理ThreadLocalMap範例key為null的程式碼塊。總結下來,記憶體漏失可能出現的地方是:

大量地(靜態)初始化ThreadLocal範例,初始化之後不再呼叫get()、set()、remove()方法。

初始化了大量的ThreadLocal,這些ThreadLocal中存放了容量大的Value,並且使用了這些ThreadLocal範例的執行緒一直處於活躍的狀態。
ThreadLocal中一個設計亮點是ThreadLocalMap中的Entry結構的Key用到了弱參照。試想如果使用強參照,等於ThreadLocalMap中的所有資料都是與Thread的生命週期繫結,這樣很容易出現因為大量執行緒持續活躍導致的記憶體漏失。使用了弱參照的話,JVM觸發GC回收弱參照後,ThreadLocal在下一次呼叫get()、set()、remove()方法就可以刪除那些ThreadLocalMap中Key為null的值,起到了惰性刪除釋放記憶體的作用。

其實ThreadLocal在設定內部類ThreadLocal.ThreadLocalMap中構建的Entry雜湊表已經考慮到記憶體漏失的問題,所以ThreadLocal.ThreadLocalMap$Entry類設計為弱參照,類簽名為static class Entry extends WeakReference<ThreadLocal<?>>。之前一篇文章介紹過,如果弱參照關聯的物件如果置為null,那麼該弱參照會在下一次GC時候回收弱參照關聯的物件。舉個例子:

public class ThreadLocalMain {

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

這種情況下,TL_1這個ThreadLocal在主動GC之後,執行緒繫結的ThreadLocal.ThreadLocalMap範例中的Entry雜湊表中原來的TL_1所在的雜湊槽Entry的參照持有值referent(繼承自WeakReference)會變成null,但是Entry中的value是強參照,還存放著TL_1這個ThreadLocal未回收之前的值。這些被」孤立」的雜湊槽Entry就是前面說到的要惰性刪除的雜湊槽。

六、ThreadLocal的最佳實踐

其實ThreadLocal的最佳實踐很簡單:

  • 每次使用完ThreadLocal範例,都呼叫它的remove()方法,清除Entry中的資料。

呼叫remove()方法最佳時機是執行緒執行結束之前的finally程式碼塊中呼叫,這樣能完全避免操作不當導致的記憶體漏失,這種主動清理的方式比惰性刪除有效。

七、總結

ThreadLocal執行緒本地變數是執行緒範例傳遞和儲存共用變數的橋樑,真正的共用變數還是存放線上程範例本身的屬性中。ThreadLocal裡面的基本邏輯並不複雜,但是一旦涉及到效能影響、記憶體回收(弱參照)和惰性刪除等環節,其實它考慮到的東西還是相對全面而且有效的。

ThreadLocalMap 的 value 清理觸發時間:

  1. set(ThreadLocal<?> key, Object value)
    若無hash衝突,則先向後檢測log2(N)個位置,發現過期 slot 則清除,如果沒有任何 slot 被清除,則判斷 size >= threshold,超過閥值會進行 rehash(),rehash()會清除所有過期的value;
  2. getEntry(ThreadLocal<?> key) (ThreadLocal 的 get() 方法呼叫)
    如果沒有直接在hash計算的 slot 中找到entry, 則需要向後繼續查詢(直到null為止),查詢期間發現的過期 slot 會被清除;
  3. remove(ThreadLocal<?> key)
    remove 不僅會清除需要清除的 key,還是清除hash衝突的位置的已過期的 key;
    清晰了以上過程,相信對於 ThreadLocal 的 記憶體溢位問題會有自己的看法。在實際開發中,不應亂用 ThreadLocal ,如果使用 ThreadLocal 發生了記憶體溢位,那應該考慮是否使用合理。

PS:這裡的清除並不代表被回收,只是把 value 置為 null,value 的具體回收時間由 垃圾收集器 決定。

參考連結:

https://www.jianshu.com/p/56f64e3c1b6c
https://blog.51cto.com/14409778/2416835?source=dra