ThreadLocal全解析——你想要的這裏都有

ThreadLocal


概念

ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵,任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以通過ThreadLocal對象查詢到綁定在這個線程上的一個值。


原理

關於ThreadLocal的原理,理清四個角色關係:Thread,ThreadLocal,ThreadLocalMap,Entry

image-20191013150324072

在ThreadLocal中有個變量指向ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是ThreadLocal的靜態內部類,當線程第一次執行set時,ThreadLocal會創建一個ThreadLocalMap對象,設置給Thread的threadLocals變量。

ThreadLocalMap中存放的是Entry,Entry是ThreadLocal和value的映射。

每一個線程都擁有一個ThreadLocalMap。

image-20191013151928154


關於內存泄漏

ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。

這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,一直存在,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係:Thread --> ThreadLocalMap–>Entry–>Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。

但是JVM團隊已經考慮到這樣的情況,並做了一些措施來保證ThreadLocal儘量不會內存泄漏:

  • 在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收Entry、value。

ThreadLocalMap處理Hash衝突

採用線性探測法來處理衝突,從當前位置往後找尋空位,空位指的是table[ i ] = null 或是 table[ i ] .key = null,將Entry插入該位置。也就是說一個Entry要麼在它的hash位置上,要麼就在該位置往後的某一位置上。

由於線性探測發 table 數組中的情況一定是一段一段連續的片段,我們將一個連續的片段稱爲 run


關於線程安全性

每個線程都有自己的ThreadLocalMap,以及Entry[] 數組,只有自己操作,所以是線程安全的。那麼ThreadLocal呢?它並沒有可更改的狀態,所以也是線程安全的,來看看它的三個成員變量

// 每個ThreadLocal對象初始化後都會得到自己的hash值,之後不會再變
private final int threadLocalHashCode = nextHashCode();

// 靜態對象AtomicInteger,與ThreadLocal對象無關,
// 在第一次ThreadLocal類加載時初始化
private static AtomicInteger nextHashCode = new AtomicInteger();

// 不可變
private static final int HASH_INCREMENT = 0x61c88647;

所以說 ThreadLocal 也是線程安全的。


使用場景

常用於同一次請求的參數傳遞。比如說把身份信息埋到ThreadLocal中,然後該請求的所有接口都可以獲取到這個身份信息。


父子線程傳遞實現方案

如果子線程想要拿到父線程的中的ThreadLocal值怎麼辦呢?


錯誤的示例

比如會有以下的這種代碼的實現。由於ThreadLocal的實現機制,在子線程中get時,我們拿到的Thread對象是當前子線程對象,那麼他的ThreadLocalMap是null的,所以我們得到的value也是null。

private static void demo1() throws Exception {

  Thread.currentThread().setName("主線程");

  final ThreadLocal<String> threadLocal = new ThreadLocal<>();
  // 調用set方法的時候,會初始化一個ThreadLocalMap
  threadLocal.set("這個父線程設置的變量");

  Thread subThread = new Thread(new Runnable() {
    @Override
    public void run() {
      // 子線程獲取父線程的threadLocal,結果爲null
      System.out.println("子線程獲取的變量爲   " +
                         threadLocal.get());
    }
  });
  subThread.setName("子線程");
  subThread.start();
}

public static void main(String[] args) throws Exception {
  demo1();
}

那麼有沒有方法正確的獲取父線程中的ThreadLocal呢?


InheritableThreadLocal

