一文詳解 JDK 中的 ThreadLocal(全網最透徹易懂)

1 ThreadLocal 概述

ThreadLocal 提供了一種變量與線程綁定的機制,通常把這種機制稱爲線程本地變量,在線程調用棧(方法調用鏈)的入口或中間可以讓一些重要的變量與線程綁定,在後繼的調用棧(方法調用鏈)可使用該變量。這個特性使得 ThreadLocal 適用於方法調用鏈上參數透傳,如 APM、日誌、權限框架中透傳上游重要的參數到下游。

ThreadLocal 不支持不同線程間變量的透傳,即如果在線程 A 中設置了一個變量 ThreadLocalA 其中存儲的值爲 AA,在線程 B 中想拿到 ThreadLocalA 中存儲的 AA 是拿不到的(結果將是 null)。這也導致了使用線程池的線程,不能通過 ThreadLocal線程本地變量傳遞到線程池中供其使用(線程池中執行任務的線程通常不是提交任務對應的線程),同樣類似線程間的異步操作 ThreadLocal 也不支持。線程間線程本地變量的透傳可以通過阿里開源的 TrasmittableThreadLocal 來實現。當子線程要獲取父線程中的線程本地變量,通過 ThreadLocal 同樣無法獲取,但可能通過其子類 InheritableThreadLocal 實現。

在高併發場景下,JDK 自帶的 ThreadLocal 性能不如 Netty 框架中實現的 FastThreadLocalFastThreadLocal 獲取其中保證的變量值時,使用內部的 index 變量便可定位對應變量值,而不用像 ThreadLocal 那樣通過開放地址法去定位對應變量值。

ThreadLocal 的線程本地變量機制實際是通過 Thread 類中的 ThreadLocalMap 成員變量實現的。每一個 Thread 實例都維護了一個 ThreadLocalMap 實例。ThreadLocalMap 通過開放地址法實現,其 KeyThreadLocalValueThreadLocal 中保存的變量。其開放地址法底層對應的數組爲 Entry 數組Entry 類持有了 ThreadLocalWeakReference,持有了 ThreadLocal 中保存變量的強引用,這導致了不恰當的使用 ThreadLocal 容易引發內存泄漏問題。

2 源碼分析

注意:下面的 ThreadLocal 的源碼分析基本 JDK8

2.1 引用類型
public class ThreadLocal<T> {
  ...
}

**        ThreadLocal** 類本身比較簡單其支持泛型,一共沒有幾行代碼,其 set 與 get 方法上覆雜的處理最終都到了內部類類 **ThreadLocalMap** 上,並且 **ThreadLocalMap** 使用 **WeakReference** 所涉及的東西比較多。**Reference** 的源碼分析參考之前的文章 Java Reference 核心原理分析

這裏先簡單介紹一下 java 語言中的引用類型。java 語言中引用類型分爲強引用、軟引用(SoftReference)、弱引用(WeakReference)、虛引用(PhantomReference)

強引用

通常代碼中看到的變量引用關係如下面的 threadLocalData,variable 對對象的引用都是強引用。

ThreadLocal<Integer> threadLocalData = ThreadLocal.withInitial(() -> -1);
String variable = "123";

軟引用 (SoftReference)

垃圾回收器會根據內存需求酌情回收軟引用指向的對象。普通的 GC 並不會回收軟引用,只有在即將 OOM 的時候 (也就是最後一次 Full GC) 如果被引用的對象只有 SoftReference 指向的引用,纔會回收。如下 SoftValueReference 便持有其值 V 的軟引用。

static class SoftValueReference<K, V> extends
  SoftReference<V> implements ValueReference<K, V> {
  final ReferenceEntry<K, V> entry;
  SoftValueReference(ReferenceQueue<V> queue, V referent,                     ReferenceEntry<K,V> entry) {
     super(referent, queue);
     this.entry = entry;
  }
}

弱引用(WeakReference)

當發生 GC 時,如果當前對象只有 WeakReference 類型的引用,則會被 GC 給回收掉。如下 ThreadLocalMap map 中的 Entry 便持有 ThreadLocal 的軟引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
 Object value;
 Entry(ThreadLocal<?> k, Object v) {
   super(k);
   value = v;
 }
}

虛引用(PhantomReference)

