ThreadLocal奪命11連問

前言

前一段時間,有同事使用ThreadLocal踩坑了,正好引起了我的興趣。

所以近期,我抽空把ThreadLocal的源碼再研究了一下,越看越有意思,發現裡面的東西還真不少。

我把精華濃縮了一下,彙集成了下面11個問題,看看你能頂住第幾個?

1. 為什麼要用ThreadLocal?

並發編程是一項非常重要的技術,它讓我們的程式變得更加高效。

但在並發的場景中,如果有多個執行緒同時修改公共變數,可能會出現執行緒安全問題,即該變數最終結果可能出現異常。

為了解決執行緒安全問題,JDK出現了很多技術手段,比如:使用synchronizedLock,給訪問公共資源的程式碼上鎖,保證了程式碼的原子性

但在高並發的場景中,如果多個執行緒同時競爭一把鎖,這時會存在大量的鎖等待,可能會浪費很多時間,讓系統的響應時間一下子變慢。

因此,JDK還提供了另外一種用空間換時間的新思路:ThreadLocal

它的核心思想是:共享變數在每個執行緒都有一個副本,每個執行緒操作的都是自己的副本,對另外的執行緒沒有影響。

例如:

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public void add() {
        threadLocal.set(1);
        doSamething();
        Integer integer = threadLocal.get();
    }
}

2. ThreadLocal的原理是什麼?

為了搞清楚ThreadLocal的底層實現原理,我們不得不扒一下源碼。

ThreadLocal的內部有一個靜態的內部類叫:ThreadLocalMap

public class ThreadLocal<T> {
     ...
     public T get() {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取當前執行緒的成員變數ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根據threadLocal對象從map中獲取Entry對象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //獲取保存的數據
                T result = (T)e.value;
                return result;
            }
        }
        //初始化數據
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //獲取要初始化的數據
        T value = initialValue();
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取當前執行緒的成員變數ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        //如果map不為空
        if (map != null)
            //將初始值設置到map中,key是this,即threadLocal對象,value是初始值
            map.set(this, value);
        else
           //如果map為空,則需要創建新的map對象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取當前執行緒的成員變數ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        //如果map不為空
        if (map != null)
            //將值設置到map中,key是this,即threadLocal對象,value是傳入的value值
            map.set(this, value);
        else
           //如果map為空,則需要創建新的map對象
            createMap(t, value);
    }
    
     static class ThreadLocalMap {
        ...
     }
     ...
}

ThreadLocalget方法、set方法和setInitialValue方法,其實最終操作的都是ThreadLocalMap類中的數據。

其中ThreadLocalMap類的內部如下:

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
   }
   ...
   private Entry[] table;
   ...
}

ThreadLocalMap裡面包含一個靜態的內部類Entry,該類繼承於WeakReference類,說明Entry是一個弱引用。

ThreadLocalMap內部還包含了一個Entry數組,其中:Entry = ThreadLocal + value

ThreadLocalMap被定義成了Thread類的成員變數。

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

下面用一張圖從宏觀上,認識一下ThreadLocal的整體結構:

從上圖中看出,在每個Thread類中,都有一個ThreadLocalMap的成員變數,該變數包含了一個Entry數組,該數組真正保存了ThreadLocal類set的數據。

Entry是由threadLocal和value組成,其中threadLocal對象是弱引用,在GC的時候,會被自動回收。而value就是ThreadLocal類set的數據。

下面用一張圖總結一下引用關係:

上圖中除了Entry的key對ThreadLocal對象是弱引用,其他的引用都是強引用

需要特別說明的是,上圖中ThreadLocal對象我畫到了堆上,其實在實際的業務場景中不一定在堆上。因為如果ThreadLocal被定義成了static的,ThreadLocal的對象是類共用的,可能出現在方法區。

3. 為什麼用ThreadLocal做key?

不知道你有沒有思考過這樣一個問題:ThreadLocalMap為什麼要用ThreadLocal做key,而不是用Thread做key?

如果在你的應用中,一個執行緒中只使用了一個ThreadLocal對象,那麼使用Thread做key也未嘗不可。

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}    

但實際情況中,你的應用,一個執行緒中很有可能不只使用了一個ThreadLocal對象。這時使用Thread做key不就出有問題?

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}    

假如使用Thread做key時,你的程式碼中定義了3個ThreadLocal對象,那麼,通過Thread對象,它怎麼知道要獲取哪個ThreadLocal對象呢?

