講透ThreadLocal

講透ThreadLocal

一. 簡介

ThreadLocal是JDK提供的一個工具類,其作用是在多線程共享資源的情況下,使每個線程持有一份該資源的副本,每個線程的副本都是獨立互不影響的。線程操作各自的副本,這樣就避免了資源競爭引發的線程安全問題。

二. 使用示例

模擬Spring中的事務管理,每個事務與當前線程綁定,不同線程的事務之間相互獨立互不影響。代碼如下:

public class TransactionManager {
  //業務線程
  private static final class BizTask implements Runnable{
    private final ThreadLocal<Transaction> transaction;

    public BizTask(){
      //創建事務,並與當前線程綁定
      transaction = ThreadLocal.withInitial(() -> {
        long id = Thread.currentThread().getId();
        return new Transaction(id);
      });
    }

    @Override
    public void run() {
      transaction.get().begin();
      System.err.println("執行業務邏輯");
      try {
        Thread.sleep(1000L);
        transaction.get().commit();
        transaction.remove();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  //事務
  private static final class Transaction {
    private long id;
    private TransactionStatus status;

    public Transaction(long id) {
      this.id = id;
    }

    public void begin(){
      status = TransactionStatus.BEGIN;
      System.err.println("開啓事務, id: " + id + ", status: " + status);
    }

    public void commit() {
      status = TransactionStatus.COMMIT;
      System.err.println("提交事務, id: " + id + ", status: " + status);
    }

    public void rollback() {
      status = TransactionStatus.ROLLBACK;
      System.err.println("回滾事務, id: " + id + ", status: " + status);
    }
  }

  //事務狀態
  private enum TransactionStatus {
    BEGIN,
    COMMIT,
    ROLLBACK,

  }

  public static void main(String[] args) throws InterruptedException {
    int threadNum = 5;
    ExecutorService executor = Executors.newFixedThreadPool(threadNum);

    //執行業務邏輯,可以看到每個線程的事務相互獨立,互不影響
    for (int i = 0; i < threadNum; i++) {
      executor.submit(new BizTask());
    }
    executor.shutdown();

  }

}

結果如下:

開啓事務, id: 17, status: BEGIN
執行業務邏輯
開啓事務, id: 14, status: BEGIN
執行業務邏輯
開啓事務, id: 13, status: BEGIN
執行業務邏輯
開啓事務, id: 16, status: BEGIN
執行業務邏輯
開啓事務, id: 15, status: BEGIN
執行業務邏輯
提交事務, id: 14, status: COMMIT
提交事務, id: 17, status: COMMIT
提交事務, id: 13, status: COMMIT
提交事務, id: 16, status: COMMIT
提交事務, id: 15, status: COMMIT

三. ThreadLocal源碼分析

ThreadLocal,線程本地變量,該變量爲每個線程私有。ThreadLocal類有一個內部類,名爲ThreadLocalMap,可以理解爲一個簡化版的HashMap。源碼如下:

static class ThreadLocalMap {
  //該Map的Entry,Key爲ThreadLocal實例,Value爲ThreadLocal對象所引用的值。
  //這裏使用了WeakReference弱引用,當Entry爲null時可以儘快被GC
  static class Entry extends WeakReference<ThreadLocal<?>> {
		//與ThreadLocal關聯的對象引用,爲當前Entry的value
    Object value;

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

  //初始容量16
  private static final int INITIAL_CAPACITY = 16;

  //使用數組保存了所有的線程本地變量
  private Entry[] table;
}

其中Entry爲ThreadLocalMap的一個內部類,與HashMap的Entry結構類似,都是key-value對的形式。它的Key爲ThreadLocal實例,Value爲ThreadLocal對象所引用的對象。ThreadLocalMap使用一個Entry[]數組保存了所有的線程本地變量,因爲一個線程可以維護多個ThreadLocal實例。

ThreadLocalMap內部保存了衆多的ThreadLocal對象,既然說ThreadLocal是線程私有的,那麼ThreadLocalMap是存放在哪裏呢?

Thread類有一個成員變量——threadLocals,它就是保存了與當前Thread關聯的一個ThreadLocalMap,源碼如下:

//當前線程內部維護的ThreadLocalMap對象,用於保存所有ThreadLocal實例
ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,ThreadLocalMap對象保存在了Thread的內部,也即當前線程的私有內存中。通過上面的分析,我們可以瞭解ThreadLocal變量的大致內存結構如下:

在這裏插入圖片描述

ThreadLocal的主要方法爲get()、set()和initialValue()。首先看set():

public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    
    //獲取當前線程關聯的ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    
    //創建一個Entry,以當前ThreadLocal對象爲Key,待存儲對象爲Value,保存在ThreadLocalMap中
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

可以看到,set()的邏輯很簡單,從當前線程中獲取ThreadLocalMap,然後將該ThreadLocal的值保存在裏面。

再看get()方法:

public T get() {
    //獲取當前線程對象
    Thread t = Thread.currentThread();
    
    //獲取當前線程關聯的ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    
    //從ThreadLocalMap獲取以ThreadLocal爲key的Entry的value
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    //如果當前ThreadLocalMap不存在,則調用setInitialValue()方法,獲取初始值
    return setInitialValue();
}

ThreadLocal還有一個方法initialValue(),該方法提供給子類覆蓋,以在創建ThreadLocal時指定初始值。

四. 應用場景

ThreadLocal最常見的使用場景爲管理數據庫連接Connection對象等。Spring中使用ThreadLocal來設計TransactionSynchronizationManager類,實現了事務管理與數據訪問服務的解耦,同時也保證了多線程環境下connection的線程安全問題。

五. ThreadLocal的內存泄漏問題

  1. JVM引用類型

    從源碼中可以看出,ThreadLocalMap中Entry的key爲ThreadLocal對象,並將其聲明成了一個WeakReference弱引用。在分析其設計思想之前,先簡單回顧下JVM中的幾種引用類型:

    1. 強應用:普通的引用類型,被強引用的對象是一定不會被GC回收的。
    2. 軟引用SoftReference:軟引用一般用來實現內存敏感的緩存,如果有空閒內存就可以保留緩存,當內存不足時就清理掉,這樣就保證使用緩存的同時不會耗盡內存。
    3. 弱引用WeakReference:它的生命週期比軟引用還要短,在GC的時候,不管內存空間是否夠用,都會回收WeakReference對象。
    4. 虛引用PhantomReference(較少使用):任何時候可能被GC回收,就像沒有引用一樣。

    ThreadLocal之所以設計成WeakReference,目的就是當外部不再持有ThreadLocal的強引用時,儘快回收該ThreadLocalMap中對應的key。

  2. ThreadLocal的內存泄漏問題

    如前文所述,當ThreadLocal沒有外部強引用來引用它時,ThreadLocal對象就會在下次JVM垃圾收集時被回收,這個時候就會出現Entry中Key已經被回收,但是Value仍然所在,即所謂的"null Key"情況。此時外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就會一直存在一條強引用鏈:Thread --> ThreadLocalMap–>Entry–>Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收了,進而造成內存泄漏。

  3. JDK對於ThreadLocal內存泄漏的解決方案

    JDK團隊已經考慮到這樣的情況,並做了一些措施來使得ThreadLocal儘量不會出現內存泄漏:在ThreadLocal的get()、set()、remove()方法調用的時候,會清除掉ThreadLocalMap的所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收。

    以ThreadLocal的get()方法爲例:

    public T get() {
      //獲取當前線程實例
      Thread t = Thread.currentThread();
      
      //獲取當前線程中的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      if (map != null) {
        
        //通過ThreadLocalMap的getEntry()方法獲取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
        }
      }
      return setInitialValue();
    }
    

