天天看點

ThreadLocal源碼奪命12問,你能堅持到第幾問?

本篇總結的是 并發程式設計 ThreadLocal源碼面試題,後續會每日更新~

ThreadLocal 源碼深入分析參考我的往期部落格:ThreadLocal源碼分析_01 入門案例以及表層源碼分析、ThreadLocal源碼分析_02 核心(ThreadLocalMap)

注:不了解 ThreadLocal 如何使用以及其原理的小夥伴,一定要先看上面的兩篇部落格哦

ThreadLocal源碼奪命12問,你能堅持到第幾問?
ThreadLocal源碼奪命12問,你能堅持到第幾問?

、面試官:請你說一說你對ThreadLocal的了解?

ThreadLocal 是一個全局對象,ThreadLocal 是線程範圍内變量共享的解決方案;ThreadLocal 可以看作是一個map集合,key就是目前線程,value就是要存放的變量。

參考回答:

ThreadLocal 對象可以給每個線程配置設定一份屬于自己的局部變量副本,多個線程之間可以互不幹擾。一般我們會重寫 initalValue()方法來給目前 ThreadLocal 對象賦初始值。

2、面試官:簡單描述一下JDK1.8中,ThreadLocal原理?

JDK8 中,每個線程對象 Thread 類内部都有一個成員屬性 threadLocals(即ThreadLocalMap,它是一個Entry[]數組,而不是 Map 集合哦~),各個線程在調用同一個 ThreadLocal 對象的set(value)方法設定值的時候,就是往各自的 ThreadLocalMap 對象數組中新增值。

ThreadLocalMap (Entry[]數組)中存放的是一個個的 Entry節點,它有兩個屬性字段,弱引用 key(ThreadLocal對象) ,和強引用 value (目前線程變量副本的值)。

3、面試官:ThreadLocal是怎樣坐到線程互不幹擾的呢(線程隔離)?

首先,每個線程 Thread 都有一份屬于自己的 ThreadLoacalMap 用于存儲資料。

當線程通路某個 ThreadLocal 對象的 get()方法時,方法内部會檢測該線程的 ThreadLoacalMap 數組(Entry[])内是否存在 key 為目前 ThreadLocal 對象的 Entry 節點。如果數組内沒有對應的節點,那麼目前 ThreadLocal 對象就會調用其内部的 initialValue() 方法建立一個 Entry 節點存放到 ThreadLocalMap 中去。

4、面試官:ThreadLocal 使用的 hash 是怎樣計算得來的?

首先,ThreadLocal 使用的 hash 并不是重寫自 Object 的 hashCode() 方法,而是通過自身的nextHashCode();計算得來。代碼如下:

// threadLocalHashCode ---> 用于threadLocals的桶位尋址:
// 1.線程擷取threadLocal.get()時:
//      如果是第一次在某個threadLocal對象上get,那麼就會給目前線程配置設定一個value,
//      這個value 和 目前的threadLocal對象被包裝成為一個 entry 
//      其中entry的 key 是threadLocal對象,value 是threadLocal對象給目前線程生成的value
// 2.這個entry存放到目前線程 threadLocals 這個map的哪個桶位呢? 
//      桶位尋址與目前 threadLocal對象的 threadLocalHashCode有關系:
//      使用 threadLocalHashCode & (table.length - 1) 計算結果得到的位置就是目前 entry 需要存放的位置。
private final int threadLocalHashCode = nextHashCode();

// nextHashCode: 表示hash值
// 建立ThreadLocal對象時會使用到該屬性:
// 每建立一個threadLocal對象時,就會使用 nextHashCode 配置設定一個hash值給這個對象。
private static AtomicInteger nextHashCode = new AtomicInteger();

// HASH_INCREMENT: 表示hash值的增量~
// 每建立一個ThreadLocal對象,ThreadLocal.nextHashCode的值就會增長HASH_INCREMENT(0x61c88647)。
// 這個值很特殊,它是斐波那契數也叫黃金分割數。
// hash增量為這個數字,帶來的好處就是hash分布非常均勻。
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 傳回一個nextHashCode的hash值:
 * 建立新的ThreadLocal對象時,使用這個方法,會給目前對象配置設定一個hash值。
 */
