1. 程式人生 > 其它 >ThreadLocal 原始碼分析

ThreadLocal 原始碼分析


name: threadlocal
title: ThreadLocal 原始碼分析
date: 2021-12-02 19:10:00

ThreadLocal 早在JDK1.2 就由Josh BlochEffective Java作者)提出,在JDK1.5 由 Doug lea( juc作者) 進行優化;ThreadLocal譯為執行緒區域性變數,核心就是利用記憶體空間來換取執行時間(空間:每個執行緒Thread擁有一份自己的副本變數,執行緒互不干擾 時間:避免使用鎖來解決競爭情況,鎖會導致效能下降,時間變長)

  • 當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本
  • 既然每個Thread有自己的例項副本,且其它Thread不可訪問,那就不存在多執行緒間共享的問題,所以不會出現執行緒不安全的情況

先強調一點:ThreadLocal不是用來解決共享變數問題的,它與多執行緒的併發問題沒有任何關係

引言

ThreadLocal在java生態中應用很廣泛,本文基於jdk1.8,現在我們就從原始碼的角度來進行剖析;我先把ThreadLocal常見的問題先丟擲來,大家可以先思考下:

  • ThreadLocal的例項副本是什麼?

  • ThreadLocalMap的key為什麼是ThreadLocal物件,為什麼繼承WeakReference?如果在get()的時候發生gc,key是否為null

  • ThreadLocal在什麼情況下會出現記憶體洩漏問題,如何避免記憶體洩漏?

  • ThreadLocalMap的資料結構和HashMap有什麼區別

  • ThreadLocalMap如何來解決Hash衝突?

  • ThreadLocalMap 擴容機制

  • ThreadLocal set() 和 get() 原始碼分析

  • ThreadLocalMap中過期key的清理機制?探測式清理和啟發式清理流程

  • InheritableThreadLocal的作用

  • ThreadLocal在系統中的使用場景有哪些

ThreadLocal為什麼是執行緒安全

先看下最基本的ThreadLocal使用例子:

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                longLocal.set(i);
                System.out.println(Thread.currentThread().getName() + " : " + longLocal.get());
            }
        }, "thread-1").start();

        for (int i = 0; i < 5; i++) {
            longLocal.set(i);
            System.out.println(Thread.currentThread().getName() + " : " + longLocal.get());
        }
    }
}

列印結果:

main : 0
thread-1 : 0
thread-1 : 1
thread-1 : 2
thread-1 : 3
thread-1 : 4
main : 1
main : 2
main : 3
main : 4

可以看到,各個執行緒的threadLocal值是相互獨立的,本執行緒的累加操作不會影響到其他執行緒的值,真正達到了執行緒內部隔離的效果

ThreadLocal的資料結構

Thread類有一個型別為ThreadLocal.ThreadLocalMap的例項變數threadLocals,也就是說每個執行緒有一個自己的ThreadLocalMap;這就是上面的第一個問題,ThreadLocalMap就是執行緒的例項副本

	/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals後面講,主要用於執行緒之前資料的傳遞

ThreadLocalMap有自己的獨立實現,可以簡單地將它的key視作ThreadLocalvalue為程式碼中放入的值(實際上key並不是ThreadLocal本身,而是它的一個弱引用

每個執行緒在往ThreadLocal裡放值的時候,都會往自己的ThreadLocalMap裡存,讀也是以ThreadLocal作為引用,在自己的map裡找對應的key,從而實現了執行緒隔離

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // current-thread獲取副本
        if (map != null)
            map.set(this, value);  // this 當前物件當作key,ThreadLocal物件,弱引用
        else
            createMap(t, value);  // 建立新的map
    }

我們來看看存的程式碼

  • 當前執行緒中獲取到ThreadLocalMap,然後將當前ThreadLocal的引用當作key,set的值當初value進行儲存
  • 如果不存在,則呼叫createMap建立一個新的map
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

記憶體佈局 如下圖:

  • Thread Ref 和 ThreadLocal Ref 存放在棧中,具體的Thread和ThreadLocal物件都存在堆
  • ThreadLocalMap的key指向ThreadLocal物件,value就是具體的值

為什麼用ThreadLocal物件當作key的好處