如下圖所示:

因此,不能使用Thread做key,而應該改成用ThreadLocal對象做key,這樣才能通過具體ThreadLocal對象的get方法,輕鬆獲取到你想要的ThreadLocal對象。

如下圖所示:

4. Entry的key為什麼設計成弱引用?

前面說過,Entry的key,傳入的是ThreadLocal對象,使用了WeakReference對象,即被設計成了弱引用。

那麼,為什麼要這樣設計呢?

假如key對ThreadLocal對象的弱引用,改為強引用。

我們都知道ThreadLocal變數對ThreadLocal對象是有強引用存在的。

即使ThreadLocal變數生命周期完了,設置成null了,但由於key對ThreadLocal還是強引用。

此時,如果執行該程式碼的執行緒使用了執行緒池,一直長期存在,不會被銷毀。

就會存在這樣的強引用鏈:Thread變數 -> Thread對象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對象。

那麼,ThreadLocal對象和ThreadLocalMap都將不會被GC回收,於是產生了記憶體泄露問題。

為了解決這個問題,JDK的開發者們把Entry的key設計成了弱引用

弱引用的對象,在GC做垃圾清理的時候,就會被自動回收了。

如果key是弱引用,當ThreadLocal變數指向null之後,在GC做垃圾清理的時候,key會被自動回收,其值也被設置成null。

如下圖所示:

接下來,最關鍵的地方來了。

由於當前的ThreadLocal變數已經被指向null了,但如果直接調用它的getsetremove方法,很顯然會出現空指針異常。因為它的生命已經結束了,再調用它的方法也沒啥意義。

此時,如果系統中還定義了另外一個ThreadLocal變數b,調用了它的getsetremove,三個方法中的任何一個方法,都會自動觸發清理機制,將key為null的value值清空。

如果key和value都是null,那麼Entry對象會被GC回收。如果所有的Entry對象都被回收了,ThreadLocalMap也會被回收了。

這樣就能最大程度的解決記憶體泄露問題。

需要特別注意的地方是:

  1. key為null的條件是,ThreadLocal變數指向null,並且key是弱引用。如果ThreadLocal變數沒有斷開對ThreadLocal的強引用,即ThreadLocal變數沒有指向null,GC就貿然的把弱引用的key回收了,不就會影響正常用戶的使用?
  2. 如果當前ThreadLocal變數指向null了,並且key也為null了,但如果沒有其他ThreadLocal變數觸發getsetremove方法,也會造成記憶體泄露。

下面看看弱引用的例子:

public static void main(String[] args) {
    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
    System.out.println(weakReference0.get());
    System.gc();
    System.out.println(weakReference0.get());
}

列印結果:

java.lang.Object@1ef7fe8e
null

傳入WeakReference構造方法的是直接new處理的對象,沒有其他引用,在調用gc方法後,弱引用對象會被自動回收。

但如果出現下面這種情況:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
}

執行結果:

java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e

先定義了一個強引用object對象,在WeakReference構造方法中將object對象的引用作為參數傳入。這時,調用gc後,弱引用對象不會被自動回收。

我們的Entry對象中的key不就是第二種情況嗎?在Entry構造方法中傳入的是ThreadLocal對象的引用。

如果將object強引用設置為null:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());

    object=null;
    System.gc();
    System.out.println(weakReference1.get());
}

執行結果:

java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null

第二次gc之後,弱引用能夠被正常回收。

由此可見,如果強引用和弱引用同時關聯一個對象,那麼這個對象是不會被GC回收。也就是說這種情況下Entry的key,一直都不會為null,除非強引用主動斷開關聯。

此外,你可能還會問這樣一個問題:Entry的value為什麼不設計成弱引用?

答:Entry的value如果只是被Entry引用,有可能沒被業務系統中的其他地方引用。如果將value改成了弱引用,被GC貿然回收了(數據突然沒了),可能會導致業務系統出現異常。

而相比之下,Entry的key,管理的地方就非常明確了。

這就是Entry的key被設計成弱引用,而value沒被設計成弱引用的原因。

5. ThreadLocal真的會導致記憶體泄露?

通過上面的Entry對象中的key設置成弱引用,並且使用getsetremove方法清理key為null的value值,就能徹底解決記憶體泄露問題?

答案是否定的。

如下圖所示:

