ThreadLocal在JDK中是一個很是重要的工具類,經過閱讀源碼,能夠在各大框架都能發現它的蹤跡。它最經典的應用就是 事務管理 ,同時它也是面試中的常客。
今天就來聊聊這個ThreadLocal;本文主線:java
①、ThreadLocal 介紹面試
②、ThreadLocal 實現原理數組
③、ThreadLocal 內存泄漏分析安全
④、ThreadLocal 應用場景及示例數據結構
注:本文源碼基於 JDK1.8
正如 JDK 註釋中所說的那樣: ThreadLocal 類提供線程局部變量,它一般是私有類中但願將狀態與線程關聯的靜態字段。
簡而言之,就是 ThreadLocal 提供了線程間數據隔離的功能,從它的命名上也能知道這是屬於一個線程的本地變量。也就是說,每一個線程都會在 ThreadLocal 中保存一份該線程獨有的數據,因此它是線程安全的。多線程
熟悉 Spring 的同窗可能知道 Bean 的做用域(Scope),而 ThreadLocal 的做用域就是線程。框架
下面經過一個簡單示例來展現一下 ThreadLocal 的特性:函數
public static void main(String[] args) { ThreadLocal<String> threadLocal = new ThreadLocal<>(); // 建立一個有2個核心線程數的線程池 ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10)); // 線程池提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中 threadPool.execute(() -> threadLocal.set("任務1: " + Thread.currentThread().getName())); threadPool.execute(() -> threadLocal.set("任務2: " + Thread.currentThread().getName())); threadPool.execute(() -> threadLocal.set("任務3: " + Thread.currentThread().getName())); // 輸出 ThreadLocal 中的內容 for (int i = 0; i < 10; i++) { threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get())); } // 線程池記得關閉 threadPool.shutdown(); }
上面代碼首先建立了一個有2個核心線程數的普通線程池,隨後提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中,最後在一個 for 循環中輸出線程池中各個線程存儲在 ThreadLocal 中的值。
這個程序的輸出結果是:工具
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1 ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2 ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1 ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2 ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1 ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2 ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1 ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2 ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1 ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
因而可知,線程池中執行提交的任務的是名爲 pool-1-thread-1 的線程,隨後屢次輸出線程池核心線程在 ThreadLocal 變量中存儲的的內容也代表:每一個線程在 ThreadLocal 中存儲的內容是當前線程獨有的,在多線程環境下,可以有效防止本身的變量被其餘線程修改(存儲的內容是同一個引用類型對象的狀況除外)。this
在 JDK1.8 版本中 ThreadLocal 類的源碼總共723行,去掉註釋大概有350行,應該算是 JDK 核心類庫中代碼量比較少的一個類了,相對來講它的源碼仍是挺容易理解的。
下面,就從 ThreadLocal 的數據結構開始聊聊它的實現原理吧。
ThreadLocal 底層是經過 ThreadLocalMap 這個靜態內部類來存儲數據的,ThreadLocalMap 就是一個鍵值對的 Map,它的底層是 Entry 對象數組,Entry 對象中存放的鍵是 ThreadLocal 對象,值是 Object 類型的具體存儲內容。
除此以外,ThreadLocalMap 也是 Thread 類一個屬性。
如何證實上面給出的 ThreadLocal 類底層數據結構的正確性?
咱們能夠從 ThreadLocal#get() 方法開始追蹤代碼,看看線程局部變量究竟是從哪裏被取出來的。
public T get() { // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量 ThreadLocalMap map = getMap(t); // 若 threadLocals 變量不爲空,根據 ThreadLocal 對象來獲取 key 對應的 value if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 若 threadLocals 變量是 NULL,初始化一個新的 ThreadLocalMap 對象 return setInitialValue(); } // ThreadLocal#setInitialValue // 初始化一個新的 ThreadLocalMap 對象 private T setInitialValue() { // 初始化一個 NULL 值 T value = initialValue(); // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } // ThreadLocalMap#createMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
經過 ThreadLocal#get() 方法能夠很清晰的看到,咱們根據 ThreadLocal 對象從 ThreadLocal 中讀取數據時,首先會獲取當前線程對象,而後獲得當前線程對象中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性;
若是 threadLocals 屬性不爲空,會根據 ThreadLocal 對象做爲 key 來獲取 key 對應的 value;若是 threadLocals 變量是 NULL,就初始化一個新的ThreadLocalMap 對象。
再看 ThreadLocalMap 的構造方法,也就是 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性不爲空時的執行邏輯。
// 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); }
這個構造方法實際上是將 ThreadLocal 對象做爲 key,存儲的具體內容 Object 對象做爲 value,包裝成一個 Entry 對象,放到 ThreadLocalMap 類中類型爲 Entry 數組的 table 屬性中,這樣就完成了線程局部變量的存儲。
因此說, ThreadLocal 中的數據最終是存放在 ThreadLocalMap 這個類中的 。
在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中我寫了一行註釋:
// 獲取當前 ThreadLocal 對象的散列值 int i = key.threadLocalHashCode & (len-1);
這行代碼獲得的值實際上是一個 ThreadLocal 對象的散列值,這就是 ThreadLocal 的散列方式,咱們稱之爲 斐波那契散列 。
// ThreadLocal#threadLocalHashCode private final int threadLocalHashCode = nextHashCode(); // ThreadLocal#nextHashCode private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // ThreadLocal#nextHashCode private static AtomicInteger nextHashCode = new AtomicInteger(); // AtomicInteger#getAndAdd public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } // 魔數 ThreadLocal#HASH_INCREMENT private static final int HASH_INCREMENT = 0x61c88647;
key.threadLocalHashCode 所涉及的函數及屬性如上所示,每個 ThreadLocal 的 threadLocalHashCode 屬性都是基於魔數 0x61c88647 來生成的。
這裏就不討論選擇這個魔數的緣由了(實際上是我看不太懂),總之大量的實踐證實: 使用 0x61c88647 做爲魔數生成的 threadLocalHashCode 再與2的冪取餘,獲得的結果分佈很均勻。
注: 對 A 進行2的冪取餘操做 A % 2^N 能夠經過 A & (2^n-1) 來代替,位運算的效率比取模效率高不少。
咱們已經知道 ThreadLocalMap 類的底層數據結構是一個 Entry 類型的數組,但與 HashMap 中的 Node 類數組+鏈表形式不一樣的是,Entry 類沒有 next 屬性來構成鏈表,因此它是一個單純的數組。
就算上面所說的 斐波那契散列法 真的可以充分散列,但不免仍是可能會發生哈希碰撞,那麼問題來了,Entry 數組是如何解決哈希衝突的?
這就須要拿出 ThreadLocal#set(T value) 方法了,而具體處理哈希衝突的邏輯是在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中的:
public void set(T value) { // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量 ThreadLocalMap map = getMap(t); // 若 threadLocals 變量不爲空,進行賦值;不然新建一個 ThreadLocalMap 對象來存儲 if (map != null) map.set(this, value); else createMap(t, value); } // ThreadLocalMap#set private void set(ThreadLocal<?> key, Object value) { // 獲取 ThreadLocalMap 的 Entry 數組對象 Entry[] tab = table; int len = tab.length; // 基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值 int i = key.threadLocalHashCode & (len-1); // 解決哈希衝突,線性探測法 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 代碼(1) if (k == key) { e.value = value; return; } // 代碼(2) if (k == null) { replaceStaleEntry(key, value, i); return; } } // 代碼(3)將 key-value 包裝成 Entry 對象放在數組退出循環時的位置中 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } // ThreadLocalMap#nextIndex // Entry 數組的下一個索引,若超過數組大小則從0開始,至關於環形數組 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
具體分析處理哈希衝突的 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法,能夠看到,在拿到 ThreadLocal 對象的散列值以後進入了一個 for 循環,循環的條件也很清楚:從 Entry 數組的 ThreadLocal 對象散列值處開始,每次向後挪一位,若是超過數組大小則從0開始繼續遍歷,直到 Entry 對象爲 NULL 爲止。
在循環過程當中:
至此,咱們分析完了在向 ThreadLocal 中存儲數據時,拿到 ThreadLocal 對象散列值以後的邏輯,回到本小節的主題—— ThreadLocal 是如何解決哈希衝突的?
由上面的代碼能夠知道,在基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值以後進入了一個循環,在循環中是處理具體處理哈希衝突的方法:
因此,來總結一下 ThreadLocal 處理哈希衝突的方式就是:若是在 set 時遇到哈希衝突,ThreadLocal 會經過線性探測法嘗試在數組下一個索引位置進行存儲,同時在 set 過程當中 ThreadLocal 會釋放 key 爲 NULL,value 不爲 NULL 的髒 Entry對象的 value 屬性來防止內存泄漏 。
在上文中有提到過 ThreadLocalMap 的構造方法,這裏詳細說明一下。
// ThreadLocalMap 構造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { // 初始化 Entry 數組 table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; // 設置擴容條件 setThreshold(INITIAL_CAPACITY); }
ThreadLocalMap 的初始容量是 16:
// 初始化容量 private static final int INITIAL_CAPACITY = 16;
下面聊一下 ThreadLocalMap 的擴容機制 ,它在擴容前有兩個判斷的步驟,都知足後纔會進行最終擴容:
// rehash 條件 private void setThreshold(int len) { threshold = len * 2 / 3; }
// 擴容條件 private void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }
// 具體的擴容函數 private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; 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; }
咱們已經知道 ThreadLocal 中存儲的是線程的局部變量,那若是如今有個需求,想要實現線程間局部變量傳遞,這該如何實現呢?
大佬們早已料到會有這樣的需求,因而設計出了 InheritableThreadLocal 類。
InheritableThreadLocal 類的源碼除去註釋以外一共不超過10行,由於它是繼承於 ThreadLocal 類,不少東西在 ThreadLocal 類中已經實現了,InheritableThreadLocal 類只重寫了其中三個方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }
咱們先用一個簡單的示例來實踐一下父子線程間局部變量的傳遞功能。
public static void main(String[] args) { ThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); threadLocal.set("這是父線程設置的值"); new Thread(() -> System.out.println("子線程輸出:" + threadLocal.get())).start(); } // 輸出內容 子線程輸出:這是父線程設置的值
能夠看到,在子線程中經過調用 InheritableThreadLocal#get() 方法,拿到了在父線程中設置的值。
那麼,這是如何實現的呢?
實現父子線程間的局部變量共享須要追溯到 Thread 對象的構造方法:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, // 該參數通常默認是 true boolean inheritThreadLocals) { // 省略大部分代碼 Thread parent = currentThread(); // 複製父線程的 inheritableThreadLocals 屬性,實現父子線程局部變量共享 if (inheritThreadLocals && parent.inheritableThreadLocals != null) { this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); } // 省略部分代碼 }
在最終執行的構造方法中,有這樣一個判斷:若是當前父線程(建立子線程的線程)的 inheritableThreadLocals 屬性不爲 NULL,就會將當下父線程的 inheritableThreadLocals 屬性複製給子線程的 inheritableThreadLocals 屬性。具體的複製方法以下:
// ThreadLocal#createInheritedMap static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; // 一個個複製父線程 ThreadLocalMap 中的數據 for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // childValue 方法調用的是 InheritableThreadLocal#childValue(T parentValue) Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
須要注意的是,複製父線程共享變量的時機是在建立子線程時,若是在建立子線程後父線程再往 InheritableThreadLocal 類型的對象中設置內容,將再也不對子線程可見。
最後再來講說 ThreadLocal 的內存泄漏問題,衆所周知,若是使用不當,ThreadLocal 會致使內存泄漏。
內存泄漏 是指程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。
而 ThreadLocal 發生內存泄漏的緣由須要從 Entry 對象提及。
// ThreadLocal->ThreadLocalMap->Entry static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry 對象的 key 即 ThreadLocal 類是繼承於 WeakReference 弱引用類。具備弱引用的對象有更短暫的生命週期,在發生 GC 活動時,不管內存空間是否足夠,垃圾回收器都會回收具備弱引用的對象。
因爲 Entry 對象的 key 是繼承於 WeakReference 弱引用類的,若 ThreadLocal 類沒有外部強引用,當發生 GC 活動時就會將 ThreadLocal 對象回收。
而此時若是建立 ThreadLocal 類的線程依然活動,那麼 Entry 對象中 ThreadLocal 對象對應的 value 就依舊具備強引用而不會被回收,從而致使內存泄漏。
要想解決內存泄漏問題其實很簡單,只須要記得在使用完 ThreadLocal 中存儲的內容後將它 remove 掉就能夠了。
這是主動防止發生內存泄漏問題的手段,但其實設計 ThreadLocal 的大神固然也發現了 ThreadLocal 可能引起內存泄漏的問題,因此他們也設計了相應的手段來防止內存泄漏。
在上文中描述 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 其實已經有涉及 ThreadLocal 內部清理無效 Entry 的邏輯了,在經過線性檢測法處理哈希衝突時,若 Entry 數組的 key 與當前 ThreadLocal 不是同一個對象,同時 key 爲空的時候,會進行 清理無效 Entry 的處理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) 方法:
若是知足線性檢測循環結束條件了,即遇到了 Entry==NULL 的狀況,就新建一個 Entry 對象來存儲數據。而後會進行一次啓發式清理,若是啓發式清理沒有成功釋放知足條件的對象,同時知足擴容條件時,會執行 ThreadLocalMap#rehash() 方法。
private void rehash() { // 全量清理 expungeStaleEntries(); // 知足條件則擴容 if (size >= threshold - threshold / 4) resize(); }
ThreadLocalMap#rehash() 方法中會對 ThreadLocalMap 進行一次全量清理,全量清理會遍歷整個 Entry 數組,刪除全部 key 爲 NULL,value 不爲 NULL 的髒 Entry對象。
// 全量清理 private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.get() == null) expungeStaleEntry(j); } }
進行全量清理以後,若是 Entry 數組的大小大於等於 threshold - threshold / 4 ,則會進行2倍擴容。
總結一下:在ThreadLocal 內部是經過在 get、set、remove 方法中主動進行清理 key 爲 NULL 且 value 不爲 NULL 的無效 Entry 來避免內存泄漏問題。
可是基於 get、set 方法讓 ThreadLocal 自行清理無效 Entry 對象並不能徹底避免內存泄漏問題,要完全解決內存泄漏問題還得養成使用完就主動調用 remove 方法釋放資源的好習慣。
ThreadLocal 在不少開源框架中都有應用,好比:Spring 中的事務隔離級別的實現、MyBatis 分頁插件 PageHelper 的實現。
同時,我在項目中也有基於 ThreadLocal 與過濾器實現接口白名單的鑑權功能。
以面試題的形式來總結一下關於 ThreadLocal 本文所描述的內容:
您能夠VX搜索【木子雷】公衆號,堅持高質量原創java技術文章,福利多多喲!
若是本文對你們有幫助的話,請多多點贊評論呀,大家的支持就是我不斷創做的動力!