弄懂 ThreadLocal,看這一篇就夠了

1 什麼是 ThreadLocal?

ThreadLocal 類用於提供線程內部的局部變量,變量在多線程環境下訪問(通過 get 和 set 方法訪問)時能保證各個線程的變量相對獨立於其他線程內的變量。ThreadLocal 實例通常來說都是 private static 類型的,用於關聯線程和線程上下文。

ThreadLocal 有幾個常用的方法,分別爲 set(存儲),get(獲取),remove(刪除),下面我會對這幾個方法分別進行介紹。

2 set 方法

我們如何設置當前線程對應的值呢?通過 set 方法即可。

public void set(T value) {
      //獲取當前線程
      Thread t = Thread.currentThread();
      //實際存儲的數據結構類型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,沒有則創建map並set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }

getMap 方法的實現如下:

ThreadLocalMap getMap(Thread t) {
      //thred中維護了一個ThreadLocalMap
      return t.threadLocals;
  }

createMap 方法的實現如下:

void createMap(Thread t, T firstValue) {
      //實例化一個新的ThreadLocalMap,並賦值給線程的成員變量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在上面,我們可以發現,每一個線程都持有一個 ThreadLocalMap 對象,如果該對象未被實例化則就將其實例化且賦值給成員變量 threadLocals,否則就直接使用已經實例化的對象,然後將對 ThreadLocal 的操作轉化爲對 ThreadLocalMap 對象的操作。

每一個線程都持有一個 ThreadLocalMap 對象,從 Thread 的源碼來看,確實如此。以下代碼是在 Thread 中對於 ThreadLocalMap 的聲明。

ThreadLocal.ThreadLocalMap threadLocals = null;

3 ThreadLocalMap

我們上面提到過,對 ThreadLocal 的操作最終會轉化爲對 ThreadLocalMap 的操作,我們接下來就來學習一下 ThreadLocalMap 的操作。

Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

Entry 爲 ThreadLocalMap 的靜態內部類,也是對 ThreadLocal 的弱引用,使 ThreadLocal 和儲值形成 key-value 的關係。

構造方法

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //內部成員數組,INITIAL_CAPACITY值爲16的常量
        table = new Entry[INITIAL_CAPACITY];
        //位運算,結果與取模相同,計算出需要存放的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

可見,在實例化 ThreadLocalMap 時,創建了一個長度爲16的 Entry 數組,然後通過 hashCode 與 length 位運算確定了一個索引值 i,這個 i 就是元素被存儲在 table 數組中的位置。

我們之前說過,ThreadLocal 的操作在底層被轉化爲對 ThreadLocalMap 的操作,且每個線程內部實現了一個 ThreadLocalMap 類型的實例 threadLocals。在這裏,我們發現,ThreadLocalMap 其實內部維護了一個數組,即每個線程內部維護了一個數組,一切操作,都是通過對數組的操作來實現的。

在一個線程內聲明多個 ThreadLocal

如果我們在一個線程內聲明多個 ThreadLocal,由於一個線程只維護一個ThreadLocalMap,所以這多個 ThreadLocal 對應了一個 ThreadLocalMap 對象,那麼我們應該如何在一個 ThreadLocalMap 管理這多個 ThreadLocal 呢?

由於 ThreadLocalMap 的底層實現爲數組,我們便自然而然地想到把多個 ThreadLocal 存放到數組的不同位置即可。那麼問題來了,這多個 ThreadLocal 在數組中的位置是如何確定的呢?爲了能夠正確訪問,我們需要有一種方法來計算 ThreadLocal 在數組中的索引值。那麼,接下來我們便來看看在 ThreadLocalMap 的 set 方法中是如何計算索引值的。

  		//ThreadLocalMap中set方法
  		private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            //獲取索引值
            int i = key.threadLocalHashCode & (len-1);

            //遍歷tab 如果已經存在則更新值
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

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

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //如果上面沒有遍歷成功則創建新值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //滿足條件數組擴容x2
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

我們看一下獲取索引值的代碼,int i = key.threadLocalHashCode & (len-1);,其中 threadLocalHashCode 位於 ThreadLocal 中,相關代碼如下:

public class ThreadLocal<T> {
	···
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
    	//自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
	···

}

在對 ThreadLocal 進行實例化時,會使 threadLocalHashCode 值自增一次,增量爲 0x61c88647。爲什麼是 0x61c88647 而不是其他數呢?其實 0x61c88647 是斐波那契散列乘數,其優點爲通過它散列出來的結果分佈會比較均勻,可以很大程度上避免哈希衝突。

ThreadLocalMap 的底層實現

ThreadLocalMap 的底層是一個 HashMap 哈希表。核心元素包括:

  1. Entry[] table:必要時需要擴容,長度必須是2的 n 次方
  2. int size:實際存儲鍵值對元素個數
  3. int threshold:下一次擴容時的閾值,threshold 爲 table 長度的 2/3。當 size >= threshold 時,遍歷 table 並刪除 key 爲 null 的元素,如果刪除後 size >= threshold * 3/4 時,需要對 table 進行擴容

由 Entry[] table 可見,哈希表存儲的核心元素是 Entry,Entry 包括:

  1. ThreadLocal<?> k:當前存儲的 ThreadLocal 實例對象
  2. Object value:當前 ThreadLocal 儲存的值 value

在上面代碼中可見,Entry 繼承了弱引用 WeakReference。在使用 ThreadLocalMap 時,如果 key 爲 null,便說明該 key 對應的 ThreadLocal 不再被引用,需要將其從 ThreadLocalMap 中移除。

爲什麼 ThreadLocalMap 使用 ThreadLocal 的弱引用作爲 key?

如果一個 ThreadLocal 沒有外部強引用來引用它,那麼在 GC 的時候,這個 ThreadLocal 會被回收,從而導致 ThreadLocalMap 出現 key 爲 null 的 Entry,且無法訪問這些 key 爲 null 的 Entry 的 value。只要當前線程不死亡,這些 value 就會一直存在引用,永遠無法被回收,從而造成內存泄漏。

事實上,在 ThreadLocalMap 的設計中已經考慮到這種情況,也加上了一些防護措施:在 ThreadLocal 的 get,set,remove 等方法調用的時候都會清除線程 ThreadLocalMap 裏所有 key 爲 null 的 value,但這些被動的預防措施並不能保證不會內存泄漏。

如果 key 使用強引用,在引用的 ThreadLocal 的對象被回收之後,ThreadLocalMap 還持有 ThreadLocal 的強引用,只要沒有手動刪除,ThreadLocal 就不會被回收,導致 Entry 內存泄漏。

如果 key 使用弱引用,在引用的 ThreadLocal 的對象被回收之後,ThreadLocalMap 持有 ThreadLocal 的弱引用,即使沒有手動刪除,ThreadLocal 也會被回收。value 在下一次 ThreadLocalMap 調用 get,set,remove 等方法的時候會被清除。

比較上面兩種情況,可以發現由於 ThreadLocalMap 的生命週期跟 Thread 一樣,如果沒有手動刪除對應 key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用 ThreadLocal 不會內存泄漏,對應的 value 在下一次 ThreadLocalMap 調用 get,set,remove 等方法的時候會被清除。

綜上所述,ThreadLocal 內存泄漏的根源是:由於 ThreadLocalMap 的生命週期跟 Thread 一樣,如果沒有手動刪除對應 key 就會導致內存泄漏,而不是因爲弱引用。

那麼我們應該如何避免內存泄漏呢?在每次使用完 ThreadLocal 之後,都調用它的 remove 方法,清除數據,就像每次使用完鎖就解鎖一樣。

4 get 方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

setInitialValue 方法用於初始化操作。

getEntry 方法實現如下:

private Entry getEntry(ThreadLocal<?> key) {
       int i = key.threadLocalHashCode & (table.length - 1);
       Entry e = table[i];
       if (e != null && e.get() == key)
            return e;
       else
            return getEntryAfterMiss(key, i, e);
}

無非是通過計算出索引值到數組相應的地址去尋找數據罷了。

5 remove 方法

	public void remove() {
         //獲取ThreadLocalMap對象
         ThreadLocalMap m = getMap(Thread.currentThread());
         //如果map存在
         if (m != null)
             //以當前ThreadLocal爲key刪除對應的實體entry
             m.remove(this);
     }

該方法可以刪除 ThreadLocal 中對應當前線程已存儲的值。

6 ThreadLocal 的應用場景

Spring 使用 ThreadLocal 來解決線程安全問題。

一般情況下,只有無狀態的 Bean 纔可以在多線程環境下共享,在 Spring 中,絕大部分 Bean 都可以聲明爲 singleton (單例)作用域。就是因爲 Spring 對一些 Bean 中非線程安全狀態採用 ThreadLocal 進行處理,使它們成爲線程安全的狀態。

7 總結

對於同一 ThreadLocal 來說,在不同線程之間訪問的是不同的 table 數組,而且這些線程的 table 數組是互相獨立的;對於同一線程的不同 ThreadLocal 來說,它們共享一個 table 數組,每個 ThreadLocal 實例在數組中的位置是不同的。

ThreadLocal 與 Synchronized 均可解決多線程併發訪問變量問題,它們區別在於:

  1. Synchronized 犧牲了時間來解決訪問衝突,採取了線程阻塞的方法,提供一份變量,讓不同的線程排隊訪問
  2. ThreadLocal 犧牲了空間來解決訪問衝突,線程隔離,爲每一個線程都提供一份變量的副本,從而實現同時訪問而互不影響

參考:ThreadLocal
JAVA併發-自問自答學ThreadLocal

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章