假如ThreadLocalMap中存在很多key為null的Entry,但後面的程式,一直都沒有調用過有效的ThreadLocal的getsetremove方法。

那麼,Entry的value值一直都沒被清空。

所以會存在這樣一條強引用鏈:Thread變數 -> Thread對象 -> ThreadLocalMap -> Entry -> value -> Object。

其結果就是:Entry和ThreadLocalMap將會長期存在下去,會導致記憶體泄露

6. 如何解決記憶體泄露問題?

前面說過的ThreadLocal還是會導致記憶體泄露的問題,我們有沒有解決辦法呢?

答:有辦法,調用ThreadLocal對象的remove方法。

不是在一開始就調用remove方法,而是在使用完ThreadLocal對象之後。列如:

先創建一個CurrentUser類,其中包含了ThreadLocal的邏輯。

public class CurrentUser {
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
    
    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }
    
    public static UserInfo get() {
       THREA_LOCAL.get();
    }
    
    public static void remove() {
       THREA_LOCAL.remove();
    }
}

然後在業務程式碼中調用相關方法:

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);
   
   try{
     CurrentUser.set(userInfo);
     ...
     
     //業務程式碼
     UserInfo userInfo = CurrentUser.get();
     ...
   } finally {
      CurrentUser.remove();
   }
}

需要我們特別注意的地方是:一定要在finally程式碼塊中,調用remove方法清理沒用的數據。如果業務程式碼出現異常,也能及時清理沒用的數據。

remove方法中會把Entry中的key和value都設置成null,這樣就能被GC及時回收,無需觸發額外的清理機制,所以它能解決記憶體泄露問題。

7. ThreadLocal是如何定位數據的?

前面說過ThreadLocalMap對象底層是用Entry數組保存數據的。

那麼問題來了,ThreadLocal是如何定位Entry數組數據的?

在ThreadLocal的get、set、remove方法中都有這樣一行程式碼:

int i = key.threadLocalHashCode & (len-1);

通過key的hashCode值,數組的長度減1。其中key就是ThreadLocal對象,數組的長度減1,相當於除以數組的長度減1,然後取模

這是一種hash演算法。

接下來給大家舉個例子:假設len=16,key.threadLocalHashCode=31,

於是: int i = 31 & 15 = 15

相當於:int i = 31 % 16 = 15

計算的結果是一樣的,但是使用與運算效率跟高一些。

為什麼與運算效率更高?

答:因為ThreadLocal的初始大小是16,每次都是按2倍擴容,數組的大小其實一直都是2的n次方。這種數據有個規律就是高位是0,低位都是1。在做與運算時,可以不用考慮高位,因為與運算的結果必定是0。只需考慮低位的與運算,所以效率更高。

如果使用hash演算法定位具體位置的話,就可能會出現hash衝突的情況,即兩個不同的hashCode取模後的值相同。

ThreadLocal是如何解決hash衝突的呢?

我們看看getEntry是怎麼做的:

private Entry getEntry(ThreadLocal<?> key) {
    //通過hash演算法獲取下標值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果下標位置上的key正好是我們所需要尋找的key
    if (e != null && e.get() == key)
        //說明找到數據了,直接返回
        return e;
    else
        //說明出現hash衝突了,繼續往後找
        return getEntryAfterMiss(key, i, e);
}

再看看getEntryAfterMiss方法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //判斷Entry對象如果不為空,則一直循環
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果當前Entry的key正好是我們所需要尋找的key
        if (k == key)
            //說明這次真的找到數據了
            return e;
        if (k == null)
            //如果key為空,則清理臟數據
            expungeStaleEntry(i);
        else
            //如果還是沒找到數據,則繼續往後找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

關鍵看看nextIndex方法:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

當通過hash演算法計算出的下標小於數組大小,則將下標值加1。否則,即下標大於等於數組大小,下標變成0了。下標變成0之後,則循環一次,下標又變成1。。。

尋找的大致過程如下圖所示:

如果找到最後一個,還是沒有找到,則再從頭開始找。

不知道你有沒有發現,它構成了一個:環形

ThreadLocal從數組中找數據的過程大致是這樣的:

  1. 通過key的hashCode取余計算出一個下標。
  2. 通過下標,在數組中定位具體Entry,如果key正好是我們所需要的key,說明找到了,則直接返回數據。
  3. 如果第2步沒有找到我們想要的數據,則從數組的下標位置,繼續往後面找。
  4. 如果第3步中找key的正好是我們所需要的key,說明找到了,則直接返回數據。
  5. 如果還是沒有找到數據,再繼續往後面找。如果找到最後一個位置,還是沒有找到數據,則再從頭,即下標為0的位置,繼續從前往後找數據。
  6. 直到找到第一個Entry為空為止。