private static int nextHashCode() {
    // 每建立一個對象,nextHashCode計算得到的hash值就增長HASH_INCREMENT(0x61c88647)
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

/*
 * 初始化一個起始value:
 * 預設傳回null,一般情況下都是需要重寫這個方法的(例如第2小節的入門案例中就重寫了該方法)。
 */
protected T initialValue() {
    return null;
}
      

5、面試官:為什麼 ThreadLocalMap 選擇去重新設計"Map",而不直接使用 JDK中的 HashMap呢?

因為 ThreadLocal 自己重新設計的 Map,它可以把自己的 Key 限定為特有類型(ThreadLocal),這個特定類型的Key 使用的是弱引用 WeakReference>,而 HashMap 中的 Key 采用的是強引用方式。

6、面試官:ThreadLocalMap的Enrty的key為什麼要設定成弱引用?

ThreadLocalMap 存儲的格式是 Entry。如果使用強引用,當 key 原來對象失效的時候,jvm不會回收 map 裡面的 ThreadLocal。

弱引用 WeakReference定義:如果一個對象隻具有弱引用,那麼垃圾回收器在掃描到該對象時,無論記憶體充足與否,都會回收該對象的記憶體。

ThreadLocalMap 使用 ThreadLocal 的弱引用作為 key,如果一個 ThreadLocal 沒有外部強引用引用他,那麼系統 GC 的時候,這個 ThreadLocal 勢必會被回收,這樣一來,ThreadLocalMap 中就會出現 key 為 null 的 Entry,就沒有辦法通路這些 key 為 null 的 Entry 的 value。

站在 ThreadLocalMap 角度就可以區分出哪些 Entry 是過期的,哪些 Entry 是非過期的。

例如:在set()方法向下尋找可用 solt 桶位的過程中,如果碰到key == null 的情況,說明目前Entry 是過期資料,這個時候可以強行占用該桶位,通過replaceStaleEntry方法執行替換過期資料的邏輯。

例如:cleanSomeSlots(int i, int n)方法通過周遊桶位,也會将 key == null 過期資料清理掉。

7、面試官:ThreadLocalMap 對象是何時第一次被建立呢?

每個線程 Thread 對象的 ThreadLocalMap 都是延遲初始化的,當我們在調用 ThreadLocal 對象的 set() 或 get()方法時,它會檢測目前線程是否已經綁定了 ThreadLocalMap,如果已經綁定,則繼續執行 set() 或 get()方法的邏輯。

而如果沒有,則會先建立 ThreadLocalMap 并将其綁定給 Thread 對象。

面試官:那麼線程的 ThreadLocalMap 會被多次建立嗎?

不會,線上程的生命周期内,ThreadLocalMap 對象隻會被初始化一次。

8、面試官:ThreadLocalMap 的初始化長度是多少呢?

初始化時,ThreadLocalMap 容量為 16。

9、面試官:上面你說初始化長度是16,那為什麼初始容量要是2的N次幂數呢?

這個設計它和 HashMap 是一樣的,目的都是為了友善 hash 尋址時,得到的 index (桶位)更均勻分布,減少 hash 沖突。

尋址算法為:index = threadLocalHashCode & (table.length - 1)。這個算法實際就是取模運算:hash % tab.length,而計算機中直接求餘運算效率不如位移運算。

是以源碼中做了優化,使用 hash & (tab.length- 1)來尋找桶位。而實際上 hash % length 等于 hash & ( length - 1) 的前提是 length 必須為 2 的 n 次幂。

例如,數組長度 tab.length = 8 的時候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(數組索引) 3 和 2,不同位置上,不發生 hash 碰撞。

10、面試官:ThreadLocalMap 的 擴容門檻值是多少?它的擴容機制是怎樣的?

首先,ThreadLocalMap 的擴容門檻值為初始容量的 2/3,當數組中,存儲 Entry 節點的個數大于等于 2/3 時,會它并不會直接開始擴容。

而是先調用 rehash()方法,在該方法中,全面掃描整個數組,并将數組中過期的資料(key == null)給清理掉,重新整理數組。

如果重新整理數組,并将過期的資料清理後,再次重新判斷數組内的 Entry 節點的個數是否達到擴容門檻值的3/4,如果達到再調用真正擴容的方法resize();

面試官:那麼你對 resize() 方法内部的擴容算法了解嗎?

resize() 方法在真正執行擴容時,内部邏輯是先建立一個新的數組,新數組長度是原來數組長度的 2 倍。

然後周遊舊數組,将舊數組中的資料重新按照 hash 算法遷移到新數組裡面。

接着重新計算出下次擴容的門檻值threshold。

最後更新 Thread 對象的 threadLocals 字段引用,使其指向新數組。

11、面試官:請你說一下 ThreadLocal 的 get 方法的執行流程?

① 首先 get() 方法中會先擷取目前線程對象 t : Thread t = Thread.currentThread();

② 接下來根據 t 擷取其獨有的 ThreadLocalMap 數組:ThreadLocalMap map = getMap(t);

③ 如果 ② 擷取的 map為空,則調用setInitialValue()方法,該方法内部調用 initialValue();方法擷取 value,并根據 目前線程t 和 value 調用 createMap(t, value); 方法建立 ThradLocalMap。

④ 如果 ② 擷取的 map不為空,則直接調用 ThreadLocalMap.Entry e = map.getEntry(this); 方法通過 this(目前ThreadLocal對象)從 ThreadLocalMap 中擷取對應封裝資料的 Entry 節點。

⑤ 最終通過 T result = (T)e.value; 得到要擷取的線程變量副本的值。

注意:

第 ④ 步中,通過目前 ThreadLocal 對象從 ThreadLocalMap 中擷取對應封裝資料的 Entry 節點時,内部邏輯是需要涉及到桶位尋址 index = threadLocalHashCode & (table.length - 1),如果擷取的 inde 桶位中沒有目标資料,這時候會執行``nextIndex(int i, int len)方法,**線性的向前或者向後去尋找目标資料所在的桶位,直到周遊整個數組仍未找到,則傳回null`**。

此外,線上性的向前、向後周遊數組尋找目标元素所在的桶位時,如果發現資料過期了(key == null),則需要調用expungeStaleEntry(i);方法進行一次探測式過期資料回收。

12、面試官:請你說一下 ThreadLocal 的 set 方法的執行流程?

① 首先,set()方法向 ThreadLocalMap 中添加資料時,也是需要根據 Key (ThreadLocal對象) 的去尋址找到要插入的桶位下标 i = key.threadLocalHashCode & (len-1);

② 根據桶位下标,擷取對應桶中的Enety 對象Entry e = tab[i];,如果擷取的 e 為 null ,則說明是空桶,直接講 Key 和 Value 包裝成 Entry 放入桶中即可:tab[i] = new Entry(key, value);

③ 如果第 ② 步驟擷取的 e 不為 null,說明不是空桶,則需要從以下三種情況考慮:

如果目前桶中 Entry 的 Key 不是目前 ThreadLocal 對象,且不為 null,則調用nextIndex(int i, int len)方法線性查找下一個空桶位,并将新資料放入。

如果目前桶中 Entry 的 Key 是目前 ThreadLocal 對象,則通過更新操作,将就 Entry 的 Value 值覆寫。

如果如果目前桶中 Entry 的 Key 是null,則說明目前 Entry 已經過期,需要執行 替換過期資料的邏輯: replaceStaleEntry(key, value, i);。

總結的面試題也挺費時間的,文章會不定時更新,有時候一天多更新幾篇,如果幫助您複習鞏固了知識點,還請三連支援一下,後續會億點點的更新!

ThreadLocal源碼奪命12問,你能堅持到第幾問?