他是一種特殊的引用類型,不能通過虛引用獲取到其關聯的對象,但當 GC 時如果其引用的對象被回收,這個事件程序可以感知,這樣我們可以做相應的處理。其最常用的場景是 GC 回收 DirectByteBuffer 對象時,利用 Cleaner 調用 Unsafe 類回收其對應的堆外內存。具體源碼分析可參考 Java Reference 核心原理分析

public class Cleaner extends PhantomReference<Object> {
  ...
}

        前面簡單地介紹了對象的引用類型,GC 決定一個對象是否能被回收與當對象具有的引用類型有很大的關係。一般會從 GC Root 開始向下搜索,如果對象與 GC Root 之間存在直接或間接的強引用有關係,則當前對象強可到達,不能被回收。如對象與 GC Root 之間只存在直接或間接的軟引用有關係,則當前對象軟可到達,GC 時會視當前內存情況確定是否回收該對象。如對象與 GC Root 之間只存在直接或間接的弱引用有關係,則當前對象弱可到達,GC 時不管內存如何該對象將都被回收,但在 GC 前可以再次強引用該對象達到讓該對象不被回收。如對象與 GC Root 之間只存在直接或間接的虛引用有關係,則當前對象虛可到達,GC 時該對象將被回收。

上面 ObjectA、ObjectB、ObjectC、ObjectD、ObjectE、ObjectF、ObjectG 7 個對象。

2.2 ThreadLocal#get、#set、#remove 方法

ThreadLocal#get 整體邏輯相對簡單,具體分析見下面代碼的註解。當未給 ThreadLocal 設置值時,get 方法將調用 setInitialValue 方法返回 initialValue 方法指定的 ThreadLocal 的初始值,默認 ThreadLocalinitialValue 爲 null。ThreadLocal#get 方法實際是用其自身作爲 Key 通過開放尋址法在其所屬線程的 ThreadLocalMap 上查找對應的 value(與線程綁定的變量)。

public T get() {
  //獲取當前線程
  Thread t = Thread.currentThread();
  //從線程中獲取成員變量ThreadLocalMap,此時ThreadLocalMap可能沒被初始化
  ThreadLocalMap map = getMap(t);
  //當前線程成員變量ThreadLocalMap已初始化
  if(map != null) {
     //用this(當前ThreadLocal)爲Key從ThreadLocalMap的Entry數組中找到對應的value
     ThreadLocalMap.Entry e = map.getEntry(this);
     //entry存在直接從其中拿出對應的值,然後返回
     if (e != null) {
       @SuppressWarnings("unchecked")
       T result = (T)e.value;
       return result;
     }
  }
  //當前線程成員變量ThreadLocalMap未初始化或者在其Entry數組中未找到對應的value, 設置value值
  returnsetInitialValue();
}
//設置ThreadLocal初始化值
private T setInitialValue() {
  //獲取ThreadLocal的初始化值
  T value= initialValue();
  //獲取當前線程
  Thread t = Thread.currentThread();
  //從線程中獲取成員變量ThreadLocalMap,此時ThreadLocalMap可能沒被初始化
  ThreadLocalMap map = getMap(t);
  if(map != null)
     //在ThreadLocalMap上設置當前ThreadLocal對應值
     map.set(this, value);
  else
     //爲當前線程設置成員變量ThreadLocalMap值
     createMap(t, value);
  returnvalue;
}
//爲線程成員變量ThreadLocalMap設置值,同時將當前的TheadLocal對的值綁定到ThreadLocalMap上
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//默認ThreadLocal的initialValue爲null,創建ThreadLocal對象時可以覆蓋該方法指定初始值
protected T initialValue() {
   return null;
}

ThreadLocal#set 整體邏輯相對簡單,具體分析見下面代碼的註解。ThreadLocal#set 方法實際是用其自身作爲 Key 通過開放尋址法在其所屬線程的 ThreadLocalMap 上將 value 與線程綁定。

public void set(T value) {
   //獲取當前線程
   Thread t = Thread.currentThread();
   //從線程中獲取成員變量ThreadLocalMap,此時ThreadLocalMap可能沒被初始化
   ThreadLocalMap map = getMap(t);
   if (map != null)
    //在ThreadLocalMap上設置當前ThreadLocal對應值
       map.set(this, value);
   else
    //爲當前線程設置成員變量ThreadLocalMap值
       createMap(t, value);
}