那其實很多時候我們是有子線程獲得父線程ThreadLocal的需求的,要如何解決這個問題呢?這就是InheritableThreadLocal這個類所做的事情。先來看下InheritableThreadLocal所做的事情。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    /**
     * 重寫ThreadLocal類中的getMap方法,在原Threadlocal中是返回
     * t.theadLocals,而在這麼卻是返回了inheritableThreadLocals,因爲
     * Thread類中也有一個要保存父子傳遞的變量
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值
     *而是給inheritableThreadLocals變量賦值
     * 
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

以上代碼大致的意思就是,如果你使用InheritableThreadLocal,那麼保存的所有東西都已經不在原來的t.thradLocals裏面,而是在一個新的t.inheritableThreadLocals變量中了。下面是Thread類中兩個變量的定義

/**
 * 線程所持有的threadLocals
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

/**
 * 線程所持有的inheritableThreadLocals,保持了從父線程繼承而來的本地變量信息
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal是如何實現在子線程中能拿到當前父線程中的值的呢?

一個常見的想法就是把父線程的所有的值都copy到子線程中。

// Thread 線程類的初始化方法
private void init(ThreadGroup g, Runnable target, String name,
                     long stackSize, AccessControlContext acc) {
       //省略上面部分代碼
       if (parent.inheritableThreadLocals != null)
       //這句話的意思大致不就是,copy父線程parent的map,創建一個新的map賦值給當前線程的inheritableThreadLocals。
           this.inheritableThreadLocals =
               ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      //ignore
}

而且,在copy過程中是淺拷貝,key和value都是原來的引用地址

private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

  for (int j = 0; j < len; j++) {
    Entry e = parentTable[j];
    if (e != null) {

      // 獲取key
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null) {
        // 獲取value
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);

        // 計算存放key的位置
        int h = key.threadLocalHashCode & (len - 1);

        // 線性探測法
        while (table[h] != null)
          h = nextIndex(h, len);
        table[h] = c;

        size++;
      }
    }
  }

到了這裏,大致的解釋了一下InheritableThreadLocal爲什麼能解決父子線程傳遞Threadlcoal值的問題。

  1. 在創建新線程的時候會檢查父線程中t.inheritableThreadLocals變量是否爲null,如果不爲null則拷貝一份ThradLocalMap到子線程的t.inheritableThreadLocals成員變量中去
  2. 因爲複寫了getMap(Thread)和createMap()方法,所以調用get()方法的時候,就可以在getMap(t)的時候就會從t.inheritableThreadLocals中拿到map對象,從而實現了可以拿到父線程ThreadLocal中的值。
private static void demo2() throws Exception {
  Thread.currentThread().setName("主線程");

  final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
  // 調用set方法的時候,會初始化一個ThreadLocalMap
  threadLocal.set("這個父線程設置的變量");

  Thread subThread = new Thread(new Runnable() {
    @Override
    public void run() {
      // 子線程獲取父線程的threadLocal
      // 輸出爲:    子線程獲取的變量爲   這個父線程設置的變量
      System.out.println("子線程獲取的變量爲   " +
                         threadLocal.get());
    }
  });
  subThread.setName("子線程");
  subThread.start();
}

InheritableThreadLocal不足

我們在使用線程的時候往往不會只是簡單的new Thread對象,而是使用線程池,當然線程池的好處多多。這裏不詳解,既然這裏提出了問題,那麼線程池會給InheritableThreadLocal帶來什麼問題呢?

我們列舉一下線程池的特點:

  1. 爲了減小創建線程的開銷,線程池會緩存已經使用過的線程
  2. 生命週期統一管理,合理的分配系統資源

如下示例:

private static void demo3() throws Exception {
        final InheritableThreadLocal<String> inheritableThreadLocal =
                new InheritableThreadLocal<>();
        inheritableThreadLocal.set("xiexiexie");

        //輸出 xiexiexie
        System.out.println("父線程中獲取inheritableThreadLocal, 值爲:" +
                inheritableThreadLocal.get());

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                System.out.println("子線程中獲取inheritableThreadLocal, 值爲:" +
                        inheritableThreadLocal.get());

                inheritableThreadLocal.set("zhangzhangzhang");

                System.out.println("子線程中獲取inheritableThreadLocal, 值爲:" +
                        inheritableThreadLocal.get());
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);

        /**
         * 第二次執行的時候,使用的是上一條線程,
         * 並且InheritableThreadLocal只有在線程初始化的時候才從父線程繼承數據。
         * 因此這次執行任務直接使用線程當前的InheritableThreadLocal
         */
        executorService.submit(runnable);

        TimeUnit.SECONDS.sleep(1);

        System.out.println("父線程中獲取inheritableThreadLocal, 值爲:" +
                inheritableThreadLocal.get());

        executorService.shutdown();
}

可見,在使用線程池的情況,由於複用線程,所以造成InheriableThreadLocal被複用,從而導致無法使用父類的數據。


解決方案

如果我們能夠,在submit新任務的時候在重新從父線程中拷貝所有的變量。然後將這些變量賦值給當前線程的t.inhertableThreadLocal賦值。這樣就能夠解決在線程池中每一個新的任務都能夠獲得父線程中ThreadLocal中的值而不受其他任務的影響。Alibaba的一個庫解決了這個問題 [github:alibaba/transmittable-thread-local]


transmittable-thread-local實現原理

這個庫最簡單的方式是這樣使用的,通過簡單的修飾,使得提交的runable擁有了上一節所述的功能。具體的API文檔詳見github,這裏不再贅述。

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 額外的處理,生成修飾了的對象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);

// Task中可以讀取, 值是"value-set-in-parent"
String value = parent.get();

這個方法TtlRunnable.get(task)最終會調用構造方法,返回的是該類本身,也是一個Runable,這樣就完成了簡單的裝飾。最重要的是在run方法這個地方。

public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    //從父類copy值到本類當中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修飾對象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //裝載到當前線程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//執行提交的task
        } finally {
        //clear
            TransmittableThreadLocal.restoreBackup(backup);
        }
    }
}

在上面的使用線程池的例子當中,如果換成這種修飾的方式進行操作,B任務得到的肯定是父線程中ThreadLocal的值,解決了在線程池中InheritableThreadLocal不能解決的問題。


更新父線程ThreadLocal值?

如果線程之間出了要能夠得到父線程中的值,同時想更新值怎麼辦呢?在前面我們有提到,當子線程copy父線程的ThreadLocalMap的時候是淺拷貝的,代表子線程Entry裏面的value都是指向的同一個引用,我們只要修改這個引用的同時就能夠修改父線程當中的值了。


問題

ThreadLocal時要注意什麼?比如說內存泄漏?

需要主動調用remove()方法釋放無用的內存,原因查看上面的內存泄漏。


參考

ThreadLocal內存泄漏

ThreadLocal父子線程傳遞數據

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