一文搞懂 ThreadLocal 原理

當多線程訪問共享可變數據時,涉及到線程間同步的問題,並不是所有時候,都要用到共享數據,所以就需要線程封閉出場了。

數據都被封閉在各自的線程之中,就不需要同步,這種通過將數據封閉在線程中而避免使用同步的技術稱為線程封閉

本文主要介紹線程封閉中的其中一種體現:ThreadLocal,將會介紹什麼是 ThreadLocal;從 ThreadLocal 源碼角度分析,最後介紹 ThreadLocal 的應用場景。

什麼是 ThreadLocal?

ThreadLocal 是 Java 裡一種特殊變量,它是一個線程級別變量,每個線程都有一個 ThreadLocal 就是每個線程都擁有了自己獨立的一個變量,競態條件被徹底消除了,在併發模式下是絕對安全的變量。

可以通過 ThreadLocal value = new ThreadLocal(); 來使用。

會自動在每一個線程上創建一個 T 的副本,副本之間彼此獨立,互不影響,可以用 ThreadLocal 存儲一些參數,以便在線程中多個方法中使用,用以代替方法傳參的做法。

下面通過例子來了解下 ThreadLocal:

<code>public class ThreadLocalDemo {
    /**
     * ThreadLocal變量,每個線程都有一個副本,互不干擾
     */
    public static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        new ThreadLocalDemo().threadLocalTest();
    }

    public void threadLocalTest() throws Exception {
        // 主線程設置值
        THREAD_LOCAL.set("wupx");
        String v = THREAD_LOCAL.get();
        System.out.println("Thread-0線程執行之前," + Thread.currentThread().getName() + "線程取到的值:" + v);

        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = THREAD_LOCAL.get();
                System.out.println(Thread.currentThread().getName() + "線程取到的值:" + v);
                // 設置 threadLocal
                THREAD_LOCAL.set("huxy");
                v = THREAD_LOCAL.get();
                System.out.println("重新設置之後," + Thread.currentThread().getName() + "線程取到的值為:" + v);
                System.out.println(Thread.currentThread().getName() + "線程執行結束");
            }
        }).start();
        // 等待所有線程執行結束
        Thread.sleep(3000L);
        v = THREAD_LOCAL.get();
        System.out.println("Thread-0線程執行之後," + Thread.currentThread().getName() + "線程取到的值:" + v);
    }
}
/<code> 

首先通過 static final 定義了一個 THREAD_LOCAL 變量,其中 static 是為了確保全局只有一個保存 String 對象的 ThreadLocal 實例;final 確保 ThreadLocal 的實例不可更改,防止被意外改變,導致放入的值和取出來的不一致,另外還能防止 ThreadLocal 的內存洩漏。上面的例子是演示在不同的線程中獲取它會得到不同的結果,運行結果如下:

<code>Thread-0線程執行之前,main線程取到的值:wupx
Thread-0線程取到的值:null
重新設置之後Thread-0線程取到的值為:huxy
Thread-0線程執行結束
Thread-0線程執行之後,main線程取到的值:wupx
/<code>

首先在 Thread-0 線程執行之前,先給 THREAD_LOCAL 設置為 wupx,然後可以取到這個值,然後通過創建一個新的線程以後去取這個值,發現新線程取到的為 null,意外著這個變量在不同線程中取到的值是不同的,不同線程之間對於 ThreadLocal 會有對應的副本,接著在線程 Thread-0 中執行對 THREAD_LOCAL 的修改,將值改為 huxy,可以發現線程 Thread-0 獲取的值變為了 huxy,主線程依然會讀取到屬於它的副本數據 wupx,這就是線程的封閉。

看到這裡,我相信大家一定會好奇 ThreadLocal 是如何做到多個線程對同一對象 set 操作,但是 get 獲取的值還都是每個線程 set 的值呢,接下來就讓我們進入源碼解析環節:

ThreadLocal 源碼解析

首先看下 ThreadLocal 都有哪些重要屬性:

<code>// 當前 ThreadLocal 的 hashCode,由 nextHashCode() 計算而來,用於計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔數,主要與斐波那契散列法以及黃金分割有關
private static final int HASH_INCREMENT = 0x61c88647;
// 返回計算出的下一個哈希值,其值為 i * HASH_INCREMENT,其中 i 代表調用次數
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 保證了在一臺機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的
private static AtomicInteger nextHashCode = new AtomicInteger();
/<code>

