本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
大家好,我是小彭。
在前面的文章裡,我們聊到了雜湊表的開放定址法和分離連結串列法,也聊到了 HashMap、LinkedHashMap 和 WeakHashMap 等基於分離連結串列法實現的雜湊表。
今天,我們來討論 Java 標準庫中一個使用開放定址法的雜湊表結構,也是 Java & Android 「面試八股文」 的標準題庫之一 —— ThreadLocal。
本文原始碼基於 Java 8 ThreadLocal。
思維導圖:
在開始分析 ThreadLocal 的實現原理之前,我們先回顧雜湊表的工作原理。
雜湊表是基於雜湊思想實現的 Map 資料結構,將雜湊思想應用到雜湊表資料結構時,就是通過 hash 函數提取鍵(Key)的特徵值(雜湊值),再將鍵值對對映到固定的陣列下標中,利用陣列支援隨機存取的特性,實現 O(1) 時間的儲存和查詢操作。
雜湊表示意圖
在從鍵值對對映到陣列下標的過程中,雜湊表會存在 2 次雜湊衝突:
事實上,由於雜湊表是壓縮對映,所以我們無法避免雜湊衝突,只能保證雜湊表不會因為雜湊衝突而失去正確性。常用的雜湊衝突解決方法有 2 類:
開放定址(Open Addressing)的核心思想是: 在出現雜湊衝突時,在陣列上重新探測出一個空閒位置。 經典的探測方法有線性探測、平方探測和雙雜湊探測。線性探測是最基本的探測方法,我們今天要分析的 ThreadLocal 中的 ThreadLocalMap 雜湊表就是採用線性探測的開放定址法。
ThreadLocal 提供了一種特殊的執行緒安全方式。
使用 ThreadLocal 時,每個執行緒可以通過 ThreadLocal#get
或 ThreadLocal#set
方法存取資源在當前執行緒的副本,而不會與其他執行緒產生資源競爭。這意味著 ThreadLocal 並不考慮如何解決資源競爭,而是為每個執行緒分配獨立的資源副本,從根本上避免發生資源衝突,是一種無鎖的執行緒安全方法。
用一個表格總結 ThreadLocal 的 API:
public API | 描述 |
---|---|
set(T) | 設定當前執行緒的副本 |
T get() | 獲取當前執行緒的副本 |
void remove() | 移除當前執行緒的副本 |
ThreadLocal<S> withInitial(Supplier<S>) | 建立 ThreadLocal 並指定預設值建立工廠 |
protected API | 描述 |
T initialValue() | 設定預設值 |
ThreadLocal 在每個執行緒的 Thread 物件範例資料中分配獨立的記憶體區域,當我們存取 ThreadLocal 時,本質上是在存取當前執行緒的 Thread 物件上的範例資料,不同執行緒存取的是不同的範例資料,因此實現執行緒隔離。
Thread 物件中這塊資料就是一個使用線性探測的 ThreadLocalMap 雜湊表,ThreadLocal 物件本身就作為雜湊表的 Key ,而 Value 是資源的副本。當我們存取 ThreadLocal 時,就是先獲取當前執行緒範例資料中的 ThreadLocalMap 雜湊表,再通過當前 ThreadLocal 作為 Key 去匹配鍵值對。
ThreadLocal.java
// 獲取當前執行緒的副本
public T get() {
// 先獲取當前執行緒範例資料中的 ThreadLocalMap 雜湊表
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 通過當前 ThreadLocal 作為 Key 去匹配鍵值對
ThreadLocalMap.Entry e = map.getEntry(this);
// 詳細原始碼分析見下文 ...
}
// 獲取執行緒 t 的 threadLocals 欄位,即 ThreadLocalMap 雜湊表
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 靜態內部類
static class ThreadLocalMap {
// 詳細原始碼分析見下文 ...
}
Thread.java
// Thread 物件的範例資料
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 執行緒退出之前,會置空threadLocals變數,以便隨後GC
private void exit() {
// ...
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
// ...
}
ThreadLocal 示意圖
在業務開發的過程中,我們可能希望子執行緒可以存取主執行緒中的 ThreadLocal 資料,然而 ThreadLocal 是執行緒隔離的,包括在父子執行緒之間也是執行緒隔離的。為此,ThreadLocal 提供了一個相似的子類 InheritableThreadLocal
,ThreadLocal 和 InheritableThreadLocal 分別對應於執行緒物件上的兩塊記憶體區域:
1、ThreadLocal 欄位: 在所有執行緒間隔離;
2、InheritableThreadLocal 欄位: 子執行緒會繼承父執行緒的 InheritableThreadLocal 資料。父執行緒在建立子執行緒時,會批次將父執行緒的有效鍵值對資料拷貝到子執行緒的 InheritableThreadLocal,因此子執行緒可以複用父執行緒的區域性儲存。
在 InheritableThreadLocal 中,可以重寫 childValue()
方法修改拷貝到子執行緒的資料。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 引數:父執行緒的資料
// 返回值:拷貝到子執行緒的資料,預設為直接傳遞
protected T childValue(T parentValue) {
return parentValue;
}
}
需要特別注意:
注意 1 - InheritableThreadLocal 區域在拷貝後依然是執行緒隔離的: 在完成拷貝後,父子執行緒對 InheritableThreadLocal 的操作依然是相互獨立的。子執行緒對 InheritableThreadLocal 的寫不會影響父執行緒的 InheritableThreadLocal,反之亦然;
注意 2 - 拷貝過程在父執行緒執行: 這是容易混淆的點,雖然拷貝資料的程式碼寫在子執行緒的構造方法中,但是依然是在父執行緒執行的。子執行緒是在呼叫 start() 後才開始執行的。
InheritableThreadLocal 示意圖
ThreadLocal 提供具有自動清理資料的能力,具體分為 2 個顆粒度:
1、自動清理雜湊表: ThreadLocal 資料是 Thread 物件的範例資料,當執行緒執行結束後,就會跟隨 Thread 物件 GC 而被清理;
2、自動清理無效鍵值對: ThreadLocal 是使用弱鍵的動態雜湊表,當 Key 物件不再被持有強參照時,垃圾收集器會按照弱參照策略自動回收 Key 物件,並在下次存取 ThreadLocal 時清理無效鍵值對。
參照關係示意圖
然而,自動清理無效鍵值對會存在 「滯後性」,在滯後的這段時間內,無效的鍵值對資料沒有及時回收,就發生記憶體漏失。
綜上所述:雖然 ThreadLocal 提供了自動清理無效資料的能力,但是為了避免記憶體漏失,在業務開發中應該及時呼叫 ThreadLocal#remove
清理無效的區域性儲存。
場景 1 - 無鎖執行緒安全: ThreadLocal 提供了一種特殊的執行緒安全方式,從根本上避免資源競爭,也體現了空間換時間的思想;
場景 2 - 執行緒級別單例: 一般的單例物件是對整個程序可見的,使用 ThreadLocal 也可以實現執行緒級別的單例;
場景 3 - 共用引數: 如果一個模組有非常多地方需要使用同一個變數,相比於在每個方法中重複傳遞同一個引數,使用一個 ThreadLocal 全域性變數也是另一種傳遞引數方式。
我們採用 Android Handler 機制中的 Looper 訊息迴圈作為 ThreadLocal 的學習案例:
// /frameworks/base/core/java/android/os/Looper.java
public class Looper {
// 靜態 ThreadLocal 變數,全域性共用同一個 ThreadLocal 物件
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 設定 ThreadLocal 變數的值,即設定當前執行緒關聯的 Looper 物件
sThreadLocal.set(new Looper(quitAllowed));
}
public static Looper myLooper() {
// 獲取 ThreadLocal 變數的值,即獲取當前執行緒關聯的 Looper 物件
return sThreadLocal.get();
}
public static void prepare() {
prepare(true);
}
...
}
範例程式碼
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
// 兩個執行緒獨立存取不同的 Looper 物件
System.out.println(Looper.myLooper());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
// 兩個執行緒獨立存取不同的 Looper 物件
System.out.println(Looper.myLooper());
}
}).start();
要點如下:
Looper#prepare()
中呼叫 ThreadLocal#set()
設定當前執行緒關聯的 Looper 物件;Looper#myLooper()
中呼叫 ThreadLocal#get()
獲取當前執行緒關聯的 Looper 物件。我們可以畫出 Looper 中存取 ThreadLocal 的 Timethreads 圖,可以看到不同執行緒獨立存取不同的 Looper 物件,即執行緒間不存在資源競爭。
Looper ThreadLocal 示意圖
在《阿里巴巴 Java 開發手冊》中,亦有關於 ThreadLocal API 的程式設計規約:
DataFormat.java
private static final ThreadLocal<DataFormat> df = new ThreadLocal<DateFormat>(){
// 設定預設值 / 初始值
@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd");
}
};
// 使用:
DateUtils.df.get().format(new Date());
這一節,我們來分析 ThreadLocal 中主要流程的原始碼。
ThreadLocal 只有一個 threadLocalHashCode
雜湊值屬性:
1、threadLocalHashCode 相當於 ThreadLocal 的自定義雜湊值,在建立 ThreadLocal 物件時,會呼叫 nextHashCode()
方法分配一個雜湊值;
2、ThreadLocal 每次呼叫 nextHashCode()
方法都會將雜湊值追加 HASH_INCREMENT
,並記錄在一個全域性的原子整型 nextHashCode
中。
提示: ThreadLocal 的雜湊值序列為:0、HASH_INCREMENT、HASH_INCREMENT * 2、HASH_INCREMENT * 3、…
public class ThreadLocal<T> {
// 疑問 1:OK,threadLocalHashCode 類似於 hashCode(),那為什麼 ThreadLocal 不重寫 hashCode()
// ThreadLocal 的雜湊值,類似於重寫 Object#hashCode()
private final int threadLocalHashCode = nextHashCode();
// 全域性原子整型,每呼叫一次 nextHashCode() 累加一次
private static AtomicInteger nextHashCode = new AtomicInteger();
// 疑問:為什麼 ThreadLocal 雜湊值的增量是 0x61c88647?
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
// 返回上一次 nextHashCode 的值,並累加 HASH_INCREMENT
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
static class ThreadLocalMap {
// 詳細原始碼分析見下文 ...
}
不出意外的話又有小朋友出來舉手提問了