1、自動釋放, 當 Thread 物件銷燬後,ThreadLocalMap 物件也隨之銷燬,JVM 及時回收,避免了記憶體洩漏。如果按我們的想法:定義一個靜態的map,將當前 thread(或 thread 的 ID) 作為key,需要儲存的物件作為 value,put 到 map 中;如果任務完成之後,當前執行緒銷燬了,這個靜態 map 中該執行緒的資訊不會自動回收,如果我們不手動去釋放,這個 map 會隨著時間的積累越來越大,最後出現記憶體洩漏。而一旦需要進行手動釋放,那很有可能就會有漏網之魚,這就像埋一個定時炸彈,定期爆發,而又不好排查!

2、效能提升,各執行緒訪問的 ThreadLocalMap 是各自不同的 ThreadLocalMap,所以不需要同步,速度會快很多;而如果把所有執行緒要用的物件都放到一個靜態 map 中的話,多執行緒併發訪問需要進行同步

GC 之後key是否為null?

迴應開頭的那個問題, ThreadLocalkey是弱引用,那麼在ThreadLocal.get()的時候,發生GC之後,key是否是null

為了搞清楚這個問題,我們需要搞清楚Java四種引用型別

  • 強引用:我們常常new出來的物件就是強引用型別,只要強引用存在,垃圾回收器將永遠不會回收被引用的物件,哪怕記憶體不足的時候
  • 軟引用:使用SoftReference修飾的物件被稱為軟引用,軟引用指向的物件在記憶體要溢位的時候被回收
  • 弱引用:使用WeakReference修飾的物件被稱為弱引用,只要發生垃圾回收,若這個物件只被弱引用指向,那麼就會被回收
  • 虛引用:虛引用是最弱的引用,在 Java 中使用 PhantomReference 進行定義。虛引用中唯一的作用就是用佇列接收物件即將死亡的通知

示例程式碼:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> test("a", false),"thread-1");
    t1.start(); t1.join();
    Thread t2 = new Thread(() -> test("b", true),"thread-2");
    t2.start(); t2.join();

}