ThreadLocal#remove 整體邏輯相對簡單,ThreadLocal#remove 方法實際是用其自身作爲 Key 通過開放尋址法,將當前 ThreadLocal 與其所屬線程解綁。

public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
   //將當前ThreadLocal與其對應的線程解綁
   m.remove(this);
}
2.3 ThreadLocalMap 類

從上面的 ThreadLocal#get、#set、#remove 方法分析可以看到,最終這些操作都是在 ThreadLocalMap 上完成。文中最開始已介紹過 ThreadLocalMap 實際是通過開放地址法實現的,其內部的 Entry 數據組 table 用於存儲 ThreadLocal 與保存在 ThreadLocal 的值,最終實現 ThreadLocal 內保存的值與線程綁定。

static class ThreadLocalMap {
   // map的初始容量
   private static final int INITIAL_CAPACITY = 16;
   // Entry數組
   private Entry[] table;
   // Entry元素個數
   private int size = 0;
   // 閾值,用於擴容降低開放尋址時的衝突
   private int threshold;
}
//ThreadLocalMap中的Entry持有ThreadLocal的弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
   // ThreadLocal上關聯的值
   Object value;
   Entry(ThreadLocal<?> k, Object v) {
     super(k);
     value = v;
  }
}

ThreadLocalMap 構造函數

//創建ThreadLocalMap並在其上綁定第一個線程局部變量
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
   table = new Entry[INITIAL_CAPACITY];
    //取模獲取引用ThreadLocal的Entry的下標
   int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
   table[i] = new Entry(firstKey, firstValue);
   size = 1;
   //設置擴容的閾值
   setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap#getEntry 方法以 ThreadLocal 作爲 KeyThreadLocalMap 採用開放尋址法Entry 數組中尋找引用 ThreadLocal 對應的 EntryThreadLocal#get 方法會調用該方法,獲取保存在 Entry 內的 Value,即實際與當前線程綁定的變量值

private Entry getEntry(ThreadLocal<?> key) {
   //取模試探性獲取引用ThreadLocal的Entry的下標
   int i = key.threadLocalHashCode & (table.length - 1);
   Entry e = table[i];
   if (e != null && e.get() == key)
       return e;
   else
    //發生衝突,採用開放尋址法從Entry數組中找引用ThreadLocal的Entry
       return getEntryAfterMiss(key, i, e);
}
//發生衝突,採用開放尋址法從Entry數組中找引用ThreadLocal的Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  Entry[] tab = table;
  int len = tab.length;
  while (e != null) {
    ThreadLocal<?> k = e.get();
    if (k == key) // 找到引用ThreadLocal的Entry返回
      return e;
    if (k == null) //找到之前引用ThreadLocal的Entry,但ThreadLocal的引用已被remove方法清理掉或被GC清理掉
      //通過重新哈希,清理已被remove或被GC回收的ThreadLocal上關聯的value
      expungeStaleEntry(i);
    else // 繼續向前尋找引用ThreadLocal的Entry
      i = nextIndex(i, len);
    e = tab[i];
  }
  return null;
}
//開放尋址,獲取元素下標
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}

ThreadLocalMap#set 方法以 ThreadLocal 作爲 Key 採用開放尋址法將 value 與其所屬線程綁定。

private void set(ThreadLocal<?> key, Object value) {
   Entry[] tab = table;
   int len = tab.length;
   //取模試探性獲取引用ThreadLocal的Entry的下標
   int i = key.threadLocalHashCode & (len-1);
   //嘗試用開放尋址的方法在Entry數組中找到之前引用ThreadLocal的Entry
   for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
       ThreadLocal<?> k = e.get();
       //找到之前引用ThreadLocal的Entry,重置value值並返回
       if (k == key) {
           e.value = value;
           return;
        }
       //找到之前引用ThreadLocal的Entry,但ThreadLocal的引用已被remove方法清理掉或被GC清理掉
       if (k == null) {
        //重新將引用ThreadLocal的Entry放入到Entry數組中並清理已被remove或被GC回收的ThreadLocal上關聯的value
           replaceStaleEntry(key, value, i);
           return;
        }
   }
   //未找到之前引用ThreadLocal的Entry,創建Entry並放入Entry數組
   tab[i] = new Entry(key, value);
   int sz = ++size;
   //清理槽位失敗且Entry數組長度超過閾值,重新rehash對Entry數組擴容
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
       rehash();
}

ThreadLocalMap#remove 方法以 ThreadLocal 作爲 Key 採用開放尋址法將 value 與其所屬線程綁定。

private void remove(ThreadLocal<?> key) {
  Entry[] tab = table;
  intlen = tab.length;
  //取模試探性獲取引用ThreadLocal的Entry的下標
  inti = key.threadLocalHashCode & (len-1);
  //用開放尋址法找到引用ThreadLocal的Entry,並將其清除
  for(Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
      if (e.get() == key) {
         //清除Entry上的弱引用ThreadLocal
         e.clear();
         //通過重新哈希,清理已被remove或被GC回收的ThreadLocal上關聯的value
         expungeStaleEntry(i);
         return;
    }
  }
}

ThreadLocalMap#getEntry、#set、#remove 方法內部最終都會嘗試調用 expungeStaleEntry 方法。

expungeStaleEntry 通過重新哈希,清理已被 remove 或被 GC 回收的 ThreadLocal 上關聯的 value, 該方法可以保證由於只與 Entry 存在弱引用關係的 ThreadLocalGC 回收後,Entry 上的 Value(與 ThreadLocal 上關聯的 value)能被及時清理,而不會因爲 Entry 上的 Value 一直存在強引用最終導致的內存泄漏。實際 ThreadLocal#set、#get、#remove 方法最終都會調用 expungeStaleEntry 方法。

// 通過重新哈希,清理已被remove或被GC回收的ThreadLocal上關聯的value
private int expungeStaleEntry(int staleSlot) {
  Entry[] tab = table;
  intlen = tab.length;
  // 清理當前位置上已被remove或被GC回收的ThreadLocal上關聯的value
  tab[staleSlot].value= null;
  // 清理當前位置上的Entry
  tab[staleSlot] = null;
  size--;
  // 向後重新哈希,直到對應位置上沒有Entry。
  Entry e;
  inti;
  for(i = nextIndex(staleSlot, len); (e = tab[i]) != null;i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
       //清理當前位置上已被remove或被GC回收的ThreadLocal上關聯的value
    if (k == null) {
         e.value = null;
         tab[i] = null;
         size--;
    } else {
       //用開放尋址法,重新調整當前Entry在數組中的位置
         int h = k.threadLocalHashCode & (len - 1);
         //ThreadLocal初始位置h與i不一致,嘗試將其放回始位置或開放尋址法後的位置
         if (h != i) {
           tab[i] = null;
           while (tab[h] != null)
             h = nextIndex(h, len);
           tab[h] = e;
        }
     }
  }
 return i;
}

3 使用場景

本文開篇已介紹過,ThreadLocal 適用方法調用鏈上參數的透傳,但要注意是同線程間,但不適合異步方法調用的場景。對於異步方法調用,想做參數的透傳可以採用阿里開源的 TransmittableThreadLocal。權限、日誌、事務等框架都可以利用 ThreadLocal 透傳重要參數。

在使用 Spring Security 時,當用戶認證通過後,業務邏輯處理中經常會去獲取用戶認證時的用戶信息,通過會將這個功能封裝在工具類中,如下的 SecurityUtils#getAuthUser 方法用於獲取用戶的認證信息,如果用戶認證過返回用戶信息,否則返回 null。業務邏輯中直接通過 SecurityUtils#getAuthUser 方法便能方便的獲取用戶的認證信息。

public class SecurityUtils {
 public static User getAuthUser() {
   try {
     // 通過Srping Security 上下文獲取用戶的認證信息
     Authentication auth = SecurityContextHolder.getContext().getAuthentication();
     return  Objects.nonNull(auth) ? (User) auth.getPrincipal() : null;
   } catch (Exception e) {
     log.error("Get user auth info fail", e);
     throw new CustomException("獲取用戶信息異常", HttpStatus.UNAUTHORIZED.value());
   }
 }
}

SecurityContextHolder 採用策略模式實現,實默認策略便是通過 ThreadLocal 存儲 Spring Security 的上下文信息,這個上下文信息中包括認證信息。ThreadLocalSecurityContextHolderStrategy 源碼如下。

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
   //採用ThreadLocal存儲線程上下文信息
  private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
  public void clearContext() {
    contextHolder.remove();
  }
  public SecurityContext getContext() {
     //從ThreadLocal獲取線程上下文信息
    SecurityContext ctx = contextHolder.get();
    if (ctx == null) {
      ctx = createEmptyContext();
      contextHolder.set(ctx);
    }
    return ctx;
  }
  public void setContext(SecurityContext context) {
    Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
    contextHolder.set(context);
  }
  ...
}

4 內存泄漏問題分析

4.1 內存漏泄示例

下面通過代碼模擬 ThreadLocal 內存漏泄,注意運行指定的 VM 參數 -Xms 大於 50MB。

public class ThreadLocalOOM {
 public static void main(String[] args) throws InterruptedException {
   ThreadLocal tl = new CustomThreadLocal();
   tl.set(new Value50MB());
   //清理 CustomThreadLocal 對象的強引用
   tl = null;
   System.out.println("Call System.gc method to trigger Full GC");
   System.gc();
   //GC線程優先級較低,休眠3秒確保Full GC已完成
   Thread.sleep(3000);
  }
 public static class CustomThreadLocal extends ThreadLocal {
   private byte[] a = new byte[1024 * 1024 * 1];
   @Override
   public void finalize() {
     // Full GC 如果對象被回收,該方法會被調用
     System.out.println("CustomThreadLocal 1 MB finalized.");
   }
 }
 public static class Value50MB {
   private byte[] a = new byte[1024 * 1024 * 50];
   @Override
   public void finalize() {
     // Full GC 如果對象被回收,該方法會被調用
     System.out.println("Value50MB 50 MB finalized.");
   }
  }
}

控制檯輸出:

Call System.gc method to trigger Full GC****My threadLocal 1 MB finalized.

從上面的輸出可以知道,發生 Full GCCustomThreadLocal 對象對應的 1MB 內存被回收,但其上面關聯的值 Value50MB 對應的 50MB 內存並沒有被 GC 回收,出現了內存漏泄。如果應用中存在大量的這類 ThreadLocal 關聯的值沒被 GC 回收到,內存不斷漏泄,最終將導致應用程序整體 OOM,程序崩潰。

4.2 原因分析

在第 2 節部分源碼分析中,已知道實際 ThreadLocal 與其保存的值都是被放在 ThreadLocalMap 內部 Entry 對應的實例上。而 Entry 持有 ThreadLocal 的弱引用,當 ThreadLocal 只被 Entry 引用時,ThreadLocal 對象將在 GC 時被無條件的回收掉。

        上面 ThreadLocal 內存漏泄的示例中,強弱引用關係如下。

強引用:

弱引用:

tl = null;

執行 t1 = null 後,強弱引用關係如下。

強引用:

弱引用:

GC 時發現 TheadLocal 上只存在 Entry 對其的弱引用,於是無條件將 ThreadLocal 對應的內存回收,示例中是 CustomThreadLocal 對應的 1M 內存。

4.3  如何避內存漏泄

從第 2 節中的源碼分析中已知道,ThreadLocal#remove 方法實際會調用 ThreadLocalMap#expungeStaleEntry 方法,達到將已被 GC 回收的 ThreadLocal 上關聯的 Value 的強引斷開,避免內存泄漏。所以在每次使用完 ThreadLocal 後,只要調用其對應的 remove 方法,就可以避內存漏泄。

4.4 爲什麼 Entry 不強引用 ThreadLocal

ThreadLocalMap 源碼上的註解

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

可以看到主要是爲了避免大的 ThreadLocal 與長時間存活的使用場景。如果不採用 Entry 弱引用 ThreadLocalThreadLocal 將一直與 Thread 共存,這更加容易引起內存漏泄。

總結

本文首先對 ThreadLocal 做出了整體的概述,簡要地說明其使用場景、不足、業界的改進方案。然後對 ThreaLocal 的源碼進行了詳細地分析,接着介紹了其具體的使用場景、日常使用中可能會遇到的問題與問題的解決方案。

參考

TransimittableThreadLocal

Java Reference 核心原理分析

結尾

原創不易,點贊、在看、轉發是對我莫大的鼓勵,關注公衆號洞悉源碼是對我最大的支持。同時相信我會分享更多幹貨,我同你一起成長,我同你一起進步。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sMWPlj5GRUsgrnrRgNW5AQ