其中的 HASH_INCREMENT 也不是隨便取的,它轉化為十進制是 1640531527,2654435769 轉換成 int 類型就是 -1640531527,2654435769 等於 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黃金分割數,近似為 0.618,也就是說 0x61c88647 理解為一個黃金分割數乘以 2 的 32 次方,它可以保證 nextHashCode 生成的哈希值,均勻的分佈在 2 的冪次方上,且小於 2 的 32 次方。

下面是 javaspecialists 中一篇文章對它的介紹:

This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

下面用例子來證明下:

<code>private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) throws Exception {
    int n = 5;
    int max = 2 << (n - 1);
    for (int i = 0; i < max; i++) {
        System.out.print(i * HASH_INCREMENT & (max - 1));
        System.out.print(" ");

    }
}
/<code>

運行結果為: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

可以發現元素索引值完美的散列在數組當中,並沒有出現衝突。

ThreadLocalMap

除了上述屬性外,還有一個重要的屬性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的靜態內部類,當一個線程有多個 ThreadLocal 時,需要一個容器來管理多個 ThreadLocal,ThreadLocalMap 的作用就是管理線程中多個 ThreadLocal,源碼如下:

<code>static class ThreadLocalMap {
 /**
  * 鍵值對實體的存儲結構
  */
 static class Entry extends WeakReference> {
  // 當前線程關聯的 value,這個 value 並沒有用弱引用追蹤
  Object value;

  /**
   * 構造鍵值對
   *
   * @param k k 作 key,作為 key 的 ThreadLocal 會被包裝為一個弱引用
   * @param v v 作 value
   */
  Entry(ThreadLocal> k, Object v) {
   super(k);
   value = v;
  }
 }

 // 初始容量,必須為 2 的冪
 private static final int INITIAL_CAPACITY = 16;

 // 存儲 ThreadLocal 的鍵值對實體數組,長度必須為 2 的冪
 private Entry[] table;

 // ThreadLocalMap 元素數量
 private int size = 0;

 // 擴容的閾值,默認是數組大小的三分之二
 private int threshold;
}
/<code>

從源碼中看到 ThreadLocalMap 其實就是一個簡單的 Map 結構,底層是數組,有初始化大小,也有擴容閾值大小,數組的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。ThreadLocalMap 解決 hash 衝突的方式採用的是線性探測法,如果發生衝突會繼續尋找下一個空的位置。

這樣的就有可能會發生內存洩漏的問題,下面讓我們進行分析:

ThreadLocal 內存洩漏

ThreadLocal 在沒有外部強引用時,發生 GC 時會被回收,那麼 ThreadLocalMap 中保存的 key 值就變成了 null,而 Entry 又被 threadLocalMap 對象引用,threadLocalMap 對象又被 Thread 對象所引用,那麼當 Thread 一直不終結的話,value 對象就會一直存在於內存中,也就導致了內存洩漏,直至 Thread 被銷燬後,才會被回收。

那麼如何避免內存洩漏呢?

在使用完 ThreadLocal 變量後,需要我們手動 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持對 value 的強引用,導致 value 不能被回收,其中 remove 源碼如下所示:

<code>/**
 * 清理當前 ThreadLocal 對象關聯的鍵值對
 */
public void remove() {
 // 返回當前線程持有的 map
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null) {
  // 從 map 中清理當前 ThreadLocal 對象關聯的鍵值對
  m.remove(this);
 }
}
/<code>

remove 方法的時序圖如下所示:

一文搞懂 ThreadLocal 原理

remove 方法是先獲取到當前線程的 ThreadLocalMap,並且調用了它的 remove 方法,從 map 中清理當前 ThreadLocal 對象關聯的鍵值對,這樣 value 就可以被 GC 回收了。

那麼 ThreadLocal 是如何實現線程隔離的呢?

ThreadLocal 的 set 方法

我們先去看下 ThreadLocal 的 set 方法,源碼如下:

<code>/**
 * 為當前 ThreadLocal 對象關聯 value 值
 *
 * @param value 要存儲在此線程的線程副本的值
 */