8. ThreadLocal是如何擴容的?

從上面得知,ThreadLocal的初始大小是16。那麼問題來了,ThreadLocal是如何擴容的?

set方法中會調用rehash方法:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    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;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

注意一下,其中有個判斷條件是:sz(之前的size+1)如果大於或等於threshold的話,則調用rehash方法。

threshold默認是0,在創建ThreadLocalMap時,調用它的構造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

調用setThreshold方法給threshold設置一個值,而這個值INITIAL_CAPACITY是默認的大小16。

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

也就是第一次設置的threshold = 16 * 2 / 3, 取整後的值是:10。

換句話說當sz大於等於10時,就可以考慮擴容了。

rehash程式碼如下:

private void rehash() {
    //先嘗試回收一次key為null的值,騰出一些空間
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

在真正擴容之前,先嘗試回收一次key為null的值,騰出一些空間。

如果回收之後的size大於等於threshold的3/4時,才需要真正的擴容。

計算公式如下:

16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8

也就是說添加數據後,新的size大於等於老size的1/2時,才需要擴容。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //按2倍的大小擴容
    int newLen = oldLen * 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();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

resize中每次都是按2倍的大小擴容。

擴容的過程如下圖所示:

擴容的關鍵步驟如下:

  1. 老size + 1 = 新size
  2. 如果新size大於等於老size的2/3時,需要考慮擴容。
  3. 擴容前先嘗試回收一次key為null的值,騰出一些空間。
  4. 如果回收之後發現size還是大於等於老size的1/2時,才需要真正的擴容。
  5. 每次都是按2倍的大小擴容。

9. 父子執行緒如何共享數據?

前面介紹的ThreadLocal都是在一個執行緒中保存和獲取數據的。

但在實際工作中,有可能是在父子執行緒中共享數據的。即在父執行緒中往ThreadLocal設置了值,在子執行緒中能夠獲取到。

例如:

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父執行緒獲取數據:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子執行緒獲取數據:" + threadLocal.get());
        }).start();
    }
}

執行結果:

父執行緒獲取數據:6
子執行緒獲取數據:null

你會發現,在這種情況下使用ThreadLocal是行不通的。main方法是在主執行緒中執行的,相當於父執行緒。在main方法中開啟了另外一個執行緒,相當於子執行緒。

顯然通過ThreadLocal,無法在父子執行緒中共享數據。

那麼,該怎麼辦呢?

答:使用InheritableThreadLocal,它是JDK自帶的類,繼承了ThreadLocal類。

修改程式碼之後:

public class ThreadLocalTest {

    public static void main(String[] args) {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父執行緒獲取數據:" + threadLocal.get());

        new Thread(() -> {
            System.out.println("子執行緒獲取數據:" + threadLocal.get());
        }).start();
    }
}

執行結果:

父執行緒獲取數據:6
子執行緒獲取數據:6

果然,在換成InheritableThreadLocal之後,在子執行緒中能夠正常獲取父執行緒中設置的值。

其實,在Thread類中除了成員變數threadLocals之外,還有另一個成員變數:inheritableThreadLocals。

Thread類的部分程式碼如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

最關鍵的一點是,在它的init方法中會將父執行緒中往ThreadLocal設置的值,拷貝一份到子執行緒中。

感興趣的小夥伴,可以找我私聊。或者看看我後面的文章,後面還會有專欄。

10. 執行緒池中如何共享數據?

在真實的業務場景中,一般很少用單獨的執行緒,絕大多數,都是用的執行緒池

那麼,在執行緒池中如何共享ThreadLocal對象生成的數據呢?

因為涉及到不同的執行緒,如果直接使用ThreadLocal,顯然是不合適的。

我們應該使用InheritableThreadLocal,具體程式碼如下:

private static void fun1() {
    InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("父執行緒獲取數據:" + threadLocal.get());

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    threadLocal.set(6);
    executorService.submit(() -> {
        System.out.println("第一次從執行緒池中獲取數據:" + threadLocal.get());
    });

    threadLocal.set(7);
    executorService.submit(() -> {
        System.out.println("第二次從執行緒池中獲取數據:" + threadLocal.get());
    });
}

執行結果:

父執行緒獲取數據:6
第一次從執行緒池中獲取數據:6
第二次從執行緒池中獲取數據:6

由於這個例子中使用了單例執行緒池,固定執行緒數是1。

第一次submit任務的時候,該執行緒池會自動創建一個執行緒。因為使用了InheritableThreadLocal,所以創建執行緒時,會調用它的init方法,將父執行緒中的inheritableThreadLocals數據複製到子執行緒中。所以我們看到,在主執行緒中將數據設置成6,第一次從執行緒池中獲取了正確的數據6。

之後,在主執行緒中又將數據改成7,但在第二次從執行緒池中獲取數據卻依然是6。

因為第二次submit任務的時候,執行緒池中已經有一個執行緒了,就直接拿過來複用,不會再重新創建執行緒了。所以不會再調用執行緒的init方法,所以第二次其實沒有獲取到最新的數據7,還是獲取的老數據6。

那麼,這該怎麼辦呢?

答:使用TransmittableThreadLocal,它並非JDK自帶的類,而是阿里巴巴開源jar包中的類。

可以通過如下pom文件引入該jar包:

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>transmittable-thread-local</artifactId>
   <version>2.11.0</version>
   <scope>compile</scope>
</dependency>

程式碼調整如下:

private static void fun2() throws Exception {
    TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("父執行緒獲取數據:" + threadLocal.get());

    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    threadLocal.set(6);
    ttlExecutorService.submit(() -> {
        System.out.println("第一次從執行緒池中獲取數據:" + threadLocal.get());
    });

    threadLocal.set(7);
    ttlExecutorService.submit(() -> {
        System.out.println("第二次從執行緒池中獲取數據:" + threadLocal.get());
    });

}

執行結果:

父執行緒獲取數據:6
第一次從執行緒池中獲取數據:6
第二次從執行緒池中獲取數據:7

我們看到,使用了TransmittableThreadLocal之後,第二次從執行緒中也能正確獲取最新的數據7了。

nice。

如果你仔細觀察這個例子,你可能會發現,程式碼中除了使用TransmittableThreadLocal類之外,還使用了TtlExecutors.getTtlExecutorService方法,去創建ExecutorService對象。

這是非常重要的地方,如果沒有這一步,TransmittableThreadLocal在執行緒池中共享數據將不會起作用。

創建ExecutorService對象,底層的submit方法會TtlRunnableTtlCallable對象。

以TtlRunnable類為例,它實現了Runnable介面,同時還實現了它的run方法:

public void run() {
    Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
    if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
        Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);

        try {
            this.runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    } else {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
}

這段程式碼的主要邏輯如下:

  1. 把當時的ThreadLocal做個備份,然後將父類的ThreadLocal拷貝過來。
  2. 執行真正的run方法,可以獲取到父類最新的ThreadLocal數據。
  3. 從備份的數據中,恢復當時的ThreadLocal數據。

11. ThreadLocal有哪些用途?

最後,一起聊聊ThreadLocal有哪些用途?

老實說,使用ThreadLocal的場景挺多的。

下面列舉幾個常見的場景:

  1. 在spring事務中,保證一個執行緒下,一個事務的多個操作拿到的是一個Connection。
  2. 在hiberate中管理session。
  3. 在JDK8之前,為了解決SimpleDateFormat的執行緒安全問題。
  4. 獲取當前登錄用戶上下文。
  5. 臨時保存許可權數據。
  6. 使用MDC保存日誌資訊。

等等,還有很多業務場景,這裡就不一一列舉了。

由於篇幅有限,今天的內容先分享到這裡。希望你看了這篇文章,會有所收穫。

接下來留幾個問題給大家思考一下:

  1. ThreadLocal變數為什麼建議要定義成static的?
  2. Entry數組為什麼要通過hash演算法計算下標,即直線定址法,而不直接使用下標值?
  3. 強引用和弱引用有什麼區別?
  4. Entry數組大小,為什麼是2的N次方?
  5. 使用InheritableThreadLocal時,如果父執行緒中重新set值,在子執行緒中能夠正確的獲取修改後的新值嗎?

敬請期待我的下一篇文章,謝謝。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

關注公眾號:【蘇三說技術】,在公眾號中回復:面試、程式碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回復:加群,可以跟很多BAT大廠的前輩交流和學習。