追蹤解析 ThreadLocal 原始碼
零 前期準備
0 FBI WARNING
文章異常囉嗦且繞彎。
1 版本
JDK 版本 : OpenJDK 11.0.1
IDE : idea 2018.3
2 ThreadLocal 簡介
ThreadLocal 是 java 多執行緒中經常使用到的快取工具,被封裝在 java.lang 包下。
3 Demo
import io.netty.util.concurrent.FastThreadLocal; public class ThreadLocalDemo { public static void main(String[] args) { //jdk 的 ThreadLocal ThreadLocal<String> tl = new ThreadLocal<>(); long tlBeginTime = System.nanoTime(); //set(...) 方法存入元素 tl.set("test"); //get() 方法獲取元素 String get = tl.get(); System.out.println("tl before remove: " + get); //remove() 方法刪除元素 tl.remove(); get = tl.get(); System.out.println("tl after remove: " + get); System.out.println(System.nanoTime() - tlBeginTime); //以下程式碼為著名 io 框架 Netty 的 FastThreadLocal 類的使用 //FastThreadLocal,基本的使用方法和 ThreadLocal 沒有區別 //FastThreadLocal 的例項物件建立比較慢,但是元素的獲取、增、刪的效能很好 FastThreadLocal<String> fastTl = new FastThreadLocal<>(); long fastTlBeginTime = System.nanoTime(); fastTl.set("test"); String fastGet = fastTl.get(); System.out.println("tl2 before remove: " + fastGet); fastTl.remove(); fastGet = fastTl.get(); System.out.println("tl2 after remove: " + fastGet); System.out.println(System.nanoTime() - fastTlBeginTime); //此處的 Netty 使用 4.1.33.Final 的版本 //筆者跑了一下,FastThreadLocal 的增刪查操作大概比 ThreadLocal 快十倍 //但是此處僅為簡陋測試,並不嚴謹 } }
FastThreadLocal 的原始碼暫不展開,將來有機會單獨開一章去學習。這裡先理解 ThreadLocal。
一 ThreadLocalMap
在瞭解 ThreadLocal 的全貌之前先來理解一下 ThreadLocalMap 類。
其為 ThreadLocal 的靜態內部類。雖然類名中帶有 map 字樣,但是實際上並不是 Map 介面的子類。
ThreadLocalMap 本質上是陣列。每個 Thread 例項物件都會維護多個 ThreadLocalMap 物件:
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
但是需要注意的是,在預設情況下,執行緒物件的 ThreadLocalMap 物件們都是未初始化的,需要使用 createMap(...) 方法去初始化:
//ThreadLocal.class void createMap(Thread t, T firstValue) { //此處 ThreadLocal 將自身作為 key 值存入了 map 中 t.threadLocals = new ThreadLocalMap(this, firstValue); }
可以想到的是,此處是為了提高執行緒的效能,而設計了一個懶載入(Lazy)的呼叫模式。
[但是實際上這是理想情況,對於主執行緒來說,Collections、StringCoding 等的工具類在 jdk 載入時期就會呼叫 ThreadLocal,所以 ThreadLocalMap 肯定會被建立好]
再來看一下 ThreadLocalMap 的構造方法:
//ThreadLocalMap.class ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //Entry 是 ThreadLocalMap 的靜態內部類,代表節點的物件 //table 是一個 Entry 陣列,代表連結串列 table = new Entry[INITIAL_CAPACITY]; //這裡呼叫 key 的 hash 值進行陣列下標計算 //INITIAL_CAPACITY 為常量 16 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; //threshold = INITIAL_CAPACITY * 2 / 3 setThreshold(INITIAL_CAPACITY); }
Entry
Entry 是 ThreadLocalMap 的靜態內部類,本質上是陣列的節點 value 的封裝:
static class Entry extends WeakReference<ThreadLocal<?>> { //儲存的 value 值 Object value; Entry(ThreadLocal<?> k, Object v) { //呼叫父類的方法,會將 ThreadLocal 存入 Reference 中的 referent 物件中 super(k); value = v; } }
由上可知 Entry 繼承了 WeakReference。WeakReference 是弱連線介面,這意味著如果僅有 Entry 指向某一 ThreadLocal 類,其任然有可能被 GC 回收掉。
這裡使用弱連線的意義,是為了防止業務程式碼中置空 ThreadLocal 物件,但是由於存在連線可達,所以仍然無法回收掉該物件的情況發生。 即可以這麼說,如果使用者在業務程式碼中存在可達的強連線引用物件,那麼 ThreadLocal 永遠不會被 GC 清理掉;但是如果強連線消失了,那麼弱連線並不能保證它一定存活。當然換句話說,強連線消失的時候,證明使用者已經不需要這個物件了,那麼它被消滅也是應該的。
二 存入元素
來看一下 ThreadLocal 的 set(...) 方法:
//step 1 //ThreadLocal.class public void set(T value) { //獲取當前執行緒的例項物件 Thread t = Thread.currentThread(); //通過例項物件獲取到 map //map 實際上是定義在 Thread 類中的 ThreadLocalMap 型別的物件 ThreadLocalMap map = getMap(t); if (map != null) { //存入元素 map.set(this, value); } else { //如果 map 不存在,會在這裡建立 map createMap(t, value); } } //step 2 //ThreadLocalMap.class private void set(ThreadLocal<?> key, Object value) { //獲取陣列 table Entry[] tab = table; //獲取長度 int len = tab.length; //根據 hash 值算出下標 int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //nextIndex(...) 方法獲取陣列的下一個下標的元素 //基本等同於 i + 1,但是一般情況下不需要用到 //從節點中獲取 ThreadLocal 物件 ThreadLocal<?> k = e.get(); //正常情況下 k == key,第一次存值的時候 value = null if (k == key) { e.value = value; return; } //正常情況下不會出現 if (k == null) { replaceStaleEntry(key, value, i); return; } } //進入此處語句的條件是 k 並不為 null,且 key 不等於陣列內現存的所有 ThreadLocal //則在此處符合要求的下標處新建一個節點,並新增到 table 陣列中 //注意,這裡其實是覆蓋操作,會覆蓋掉之前在此下標處的節點 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
三 獲取元素
來看一下 ThreadLocal 的 get() 方法:
//step 1 //ThreadLocal.class public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //map 為 null 的情況下會進入該方法 //此處會將 null 作為 value,當前 ThreadLocal 作為 key,傳入 ThreadLocalMap 中 return setInitialValue(); } //step 2 //ThreadLocalMap.class 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 //此處會輪詢整個陣列去尋找,實在找不到會返回 null return getEntryAfterMiss(key, i, e); }
基本邏輯和 set(...) 方法差不多,不多贅述。
四 移除元素
來看一下 ThreadLocal 的 remove() 方法:
//step 1 //ThreadLocalMap.class public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { //呼叫 ThreadLocalMap 的 remove(...) 方法 m.remove(this); } } //step 2 //ThreadLocalMap.class private void remove(ThreadLocal<?> key) { 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)]) { //此處是一個和 set(...) 中很像的輪詢方法 //比對 key 值,如果相等的話會呼叫 clear() 方法清理掉 if (e.get() == key) { e.clear(); //此方法用於清理 key 值為 null 的節點 expungeStaleEntry(i); return; } } } //step 3 //Reference.class public void clear() { //Reference 是 WeakReference 的父類,即也就是 Entry 的父類 //將值置空 this.referent = null; }
五 ThreadLocal 的 hash 值
上述方法多次使用到了用 hash 去計算陣列下標的操作。如果不同 ThreadLocal 的 hash 值相同,那麼就會造成計算出來的下標相同,會相互影響存入的值。
所以 ThreadLocal 的 hash 值一定不能相同。
在 ThreadLocal 中,hash 值是一個 int 型別的變數:
private final int threadLocalHashCode = nextHashCode();
其呼叫了靜態方法 nextHashCode() 去產生 hash 值:
//ThreadLocal.class private static int nextHashCode() { //HASH_INCREMENT = 0x61c88647 (一個很神奇的用來解決 hash 衝突的數字) //nextHashCode 是一個定義在 ThreadLocal 中的靜態 AtomicInteger 型別變數 //getAndAdd(...) 方法會每次給 nextHashCode 的值加上 HASH_INCREMENT 的值,並返回最終的相加結果值 return nextHashCode.getAndAdd(HASH_INCREMENT); }
jdk9 以後官方應該比較希望使用 VarHandler 類來取代 Atomic 類,所以在不久的未來,很可能相關方法會有一些變動。
六 一點嘮叨
ThreadLocal 的原始碼還是比較簡潔的,方法封裝不多,讀起來不算費勁,有一些演算法層面的東西比較麻煩,但是不影響閱讀。
Netty 的 FastThreadLocal,其設計就要比 ThreadLocal 複雜得多,有機會再深入學習。
本文僅為個人的學習筆記,可能存在錯誤或者表述不清的地方,有緣補充