public void set(T value) {
 // 返回當前ThreadLocal所在的線程
 Thread t = Thread.currentThread();
 // 返回當前線程持有的map
 ThreadLocalMap map = getMap(t);
 if (map != null) {
  // 如果 ThreadLocalMap 不為空,則直接存儲鍵值對
  map.set(this, value);
 } else {
  // 否則,需要為當前線程初始化 ThreadLocalMap,並存儲鍵值對 
  createMap(t, value);
 }
}
/<code>

set 方法的作用是把我們想要存儲的 value 給保存進去。set 方法的流程主要是:

  • 先獲取到當前線程的引用
  • 利用這個引用來獲取到 ThreadLocalMap
  • 如果 map 為空,則去創建一個 ThreadLocalMap
  • 如果 map 不為空,就利用 ThreadLocalMap 的 set 方法將 value 添加到 map 中

set 方法的時序圖如下所示:

一文搞懂 ThreadLocal 原理

其中 map 就是我們上面講到的 ThreadLocalMap,可以看到它是通過當前線程對象獲取到的 ThreadLocalMap,接下來我們看 getMap方法的源代碼:

<code>/**
 * 返回當前線程 thread 持有的 ThreadLocalMap
 *
 * @param t 當前線程
 * @return ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
 return t.threadLocals;
}
/<code>

getMap 方法的作用主要是獲取當前線程內的 ThreadLocalMap 對象,原來這個 ThreadLocalMap 是線程的一個屬性,下面讓我們看看 Thread 中的相關代碼:

<code>/**
 * ThreadLocal 的 ThreadLocalMap 是線程的一個屬性,所以在多線程環境下 threadLocals 是線程安全的
 */
ThreadLocal.ThreadLocalMap threadLocals = null;
/<code>

可以看出每個線程都有 ThreadLocalMap 對象,被命名為 threadLocals,默認為 null,所以每個線程的 ThreadLocals 都是隔離獨享的。

調用 ThreadLocalMap.set() 時,會把當前 threadLocal 對象作為 key,想要保存的對象作為 value,存入 map。

其中 ThreadLocalMap.set() 的源碼如下:

<code>/**
 * 在 map 中存儲鍵值對
 *
 * @param key   threadLocal
 * @param value 要設置的 value 值
 */
private void set(ThreadLocal> key, Object value) {
 Entry[] tab = table;
 int len = tab.length;
 // 計算 key 在數組中的下標
 int i = key.threadLocalHashCode & (len - 1);
 // 遍歷一段連續的元素,以查找匹配的 ThreadLocal 對象
 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
  // 獲取該哈希值處的ThreadLocal對象
  ThreadLocal> k = e.get();

  // 鍵值ThreadLocal匹配,直接更改map中的value
  if (k == key) {
   e.value = value;
   return;
  }

  // 若 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
  if (k == null) {
   replaceStaleEntry(key, value, i);
   return;
  }
 }

 // 直到遇見了空槽也沒找到匹配的ThreadLocal對象,那麼在此空槽處安排ThreadLocal對象和緩存的value
 tab[i] = new Entry(key, value);
 int sz = ++size;
 // 如果沒有元素被清理,那麼就要檢查當前元素數量是否超過了容量闕值(數組大小的三分之二),以便決定是否擴容
 if (!cleanSomeSlots(i, sz) && sz >= threshold) {
  // 擴容的過程也是對所有的 key 重新哈希的過程
  rehash();
 }
}
/<code> 

相信到這裡,大家應該對 Thread、ThreadLocal 以及 ThreadLocalMap 的關係有了進一步的理解,下圖為三者之間的關係:

一文搞懂 ThreadLocal 原理

ThreadLocal 的 get 方法

瞭解完 set 方法後,讓我們看下 get 方法,源碼如下:

<code>/**
 * 返回當前 ThreadLocal 對象關聯的值
 *
 * @return
 */
public T get() {
 // 返回當前 ThreadLocal 所在的線程
 Thread t = Thread.currentThread();
 // 從線程中拿到 ThreadLocalMap
 ThreadLocalMap map = getMap(t);
 if (map != null) {
  // 從 map 中拿到 entry
  ThreadLocalMap.Entry e = map.getEntry(this);
  // 如果不為空,讀取當前 ThreadLocal 中保存的值
  if (e != null) {
   @SuppressWarnings("unchecked")
   T result = (T) e.value;
   return result;
  }
 }
 // 若 map 為空,則對當前線程的 ThreadLocal 進行初始化,最後返回當前的 ThreadLocal 對象關聯的初值,即 value
 return setInitialValue();
}
/<code> 