private static void test(String s,boolean isGC)  {
    try {
        new ThreadLocal<>().set(s);   //沒有建議任何強引用, 所有GC的時候能被回收,如果 ThreadLocal t = new ThreadLocal<>() 建議了強引用,則無法回收
        if (isGC) {
            System.gc();
        }
        Thread t = Thread.currentThread();
        Class<? extends Thread> clz = t.getClass();
        Field field = clz.getDeclaredField("threadLocals");  // 通過反射獲取threadLocalMap
        field.setAccessible(true);
        Object ThreadLocalMap = field.get(t);
        Class<?> tlmClass = ThreadLocalMap.getClass();
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
        for (Object o : arr) {
            if (o != null) {
                Class<?> entryClass = o.getClass();
                Field valueField = entryClass.getDeclaredField("value");
                Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referenceField.setAccessible(true);
                System.out.printf("弱引用key:%s,值:%s%n", referenceField.get(o), valueField.get(o));
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

列印結果:

弱引用key:java.lang.ThreadLocal@2591b063,值:a
弱引用key:java.lang.ThreadLocal@1f96fc87,值:java.lang.ref.SoftReference@7d7a5ede
弱引用key:null,值:b

從列印結果分析,當執行緒2觸發gc的時候,雖然執行緒2的ThreadLocal物件被清理,但 ThreadLocalMap的強引用還存在,只是key為空;這個時候如果執行緒沒中斷,則會出現記憶體洩漏的問題;對應之前的問題:如果在get()的時候發生gc,key是否為null

觸發gc的時候 ThreadLocal物件被回收,但ThreadLocalMap 的強引用沒有中斷,導致value永遠存在,如果執行緒不結束,就會出現記憶體洩漏的情況;所以alibaba規範中使用ThreadLocal必須在finally中呼叫 .remove()方法;

注:可以這麼講,但凡沒有強引用的ThreadLocal物件,都是待回收的垃圾,如果要使用ThreadLocal,就必須要例項化該物件獲取get()方法,例項化就建立了強引用,基於這點GC不會回收有效的TheadLocal物件

為什麼繼承WeakReference

這裡我們衍生幾個問題:

  • ThreadLocal為什麼使用弱引用
  • 為什麼value不使用弱引用
  1. 我們先來說說為什麼要使用弱引用

在以往使用完物件後等著GC清理(方法出棧後失去引用,等待GC回收),但是對於ThreadLocal來說,即使我們使用完成後,因為該執行緒副本還存在該物件的引用,屬於物件可達(必須當前執行緒執行完成),否則永遠不會被回收,這是就會出現記憶體浪費的情況,當程式中物件過多,而這些本地執行緒中的強引用物件無法釋放,gc無法回收,則嚴重會出現程式崩潰的情況;往往這種問題是最難排查的,特別是線上程池場景,核心執行緒不會銷燬的場景下,執行緒永遠處於可達狀態,消耗的記憶體會越積越多;所以基於以下場景,將ThreadLocal物件設計成弱引用最合適不過;

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
  1. 為什麼value不使用弱引用

不設定為弱引用,是因為不清楚這個value除了map的引用還是否還存在其他引用,如果不存在其他引用,當GC的時候就會直接將這個value幹掉了,而此時我們的ThreadLocal還處於使用期間,就會造成value為null的錯誤,所以必須將其設定為強引用

ThreadLocal在什麼情況下會出現記憶體洩漏問題,如何避免記憶體洩漏?

避免方式:

  • current-thread 當前執行緒結束釋放引用
  • 手動呼叫 threadLocal.remove()方法清理
  • ThreadLocal在擴容和定址的時候都進行了清理操作,雖然還是會存在記憶體洩漏,但影響的記憶體不大

ThreadLocalMap的資料結構和HashMap有什麼區別

當我們檢視原始碼時可以看到以下程式碼:

hashmap hash處理:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

threadlocalMap hash處理:

private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);  
}

private static final int HASH_INCREMENT = 0x61c88647;  // 黃金分割數

本質上都是hash雜湊,HASH_INCREMENT:這個值很特殊,它是斐波那契數 也叫 黃金分割數hash增量為 這個數字,帶來的好處就是 hash分佈非常均勻

借大佬示例一用:

public static void main(String[] args) {
    final int HASH_INCREMENT = 0x61c88647;

    int hash = 0;
    for (int i = 0; i < 16; i++) {
        hash = i * HASH_INCREMENT + HASH_INCREMENT;
        int bucket = hash & 15;
        System.out.println(i + " 在桶中的位置:"+ bucket);
    }
}

列印結果:

0 在桶中的位置:7
1 在桶中的位置:14
2 在桶中的位置:5
3 在桶中的位置:12
4 在桶中的位置:3
5 在桶中的位置:10
6 在桶中的位置:1
7 在桶中的位置:8
8 在桶中的位置:15
9 在桶中的位置:6
10 在桶中的位置:13
11 在桶中的位置:4
12 在桶中的位置:11
13 在桶中的位置:2
14 在桶中的位置:9
15 在桶中的位置:0

在處理hash衝突時ThreadLocalMap 主要採用開放定址法,大家可以思考下為什麼ThreadLocalMap不和HashMap一樣用連結串列的方式,而採用線性儲存的方式; HashMap Jdk1.8 主要採用 連結串列轉紅黑樹的方式來處理,HashMap後續分析

ThreadLocalMap如何來解決Hash衝突

雖然ThreadLocalMap中使用了黃金分隔數來作為hash計算因子,大大減少了Hash衝突的概率,但是仍然會存在衝突。

HashMap中解決衝突的方法是在陣列上構造一個連結串列結構,衝突的資料掛載到連結串列上,如果連結串列長度超過一定數量則會轉化成紅黑樹

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {  // i++
        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();
}

在ThreadLocalMap set原始碼中可以看出,當我們通過 int i = key.threadLocalHashCode & (len-1) 計算出 hash 值,如果出現衝突,順序查看錶中下一單元,直到找出一個空單元或查遍全表。

正因為採取的線性探測法解決衝突,所以在查詢的時候,必須比較 key 值是否相等,否則順序尋找下一個單元

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

nextIndex就是遞增索引下標

ThreadLocalMap 擴容機制

if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

擴容後的tab的大小為oldLen * 2,然後遍歷老的散列表,重新計算hash位置,然後放到新的tab陣列中,如果出現hash衝突則往後尋找最近的entrynull的槽位,遍歷完成之後,oldTab中所有的entry資料都已經放入到新的tab中了。重新計算tab下次擴容的閾值

如下:

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();
            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;
}

ThreadLocal set() 和 get() 原始碼分析

set() 原始碼分析

可以參考大佬部落格:https://blog.csdn.net/l18848956739/article/details/106122096

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  // hash運算

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {  // 如果 key.hashCode 相等,則替換value值
            e.value = value;
            return;
        }

        if (k == null) {  //如果 key 為空,說明當前桶位置的Entry是過期資料,因為 Entry e = tab[i];e != null;符合條件就表示Entry肯定是有值的,這裡k為null,就代表被gc回收
            replaceStaleEntry(key, value, i);  //替換過期資料的邏輯
            return;
        }
    }

    tab[i] = new Entry(key, value);  // 如果陣列為空,則表示可以直接存放
    int sz = ++size;
    //啟發式清理,清理雜湊陣列中Entry的key過期的資料
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();  //擴容
}

replaceStaleEntry() 方法詳解

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;  //slotToExpunge表示開始探測式清理過期資料的開始下標,預設從當前的staleSlot開始
    for (int i = prevIndex(staleSlot, len);  // 先往前查詢,直到tab[i]為null會執行完迴圈
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)  // 如果沒有找到被回收的物件,則將 slotToExpunge 更新為i
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);  //然後向後查詢
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {   // 如果存在相同的ThreadLocal引用
            e.value = value; // 則將新的value 重新賦值

            tab[i] = tab[staleSlot];  // 替換新資料並且交換當前staleSlot位置
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)  // 代表prevIndex和nextIndex 都沒有找到null值並且符合 k== key的時候,走交換資料邏輯,並從當前索引往後進行過期清理
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  // 啟發式過期清理,從當前索引i開始往後清理
            return;
        }

        // 如果k != key則會接著往下走,k == null說明當前遍歷的Entry是一個過期資料,slotToExpunge == staleSlot說明,
        // 一開始的向前查詢資料並未找到過期的Entry。如果條件成立,則更新slotToExpunge 為當前位置,這個前提是前驅節點掃描時未發現過期資料
        if (k == null && slotToExpunge == staleSlot)  
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;   // set方法 if (k == null) 才走replaceStaleEntry方法,所以將value置空
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them 
    if (slotToExpunge != staleSlot) // 如果slotToExpunge 不為staleSlot,說明存在清理項,進行過期清理,補充上面的if(k == key) 的else邏輯
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

get原始碼分析

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)   //如果從Entry 中找到相同的物件,則直接返回
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}


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;
        if (k == null)
            expungeStaleEntry(i);  // 探測式清理
        else
            i = nextIndex(i, len);  // 遍歷下個索引,因為採用地址法儲存,則直接取下個索引
        e = tab[i];
    }
    return null;
}

get()方法相對簡單,先判斷hash位置是否存在key,如果存在並且和當前請求的ThreadLocal相同,則直接返回value,否則就往後遍歷查詢

ThreadLocalMap中過期key的清理機制?探測式清理和啟發式清理流程

上面多次提及到ThreadLocalMap過期可以的兩種清理方式:探測式清理(expungeStaleEntry())啟發式清理(cleanSomeSlots())探測式清理是以當前Entry 往後清理,遇到值為null則結束清理,屬於線性探測清理。而啟發式清理被作者定義為:Heuristically scan some cells looking for stale entries.

探測式清理:探測式清理,是以當前遇到的 GC 元素開始,向後不斷的清理。直到遇到 null 為止,才停止 rehash 計算

private int expungeStaleEntry(int staleSlot) {  //staleSlot 開始清理的索引下標
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;   //先把改索引的值置空;弱引用gc後Map可能為 null,value
    tab[staleSlot] = null;  // help gc
    size--;  //長度減少

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);   // 開始遍歷當前索引之後的所有key為空的陣列
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {   // k == null 會回收該條資料
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);  //h 重新hash運算的位置,因為前面回收了一些過期的資料
            if (h != i) {  // 如果重新計算的值不等於i,就說明存在回收,則重新更換位置,而且之前發生 Hash衝突 的Entry元素的位置應該更接近真實hash出來的位置
                tab[i] = null;
                while (tab[h] != null)  //一直往後找,找到為null的位置為止,因為中間會有些資料已經被回收了,會空留出記憶體,可以看看大佬的圖文分析
                    h = nextIndex(h, len);
                tab[h] = e;  //重新賦值
            }
        }
    }
    return i;
}

啟發式清理:

就是通過while迴圈的方式在探測式清理後在進行清除,nextIndex累計的次數為 m = n>>>=1 的次數,如果遍歷過程中,連續 m 次沒有發現過期的Entry,就可以認為陣列中已經沒有過期Entry了

private boolean cleanSomeSlots(int i, int n) {  //探測式清理後返回的數字下標,這裡至少保證了Hash衝突的下標至探測式清理後返回的下標這個區間無過期的Entry, n 陣列總長度
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {  // 如果發下過期的Entry則在執行探測性清理
            n = len;  //重置n
            removed = true;
            i = expungeStaleEntry(i);   //探測性清理
        }
    } while ( (n >>>= 1) != 0);  // 迴圈條件: m = logn/log2(n為陣列長度)
    return removed;
}

這個 m 的計算是 n >>>= 1 ,你也可以理解成是陣列長度的2的幾次冪。
例如:陣列長度是16,那麼24=16,也就是連續4次沒有過期Entry,即 m = logn/log2(n為陣列長度)

InheritableThreadLocal和TransmittableThreadLocal的作用

我們使用ThreadLocal的時候,在非同步場景下是無法給子執行緒共享父執行緒中建立的執行緒副本資料的,為了解決這個問題JDK提供了InheritableThreadLocal

public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        threadLocal.set("父類資料:threadLocal");
        inheritableThreadLocal.set("父類資料:inheritableThreadLocal");
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子執行緒獲取父類threadLocal資料:" + threadLocal.get());
                System.out.println("子執行緒獲取父類inheritableThreadLocal資料:" + inheritableThreadLocal.get());
            }
        }).start();
    }
}

列印結果:

子執行緒獲取父類threadLocal資料:null
子執行緒獲取父類inheritableThreadLocal資料:父類資料:inheritableThreadLocal

實現原理是子執行緒是通過在父執行緒中通過呼叫new Thread()方法來建立子執行緒,Thread#init方法在Thread的構造方法中被呼叫。在init方法中拷貝父執行緒資料到子執行緒中:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

TransmittableThreadLocal使用

InheritableThreadLocal仍然有缺陷,一般我們做非同步化處理都是使用的執行緒池,而InheritableThreadLocal是在new Thread中的init()方法給賦值的,而執行緒池是執行緒複用的邏輯,所以這裡會存在問題

如何使用TransmittableThreadLocal:

引用依賴

<properties>
    <ttl.version>2.11.4</ttl.version>
</properties>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>${ttl.version}</version>
</dependency>
public class TransmittableThreadLocalTest {

    private static ThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    /** 保證只有1個執行緒,以便觀察這個執行緒被多個Runnable複用時,能否成功完成ThreadLocal的傳遞 **/
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
    );

    public static void main(String[] args) throws InterruptedException {
        ttl.set("a");

        for (int i = 0; i < 5; i++) {
            if (i == 1) {
                ttl.set("b");
            }
            TtlRunnable runnable = TtlRunnable.get(() -> {
                System.out.println(Thread.currentThread().getName() + " : " + ttl.get());
            });

            threadPoolExecutor.execute(runnable);
            TimeUnit.MILLISECONDS.sleep(500);
        }
    }
}

列印結果:

pool-1-thread-1 : a
pool-1-thread-1 : a
pool-1-thread-1 : b
pool-1-thread-1 : b
pool-1-thread-1 : b

後面在深入原始碼分析

ThreadLocal在系統中的使用場景有哪些

  • Web請求的使用者身份態:Session
  • 請求的鏈路跟蹤:traceId
  • SimpleDateFormat轉換時間問題:因為SimpleDateFormat不是執行緒安全的,用ThreadLocal能提升轉換效率
  • 系統鏈路日誌等