    它會調用ThreadLocalMap的getEntry()方法獲取Entry實例:

    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
        //如果Entry獲取不到,則調用getEntryAfterMiss()
        return getEntryAfterMiss(key, i, e);
    }
    

    如果獲取不到,說明該key不存在或已經被回收了,則進入getEntryAfterMiss()方法:

    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)
          return e;
        
        //如果key爲null,則清除該Entry項
        if (k == null)
          expungeStaleEntry(i);
        else
          i = nextIndex(i, len);
        e = tab[i];
      }
      return null;
    }
    

    注意這裏的k == null的情況,如果key爲null,則執行expungeStaleEntry清除該Entry項。

    此外,JDK推薦當ThreadLocal對象不再使用時,顯式調用其remove()方法,清除該線程本地變量,最終也會調用上面的ThreadLocalMap.getEntryAfterMiss()方法。

    綜上,JDK也提供了相應的策略儘量避免ThreadLocal導致的內存泄漏問題,主要是在get()和remove()操作時清除已被回收的Entry項。但是該策略也並不是完美的,如果用戶將ThreadLocal初始化後,再也不調用get()或remove()方法,則還是有內存泄漏的風險。

  4. 爲什麼要使用WeakReference?

    從前文的描述中,很可能造成一種錯覺:ThreadLocal由於使用了WeakReference而導致了內存泄漏。這其實是沒有真正理解ThreadLocal內存泄漏的本質。首先我們來看看爲什麼要使用弱引用。下面是官方文檔的說法:

    To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 
    Entry的key使用WeakReference弱引用,來處理大內存、長生命週期的線程的使用問題。
    

    我們分別考慮下不使用WeakReferences和使用WeakReferences的情況下,分別會造成什麼問題:

    1. 不使用WeakReferences,而使用強引用:外部引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致整個Entry內存泄漏。
    2. 使用使用WeakReferences:引用的ThreadLocal的對象被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set()、get()或remove()的時候會被清除。如果沒有調用,則可能造成Entry的Value的內存泄漏。

    由此可以看出,無論是否使用WeakReference,都有可能產生內存泄漏的情況,其根本原因在於ThreadLocalMap的生命週期與線程綁定。如果線程存活時間較長,且沒有顯式remove掉ThreadLocal對象,就有可能出現問題。而使用了WeakReference,至少可以保證無用的ThreadLocal對象被回收,不會出現整個Entry的內存泄漏,在一定程度上緩解了該問題。

  5. 總結

    ThreadLocal的內存泄漏問題,根本原因是ThreadLocalMap的生命週期與Thread綁定,如果線程執行時間較長,則ThreadLocalMap就會一直不被GC回收。如果不顯式調用remove()方法移除過期的ThreadLocal,則有可能造成內存泄漏。因此建議使用ThreadLocal時線程生命週期不要過長,且ThreadLocal對象使用完後顯式調用remove()方法進行移除。

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