get 方法的主要流程為:

  • 先獲取到當前線程的引用
  • 獲取當前線程內部的 ThreadLocalMap
  • 如果 map 存在,則獲取當前 ThreadLocal 對應的 value 值
  • 如果 map 不存在或者找不到 value 值,則調用 setInitialValue() 進行初始化

get 方法的時序圖如下所示:

一文搞懂 ThreadLocal 原理

其中每個 Thread 的 ThreadLocalMap 以 threadLocal 作為 key,保存自己線程的 value 副本,也就是保存在每個線程中,並沒有保存在 ThreadLocal 對象中。

其中 ThreadLocalMap.getEntry() 方法的源碼如下:

<code>/**
 * 返回 key 關聯的鍵值對實體
 *
 * @param key threadLocal
 * @return
 */
private Entry getEntry(ThreadLocal> key) {
 int i = key.threadLocalHashCode & (table.length - 1);
 Entry e = table[i];
 // 若 e 不為空,並且 e 的 ThreadLocal 的內存地址和 key 相同,直接返回
 if (e != null && e.get() == key) {
  return e;
 } else {
  // 從 i 開始向後遍歷找到鍵值對實體
  return getEntryAfterMiss(key, i, e);
 }
}
/<code>

ThreadLocalMap 的 resize 方法

當 ThreadLocalMap 中的 ThreadLocal 的個數超過容量閾值時,ThreadLocalMap 就要開始擴容了,我們一起來看下 resize 的源代碼:

<code>/**
 * 擴容,重新計算索引,標記垃圾值,方便 GC 回收
 */
private void resize() {
 Entry[] oldTab = table;
 int oldLen = oldTab.length;
 int newLen = oldLen * 2;
 // 新建一個數組,按照2倍長度擴容
 Entry[] newTab = new Entry[newLen];
 int count = 0;

 // 將舊數組的值拷貝到新數組上
 for (int j = 0; j < oldLen; ++j) {
  Entry e = oldTab[j];
  if (e != null) {
   ThreadLocal> k = e.get();
   // 若有垃圾值,則標記清理該元素的引用,以便GC回收
   if (k == null) {
    e.value = null;
   } else {
    // 計算 ThreadLocal 在新數組中的位置
    int h = k.threadLocalHashCode & (newLen - 1);
    // 如果發生衝突,使用線性探測往後尋找合適的位置
    while (newTab[h] != null) {
     h = nextIndex(h, newLen);
    }
    newTab[h] = e;
    count++;
   }
  }
 }
 // 設置新的擴容閾值,為數組長度的三分之二
 setThreshold(newLen);
 size = count;
 table = newTab;
}
/<code>

resize 方法主要是進行擴容,同時會將垃圾值標記方便 GC 回收,擴容後數組大小是原來數組的兩倍。

ThreadLocal 應用場景

ThreadLocal 的特性也導致了應用場景比較廣泛,主要的應用場景如下:

  • 線程間數據隔離,各線程的 ThreadLocal 互不影響
  • 方便同一個線程使用某一對象,避免不必要的參數傳遞
  • 全鏈路追蹤中的 traceId 或者流程引擎中上下文的傳遞一般採用 ThreadLocal
  • Spring 事務管理器採用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的實現使用了 ThreadLocal

總結

本文主要從源碼的角度解析了 ThreadLocal,並分析了發生內存洩漏的原因,最後對它的應用場景進行了簡單介紹。

歡迎留言交流討論,原創不易,覺得文章不錯,請在看轉發支持一下。

更詳細的源碼解析可以點擊鏈接查看:
https://github.com/wupeixuan/JDKSourceCode1.8

參考

《Java併發編程實戰》

https://www.javaspecialists.eu/archive/Issue164.html

https://mp.weixin.qq.com/s/vURwBPgVuv4yGT1PeEHxZQ

Java併發編程學習寶典

面試官系統精講Java源碼及大廠真題

Java 併發面試 78 講


分享到:


相關文章: