
前言
大家好,我是小彭。
在前面的文章里,我們聊到了散列表的開放尋址法和分離鏈表法,也聊到了 HashMap、LinkedHashMap 和 WeakHashMap 等基于分離鏈表法實(shí)現(xiàn)的散列表。
今天,我們來討論 Java 標(biāo)準(zhǔn)庫(kù)中一個(gè)使用開放尋址法的散列表結(jié)構(gòu),也是 Java & Android “面試八股文” 的標(biāo)準(zhǔn)題庫(kù)之一 —— ThreadLocal。
本文源碼基于 Java 8 ThreadLocal。
思維導(dǎo)圖:

1. 回顧散列表的工作原理
在開始分析 ThreadLocal 的實(shí)現(xiàn)原理之前,我們先回顧散列表的工作原理。
散列表是基于散列思想實(shí)現(xiàn)的 Map 數(shù)據(jù)結(jié)構(gòu),將散列思想應(yīng)用到散列表數(shù)據(jù)結(jié)構(gòu)時(shí),就是通過 hash 函數(shù)提取鍵(Key)的特征值(散列值),再將鍵值對(duì)映射到固定的數(shù)組下標(biāo)中,利用數(shù)組支持隨機(jī)訪問的特性,實(shí)現(xiàn) O(1) 時(shí)間的存儲(chǔ)和查詢操作。
散列表示意圖

在從鍵值對(duì)映射到數(shù)組下標(biāo)的過程中,散列表會(huì)存在 2 次散列沖突:
- 第 1 次 - hash 函數(shù)的散列沖突: 這是一般意義上的散列沖突;
- 第 2 次 - 散列值取余轉(zhuǎn)數(shù)組下標(biāo): 本質(zhì)上,將散列值轉(zhuǎn)數(shù)組下標(biāo)也是一次 Hash 算法,也會(huì)存在散列沖突。
事實(shí)上,由于散列表是壓縮映射,所以我們無法避免散列沖突,只能保證散列表不會(huì)因?yàn)樯⒘袥_突而失去正確性。常用的散列沖突解決方法有 2 類:
- 開放尋址法: 例如 ThreadLocalMap;
- 分離鏈表法: 例如 HashMap。
開放尋址(Open Addressing)的核心思想是: 在出現(xiàn)散列沖突時(shí),在數(shù)組上重新探測(cè)出一個(gè)空閑位置。 經(jīng)典的探測(cè)方法有線性探測(cè)、平方探測(cè)和雙散列探測(cè)。線性探測(cè)是最基本的探測(cè)方法,我們今天要分析的 ThreadLocal 中的 ThreadLocalMap 散列表就是采用線性探測(cè)的開放尋址法。
2. 認(rèn)識(shí) ThreadLocal 線程局部存儲(chǔ)
2.1 說一下 ThreadLocal 的特點(diǎn)?
ThreadLocal 提供了一種特殊的線程安全方式。
使用 ThreadLocal 時(shí),每個(gè)線程可以通過 ThreadLocal#get 或 ThreadLocal#set 方法訪問資源在當(dāng)前線程的副本,而不會(huì)與其他線程產(chǎn)生資源競(jìng)爭(zhēng)。這意味著 ThreadLocal 并不考慮如何解決資源競(jìng)爭(zhēng),而是為每個(gè)線程分配獨(dú)立的資源副本,從根本上避免發(fā)生資源沖突,是一種無鎖的線程安全方法。
用一個(gè)表格總結(jié) ThreadLocal 的 API:
| public API | 描述 |
|---|---|
| set(T) | 設(shè)置當(dāng)前線程的副本 |
| T get() | 獲取當(dāng)前線程的副本 |
| void remove() | 移除當(dāng)前線程的副本 |
| ThreadLocal<S> withInitial(Supplier<S>) | 創(chuàng)建 ThreadLocal 并指定缺省值創(chuàng)建工廠 |
| protected API | 描述 |
| T initialValue() | 設(shè)置缺省值 |
2.2 ThreadLocal 如何實(shí)現(xiàn)線程隔離?(重點(diǎn)理解)
ThreadLocal 在每個(gè)線程的 Thread 對(duì)象實(shí)例數(shù)據(jù)中分配獨(dú)立的內(nèi)存區(qū)域,當(dāng)我們?cè)L問 ThreadLocal 時(shí),本質(zhì)上是在訪問當(dāng)前線程的 Thread 對(duì)象上的實(shí)例數(shù)據(jù),不同線程訪問的是不同的實(shí)例數(shù)據(jù),因此實(shí)現(xiàn)線程隔離。
Thread 對(duì)象中這塊數(shù)據(jù)就是一個(gè)使用線性探測(cè)的 ThreadLocalMap 散列表,ThreadLocal 對(duì)象本身就作為散列表的 Key ,而 Value 是資源的副本。當(dāng)我們?cè)L問 ThreadLocal 時(shí),就是先獲取當(dāng)前線程實(shí)例數(shù)據(jù)中的 ThreadLocalMap 散列表,再通過當(dāng)前 ThreadLocal 作為 Key 去匹配鍵值對(duì)。
ThreadLocal.java
// 獲取當(dāng)前線程的副本
public T get() {
// 先獲取當(dāng)前線程實(shí)例數(shù)據(jù)中的 ThreadLocalMap 散列表
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 通過當(dāng)前 ThreadLocal 作為 Key 去匹配鍵值對(duì)
ThreadLocalMap.Entry e = map.getEntry(this);
// 詳細(xì)源碼分析見下文 ...
}
// 獲取線程 t 的 threadLocals 字段,即 ThreadLocalMap 散列表
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 靜態(tài)內(nèi)部類
static class ThreadLocalMap {
// 詳細(xì)源碼分析見下文 ...
}
Thread.java
// Thread 對(duì)象的實(shí)例數(shù)據(jù)
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 線程退出之前,會(huì)置空threadLocals變量,以便隨后GC
private void exit() {
// ...
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
// ...
}
ThreadLocal 示意圖

2.3 使用 InheritableThreadLocal 繼承父線程的局部存儲(chǔ)
在業(yè)務(wù)開發(fā)的過程中,我們可能希望子線程可以訪問主線程中的 ThreadLocal 數(shù)據(jù),然而 ThreadLocal 是線程隔離的,包括在父子線程之間也是線程隔離的。為此,ThreadLocal 提供了一個(gè)相似的子類 InheritableThreadLocal,ThreadLocal 和 InheritableThreadLocal 分別對(duì)應(yīng)于線程對(duì)象上的兩塊內(nèi)存區(qū)域:
1、ThreadLocal 字段: 在所有線程間隔離;
2、InheritableThreadLocal 字段: 子線程會(huì)繼承父線程的 InheritableThreadLocal 數(shù)據(jù)。父線程在創(chuàng)建子線程時(shí),會(huì)批量將父線程的有效鍵值對(duì)數(shù)據(jù)拷貝到子線程的 InheritableThreadLocal,因此子線程可以復(fù)用父線程的局部存儲(chǔ)。
在 InheritableThreadLocal 中,可以重寫 childValue() 方法修改拷貝到子線程的數(shù)據(jù)。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 參數(shù):父線程的數(shù)據(jù)
// 返回值:拷貝到子線程的數(shù)據(jù),默認(rèn)為直接傳遞
protected T childValue(T parentValue) {
return parentValue;
}
}
需要特別注意:
注意 1 - InheritableThreadLocal 區(qū)域在拷貝后依然是線程隔離的: 在完成拷貝后,父子線程對(duì) InheritableThreadLocal 的操作依然是相互獨(dú)立的。子線程對(duì) InheritableThreadLocal 的寫不會(huì)影響父線程的 InheritableThreadLocal,反之亦然;
注意 2 - 拷貝過程在父線程執(zhí)行: 這是容易混淆的點(diǎn),雖然拷貝數(shù)據(jù)的代碼寫在子線程的構(gòu)造方法中,但是依然是在父線程執(zhí)行的。子線程是在調(diào)用 start() 后才開始執(zhí)行的。
InheritableThreadLocal 示意圖

2.4 ThreadLocal 的自動(dòng)清理與內(nèi)存泄漏問題
ThreadLocal 提供具有自動(dòng)清理數(shù)據(jù)的能力,具體分為 2 個(gè)顆粒度:
1、自動(dòng)清理散列表: ThreadLocal 數(shù)據(jù)是 Thread 對(duì)象的實(shí)例數(shù)據(jù),當(dāng)線程執(zhí)行結(jié)束后,就會(huì)跟隨 Thread 對(duì)象 GC 而被清理;
2、自動(dòng)清理無效鍵值對(duì): ThreadLocal 是使用弱鍵的動(dòng)態(tài)散列表,當(dāng) Key 對(duì)象不再被持有強(qiáng)引用時(shí),垃圾收集器會(huì)按照弱引用策略自動(dòng)回收 Key 對(duì)象,并在下次訪問 ThreadLocal 時(shí)清理無效鍵值對(duì)。
引用關(guān)系示意圖

然而,自動(dòng)清理無效鍵值對(duì)會(huì)存在 “滯后性”,在滯后的這段時(shí)間內(nèi),無效的鍵值對(duì)數(shù)據(jù)沒有及時(shí)回收,就發(fā)生內(nèi)存泄漏。
- 舉例 1: 如果創(chuàng)建 ThreadLocal 的線程一直持續(xù)運(yùn)行,整個(gè)散列表的數(shù)據(jù)就會(huì)一致存在。比如線程池中的線程(大體)是復(fù)用的,這部分復(fù)用線程中的 ThreadLocal 數(shù)據(jù)就不會(huì)被清理;
- 舉例 2: 如果在數(shù)據(jù)無效后沒有再訪問過 ThreadLocal 對(duì)象,那么自然就沒有機(jī)會(huì)觸發(fā)清理;
- 舉例 3: 即使訪問 ThreadLocal 對(duì)象,也不一定會(huì)觸發(fā)清理(原因見下文源碼分析)。
綜上所述:雖然 ThreadLocal 提供了自動(dòng)清理無效數(shù)據(jù)的能力,但是為了避免內(nèi)存泄漏,在業(yè)務(wù)開發(fā)中應(yīng)該及時(shí)調(diào)用 ThreadLocal#remove 清理無效的局部存儲(chǔ)。
2.5 ThreadLocal 的使用場(chǎng)景
場(chǎng)景 1 - 無鎖線程安全: ThreadLocal 提供了一種特殊的線程安全方式,從根本上避免資源競(jìng)爭(zhēng),也體現(xiàn)了空間換時(shí)間的思想;
場(chǎng)景 2 - 線程級(jí)別單例: 一般的單例對(duì)象是對(duì)整個(gè)進(jìn)程可見的,使用 ThreadLocal 也可以實(shí)現(xiàn)線程級(jí)別的單例;
場(chǎng)景 3 - 共享參數(shù): 如果一個(gè)模塊有非常多地方需要使用同一個(gè)變量,相比于在每個(gè)方法中重復(fù)傳遞同一個(gè)參數(shù),使用一個(gè) ThreadLocal 全局變量也是另一種傳遞參數(shù)方式。
2.6 ThreadLocal 使用示例
我們采用 Android Handler 機(jī)制中的 Looper 消息循環(huán)作為 ThreadLocal 的學(xué)習(xí)案例:
// /frameworks/base/core/java/android/os/Looper.java
public class Looper {
// 靜態(tài) ThreadLocal 變量,全局共享同一個(gè) ThreadLocal 對(duì)象
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");
}
// 設(shè)置 ThreadLocal 變量的值,即設(shè)置當(dāng)前線程關(guān)聯(lián)的 Looper 對(duì)象
sThreadLocal.set(new Looper(quitAllowed));
}
public static Looper myLooper() {
// 獲取 ThreadLocal 變量的值,即獲取當(dāng)前線程關(guān)聯(lián)的 Looper 對(duì)象
return sThreadLocal.get();
}
public static void prepare() {
prepare(true);
}
...
}
示例代碼
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
// 兩個(gè)線程獨(dú)立訪問不同的 Looper 對(duì)象
System.out.println(Looper.myLooper());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
// 兩個(gè)線程獨(dú)立訪問不同的 Looper 對(duì)象
System.out.println(Looper.myLooper());
}
}).start();
要點(diǎn)如下:
- 1、Looper 中的 ThreadLocal 被聲明為靜態(tài)類型,泛型參數(shù)為 Looper,全局共享同一個(gè) ThreadLocal 對(duì)象;
- 2、
Looper#prepare()中調(diào)用ThreadLocal#set()設(shè)置當(dāng)前線程關(guān)聯(lián)的 Looper 對(duì)象; - 3、
Looper#myLooper()中調(diào)用ThreadLocal#get()獲取當(dāng)前線程關(guān)聯(lián)的 Looper 對(duì)象。
我們可以畫出 Looper 中訪問 ThreadLocal 的 Timethreads 圖,可以看到不同線程獨(dú)立訪問不同的 Looper 對(duì)象,即線程間不存在資源競(jìng)爭(zhēng)。
Looper ThreadLocal 示意圖
2.7 阿里巴巴 ThreadLocal 編程規(guī)約
在《阿里巴巴 Java 開發(fā)手冊(cè)》中,亦有關(guān)于 ThreadLocal API 的編程規(guī)約:
- 【強(qiáng)制】 SimpleDateFormate 是線程不安全的類,一般不要定義為 static ****變量。如果定義為 static,必須加鎖,或者使用 DateUtils 工具類(使用 ThreadLocal 做線程隔離)。
DataFormat.java
private static final ThreadLocal<DataFormat> df = new ThreadLocal<DateFormat>(){
// 設(shè)置缺省值 / 初始值
@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd");
}
};
// 使用:
DateUtils.df.get().format(new Date());
- 【參考】 (原文過于啰嗦,以下是小彭翻譯轉(zhuǎn)述)ThreadLocal 變量建議使用 static 全局變量,可以保證變量在類初始化時(shí)創(chuàng)建,所有類實(shí)例可以共享同一個(gè)靜態(tài)變量(例如,在 Android Looper 的案例中,ThreadLocal 就是使用 static 修飾的全局變量)。
- 【強(qiáng)制】 必須回收自定義的 ThreadLocal 變量,尤其在線程池場(chǎng)景下,線程經(jīng)常被反復(fù)用,如果不清理自定義的 ThreadLocal 變量,則可能會(huì)影響后續(xù)業(yè)務(wù)邏輯和造成內(nèi)存泄漏等問題。盡量在代碼中使用 try-finally 塊回收,在 finally 中調(diào)用 remove() 方法。
3. ThreadLocal 源碼分析
這一節(jié),我們來分析 ThreadLocal 中主要流程的源碼。
3.1 ThreadLocal 的屬性
ThreadLocal 只有一個(gè) threadLocalHashCode 散列值屬性:
1、threadLocalHashCode 相當(dāng)于 ThreadLocal 的自定義散列值,在創(chuàng)建 ThreadLocal 對(duì)象時(shí),會(huì)調(diào)用
nextHashCode()方法分配一個(gè)散列值;2、ThreadLocal 每次調(diào)用
nextHashCode()方法都會(huì)將散列值追加HASH_INCREMENT,并記錄在一個(gè)全局的原子整型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();
// 全局原子整型,每調(diào)用一次 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 {
// 詳細(xì)源碼分析見下文 ...
}
不出意外的話又有小朋友出來舉手提問了????♀?:
- ????♀?疑問 1:OK,threadLocalHashCode 類似于 hashCode(),那為什么 ThreadLocal 不重寫 hashCode()?
如果重寫 Object#hashCode(),那么 threadLocalHashCode 散列值就會(huì)對(duì)所有散列表生效。而 threadLocalHashCode 散列值是專門針對(duì)數(shù)組為 2 的整數(shù)冪的散列表設(shè)計(jì)的,在其他散列表中不一定表現(xiàn)良好。因此 ThreadLocal 沒有重寫 Object#hashCode(),讓 threadLocalHashCode 散列值只在 ThreadLocal 內(nèi)部的 ThreadLocalMap 使用。
常規(guī)做法
public class ThreadLocal<T> {
// ThreadLocal 未重寫 hashCode()
@Override
public int hashCode() {
return threadLocalHashCode;
}
}
- ????♀?疑問 2:為什么使用 ThreadLocal 作為散列表的 Key,而不是常規(guī)思維用 Thread Id 作為 Key?
如果使用 Thread Id 作為 Key,那么就需要在每個(gè) ThreadLocal 對(duì)象中維護(hù)散列表,而不是每個(gè)線程維護(hù)一個(gè)散列表。此時(shí),當(dāng)多個(gè)線程并發(fā)訪問同一個(gè) ThreadLocal 對(duì)象中的散列表時(shí),就需要通過加鎖保證線程安全。而 ThreadLocal 的方案讓每個(gè)線程訪問獨(dú)立的散列表,就可以從根本上規(guī)避線程競(jìng)爭(zhēng)。
3.2 ThreadLocal 的 API
分析代碼,可以總結(jié)出 ThreadLocal API 的用法和注意事項(xiàng):
- 1、ThreadLocal#get: 獲取當(dāng)前線程的副本;
- 2、ThreadLocal#set: 設(shè)置當(dāng)前線程的副本;
- 3、ThreadLocal#remove: 移除當(dāng)前線程的副本;
-
4、ThreadLocal#initialValue: 由子類重寫來設(shè)置缺省值:
- 4.1 如果未命中(Map 取值為 nul),則會(huì)調(diào)用
initialValue()創(chuàng)建并設(shè)置缺省值; - 4.2 ThreadLocal 的缺省值只會(huì)在緩存未命中時(shí)創(chuàng)建,即缺省值采用懶初始化策略;
- 4.3 如果先設(shè)置后又移除副本,再次 get 獲取副本未命中時(shí)依然會(huì)調(diào)用
initialValue()創(chuàng)建并設(shè)置缺省值。
- 4.1 如果未命中(Map 取值為 nul),則會(huì)調(diào)用
- 5、ThreadLocal#withInitial: 方便設(shè)置缺省值,而不需要實(shí)現(xiàn)子類。
在 ThreadLocal 的 API 會(huì)通過 getMap() 方法獲取當(dāng)前線程的 Thread 對(duì)象中的 threadLocals 字段,這是線程隔離的關(guān)鍵。
ThreadLocal.java
public ThreadLocal() {
// do nothing
}
// 子類可重寫此方法設(shè)置缺省值(方法命名為 defaultValue 獲取更貼切)
protected T initialValue() {
// 默認(rèn)不提供缺省值
return null;
}
// 幫助方法:不重寫 ThreadLocal 也可以設(shè)置缺省值
// supplier:缺省值創(chuàng)建工廠
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
// 1. 獲取當(dāng)前線程的副本
public T get() {
Thread t = Thread.currentThread();
// ThreadLocalMap 詳細(xì)源碼分析見下文
ThreadLocalMap map = getMap(t);
if (map != null) {
// 存在匹配的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 未命中,則獲取并設(shè)置缺省值(即缺省值采用懶初始化策略)
return setInitialValue();
}
// 獲取并設(shè)置缺省值
private T setInitialValue() {
T value = initialValue();
// 其實(shí)源碼中是并不是直接調(diào)用set(),而是復(fù)制了一份 set() 方法的源碼
// 這是為了防止子類重寫 set() 方法后改變?nèi)笔≈颠壿? set(value);
return value;
}
// 2. 設(shè)置當(dāng)前線程的副本
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 直到設(shè)置值的時(shí)候才創(chuàng)建(即 ThreadLocalMap 采用懶初始化策略)
createMap(t, value);
}
// 3. 移除當(dāng)前線程的副本
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
// 重點(diǎn):獲取當(dāng)前線程的 threadLocals 字段
return t.threadLocals;
}
// ThreadLocal 缺省值幫助類
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
// 重寫 initialValue() 以設(shè)置缺省值
@Override
protected T initialValue() {
return supplier.get();
}
}
3.3 InheritableThreadLocal 如何繼承父線程的局部存儲(chǔ)?
父線程在創(chuàng)建子線程時(shí),在子線程的構(gòu)造方法中會(huì)批量將父線程的有效鍵值對(duì)數(shù)據(jù)拷貝到子線程,因此子線程可以復(fù)用父線程的局部存儲(chǔ)。
Thread.java
// Thread 對(duì)象的實(shí)例數(shù)據(jù)
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 構(gòu)造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 拷貝父線程的 InheritableThreadLocal 散列表
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
ThreadLocal.java
// 帶 Map 的構(gòu)造方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
static class ThreadLocalMap {
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 詳細(xì)源碼分析見下文 ...
Object value = key.childValue(e.value);
...
}
}
InheritableThreadLocal 在拷貝父線程散列表的過程中,會(huì)調(diào)用 InheritableThreadLocal#childValue() 嘗試轉(zhuǎn)換為子線程需要的數(shù)據(jù),默認(rèn)是直接傳遞,可以重寫這個(gè)方法修改拷貝的數(shù)據(jù)。
InheritableThreadLocal.java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// 參數(shù):父線程的數(shù)據(jù)
// 返回值:拷貝到子線程的數(shù)據(jù),默認(rèn)為直接傳遞
protected T childValue(T parentValue) {
return parentValue;
}
下面,我們來分析 ThreadLocalMap 的源碼。
4. ThreadLocalMap 源碼分析
ThreadLocalMap 是 ThreadLocal 內(nèi)部使用的散列表,也是 ThreadLocal 的靜態(tài)內(nèi)部類。這一節(jié),我們就來分析 ThreadLocalMap 散列表中主要流程的源碼。
4.1 ThreadLocalMap 的屬性
先用一個(gè)表格整理 ThreadLocalMap 的屬性:
| 屬性 | 描述 |
|---|---|
| Entry[] table | 底層數(shù)組 |
| int size | 有效鍵值對(duì)數(shù)量 |
| int threshold | 擴(kuò)容閾值(數(shù)組容量的 2/3) |
| int INITIAL_CAPACITY | 默認(rèn)數(shù)組容量(16) |
可以看到,散列表必備底層數(shù)組 table、鍵值對(duì)數(shù)量 size、擴(kuò)容閾值 threshold 等屬性都有,并且也要求數(shù)組的長(zhǎng)度是 2 的整數(shù)倍。主要區(qū)別在于 Entry 節(jié)點(diǎn)上:
- 1、ThreadLocal 本身就是散列表的鍵 Key;
- 2、擴(kuò)容閾值為數(shù)組容量的 2/3;
- 3、ThreadLocalMap#Entry 節(jié)點(diǎn)沒有
next指針,因?yàn)?ThreadLocalMap 采用線性探測(cè)解決散列沖突,所以不存在鏈表指針; - 4、ThreadLocalMap#Entry 在鍵值對(duì)的 Key 上使用弱引用,這與 WeakHashMap 相似。
ThreadLocal.java
static class ThreadLocalMap {
// 默認(rèn)數(shù)組容量(容量必須是 2 的整數(shù)倍)
private static final int INITIAL_CAPACITY = 16;
// 底層數(shù)組
private Entry[] table;
// 有效鍵值對(duì)數(shù)量
private int size = 0;
// 擴(kuò)容閾值
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
// 鍵值對(duì)節(jié)點(diǎn)
static class Entry extends WeakReference<ThreadLocal<?>> {
// next:開放尋址法沒有 next 指針
// Key:與 WeakHashMap 相同,少了 key 的強(qiáng)引用
// Hash:位于 ThreadLocal#threadLocalHashCode
// Value:當(dāng)前線程的副本
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k/*注意:只有 Key 是弱引用*/);
value = v;
}
}
}
不出意外的話又有小朋友出來舉手提問了????♀?:
????♀?疑問 3:為什么 ThreadLocalMap 要求數(shù)組的容量是 2 的整數(shù)冪?(回答過多少次了,把手給我放下)
????♀?疑問 4:為什么 Key 是弱引用,而不是 Entry 或 Value 是弱引用?
首先,Entry 一定要持有強(qiáng)引用,而不能持有弱引用。這是因?yàn)?Entry 是 ThreadLocalMap 內(nèi)部維護(hù)數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)細(xì)節(jié),并不會(huì)暴露到 ThreadLocalMap 外部,即除了 ThreadLocalMap 本身之外沒有其它地方持有 Entry 的強(qiáng)引用。所以,如果持有 Entry 的弱引用,即使 ThreadLocalMap 外部依然在使用 Key 對(duì)象,ThreadLocalMap 內(nèi)部依然會(huì)回收鍵值對(duì),這與預(yù)期不符。
其次,不管是 Key 還是 Value 使用弱引用都可以實(shí)現(xiàn)自動(dòng)清理,至于使用哪一種方法各有優(yōu)缺點(diǎn),適用場(chǎng)景也不同。Key 弱引用的優(yōu)點(diǎn)是外部不需要持有 Value 的強(qiáng)引用,缺點(diǎn)是存在 “重建 Key 不等價(jià)” 問題。
由于 ThreadLocal 的應(yīng)用場(chǎng)景是線程局部存儲(chǔ),我們沒有重建多個(gè) ThreadLocal 對(duì)象指向同一個(gè)鍵值對(duì)的需求,也沒有重寫 Object#equals() 方法,所以不存在重建 Key 的問題,使用 Key 弱引用更方便。
| 類型 | 優(yōu)點(diǎn) | 缺點(diǎn) | 場(chǎng)景 |
|---|---|---|---|
| Key 弱引用 | 外部不需要持有 Value 的強(qiáng)引用,使用更簡(jiǎn)單 | 重建 Key 不等價(jià) | 未重寫 equals |
| Value 弱引用 | 重建 Key 等價(jià) | 外部需要持有 Value 的強(qiáng)引用 | 重寫 equals |
提示: 關(guān)于 “重建 Key 對(duì)象不等價(jià)的問題” 的更多詳細(xì)論述過程,我們?cè)谶@篇文章里討論過 《WeakHashMap 和 HashMap 的區(qū)別是什么,何時(shí)使用?》,去看看。
4.2 ThreadLocalMap 的構(gòu)造方法
ThreadLocalMap 有 2 個(gè)構(gòu)造方法:
1、帶首個(gè)鍵值對(duì)的構(gòu)造方法: 在首次添加元素或首次查詢數(shù)據(jù)生成缺省值時(shí),才會(huì)調(diào)用此構(gòu)造方法創(chuàng)建 ThreadLocalMap 對(duì)象,并添加首個(gè)鍵值對(duì);
2、帶 Map 的構(gòu)造方法: 在創(chuàng)建子線程時(shí),父線程會(huì)調(diào)用此構(gòu)造方法創(chuàng)建 ThreadLocalMap 對(duì)象,并添加批量父線程 ThreadLocalMap 中的有效鍵值對(duì)。
ThreadLocal.java
// 帶首個(gè)鍵值對(duì)的構(gòu)造方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 帶 Map 的構(gòu)造方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
static class ThreadLocalMap {
// -> 帶首個(gè)鍵值對(duì)的構(gòu)造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 創(chuàng)建底層數(shù)組(默認(rèn)長(zhǎng)度為 16)
table = new Entry[INITIAL_CAPACITY];
// 散列值轉(zhuǎn)數(shù)組下標(biāo)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 添加首個(gè)元素(首個(gè)元素一定不會(huì)沖突)
table[i] = new Entry(firstKey, firstValue);
// 鍵值對(duì)數(shù)量
size = 1;
// 設(shè)置擴(kuò)容閾值
setThreshold(INITIAL_CAPACITY);
}
// -> 帶 Map 的構(gòu)造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
// 設(shè)置擴(kuò)容閾值
setThreshold(len);
// 創(chuàng)建底層數(shù)組(使用 parent 的長(zhǎng)度)
table = new Entry[len];
// 逐個(gè)添加鍵值對(duì)
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
// 如果鍵值對(duì)的 Key 被回收,則跳過
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 構(gòu)造新的鍵值對(duì)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
// 散列值轉(zhuǎn)數(shù)組下標(biāo)
int h = key.threadLocalHashCode & (len - 1);
// 處理散列沖突
while (table[h] != null)
// 線性探測(cè)
h = nextIndex(h, len);
table[h] = c;
// 鍵值對(duì)數(shù)量
size++;
}
}
}
}
}
4.3 回顧線性探測(cè)的工作原理
ThreadLocalMap 后續(xù)的源碼有難度,為了幫助理解,我將文章 “第一節(jié) · 回顧散列表的工作原理” 中有關(guān)線性探測(cè)方法的部分移在這里。
添加鍵值對(duì): 先將散列值取余映射到數(shù)組下標(biāo),然后從數(shù)組下標(biāo)位置開始探測(cè)與目標(biāo) Key 相等的節(jié)點(diǎn)。如果找到,則將舊 Value 替換為新 Value,否則沿著數(shù)組順序線性探測(cè)。直到線性探測(cè)遇到空閑位置,則說明節(jié)點(diǎn)不存在,需要添加新節(jié)點(diǎn)。如果在添加鍵值對(duì)后數(shù)組沒有空閑位置,就觸發(fā)擴(kuò)容;
查找鍵值對(duì): 查找類似。也是先將散列值映射到數(shù)組下標(biāo),然后從數(shù)組下標(biāo)位置開始線性探測(cè)。直到線性探測(cè)遇到空閑位置,則說明節(jié)點(diǎn)不存在;
刪除鍵值對(duì): 刪除類似。由于查找操作在遇到空閑位置時(shí),會(huì)認(rèn)為鍵值對(duì)不存在于散列表中,如果刪除操作時(shí) “真刪除”,就會(huì)使得一組連續(xù)段產(chǎn)生斷層,導(dǎo)致查找操作失效。因此,刪除操作要做 “假刪除”,刪除操作只是將節(jié)點(diǎn)標(biāo)記為 “Deleted”,查找操作在遇到 “Deleted” 標(biāo)記的節(jié)點(diǎn)時(shí)會(huì)繼續(xù)向下探測(cè)。
開放尋址法示意圖
可以看到,在線性探測(cè)中的 “連續(xù)段” 非常重要: 線性探測(cè)在判斷節(jié)點(diǎn)是否存在于散列表時(shí),并不是線性遍歷整個(gè)數(shù)組,而只會(huì)線性遍歷從散列值映射的數(shù)組下標(biāo)后的連續(xù)段。
4.4 ThreadLocalMap 的獲取方法
ThreadLocalMap 的獲取方法相對(duì)簡(jiǎn)單,所以我們先分析,區(qū)分 2 種情況:
- 1、數(shù)組下標(biāo)直接命中目標(biāo) Key,則直接返回,也不清理無效數(shù)據(jù)(這就是前文提到訪問 ThreadLocal 不一定會(huì)觸發(fā)清理的源碼體現(xiàn));
- 2、數(shù)組下標(biāo)未命中目標(biāo) Key,則開始線性探測(cè)。探測(cè)過程中如果遇到 Key == null 的無效節(jié)點(diǎn),則會(huì)調(diào)用
expungeStaleEntry()清理連續(xù)段(說明即使觸發(fā)清理,也不一定會(huì)掃描整個(gè)散列表)。
expungeStaleEntry() 是 ThreadLocalMap 核心的連續(xù)段清理方法,下文提到的 replaceStaleEntry() 和 cleanSomeSlots() 等清理方法都會(huì)直接或間接調(diào)用到 expungeStaleEntry()。 它的邏輯很簡(jiǎn)單:就是線性遍歷從 staleSlot 位置開始的連續(xù)段:
- 1、k == null 的無效節(jié)點(diǎn): 清理;
- 2、k ≠ null 的有效節(jié)點(diǎn),再散列到新的位置上。
ThreadLocalMap#getEntry 方法示意圖
不出意外的話又有小朋友出來舉手提問了????♀?:
- ????♀?疑問 5:清理無效節(jié)點(diǎn)我理解,為什么要對(duì)有效節(jié)點(diǎn)再散列呢?
線性探測(cè)只會(huì)遍歷連續(xù)段,而清理無效節(jié)點(diǎn)會(huì)導(dǎo)致連續(xù)段產(chǎn)生斷層。如果沒有對(duì)有效節(jié)點(diǎn)做再散列,那么有效節(jié)點(diǎn)在下次查詢時(shí)就有可能探測(cè)不到了。
ThreadLocal.java
static class ThreadLocalMap {
// 獲取 Key 匹配的鍵值對(duì)
private Entry getEntry(ThreadLocal<?> key) {
// 散列值轉(zhuǎn)數(shù)組下標(biāo)
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 數(shù)組下標(biāo)直接命中,則直接返回,也不清理無效數(shù)據(jù)
return e;
else
// 線性探測(cè),并且清理連續(xù)段中無效數(shù)據(jù)
return getEntryAfterMiss(key, i, e);
}
// -> 線性探測(cè),并且清理連續(xù)段中無效數(shù)據(jù)
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)
// Key 對(duì)象被回收,觸發(fā)連續(xù)段清理
// 連續(xù)段清理在一個(gè) while 循環(huán)中只會(huì)觸發(fā)一次,因?yàn)檫@個(gè)段中 k == null 的節(jié)點(diǎn)都被清理出去了
// 如果連續(xù)段清理后,i 位置為 null,那么目標(biāo)節(jié)點(diǎn)一定不存在
expungeStaleEntry(i);
else
// 未命中,探測(cè)下一個(gè)位置
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// -> 清理連續(xù)段中無效數(shù)據(jù)
// staleSlot:起點(diǎn)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理無效節(jié)點(diǎn)(起點(diǎn)一定是無效節(jié)點(diǎn))
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 線性探測(cè)直到遇到空閑位置
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 清理無效節(jié)點(diǎn)
e.value = null;
tab[i] = null;
size--;
} else {
// 疑問 5:清理無效節(jié)點(diǎn)我理解,為什么要對(duì)有效節(jié)點(diǎn)再散列呢?
// 再散列有效節(jié)點(diǎn)
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
// -> 線性探測(cè)下一個(gè)數(shù)組位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
}
4.5 ThreadLocalMap 的添加方法
ThreadLocalMap#set 的流程非常復(fù)雜,我將主要步驟概括為 6 步:
- 1、先將散列值映射到數(shù)組下標(biāo),并且開始線性探測(cè);
- 2、如果探測(cè)中遇到目標(biāo)節(jié)點(diǎn),則將舊 Value 更新為新 Value;
- 3、如果探測(cè)中遇到無效節(jié)點(diǎn),則會(huì)調(diào)用
replaceStaleEntry()清理連續(xù)段并添加鍵值對(duì); - 4、如果未探測(cè)到目標(biāo)節(jié)點(diǎn)或無效節(jié)點(diǎn),則創(chuàng)建并添加新節(jié)點(diǎn);
- 5、添加新節(jié)點(diǎn)后調(diào)用
cleanSomeSlots()方法清理部分?jǐn)?shù)據(jù); - 6、如果沒有發(fā)生清理并且達(dá)到擴(kuò)容閾值,則觸發(fā)
rehash()擴(kuò)容。
replaceStaleEntry(): 清理連續(xù)段中的無效節(jié)點(diǎn)的同時(shí),如果目標(biāo)節(jié)點(diǎn)存在則更新 Value 后替換到 staleSlot 無效節(jié)點(diǎn)位置,如果不存在則創(chuàng)建新節(jié)點(diǎn)替換到 staleSlot 無效節(jié)點(diǎn)位置。
cleanSomeSlots(): 對(duì)數(shù)式清理,清理復(fù)雜度比全數(shù)組清理低,在大多數(shù)情況只會(huì)掃描 log(len) 個(gè)元素。如果掃描過程中遇到無效節(jié)點(diǎn),則從該位置執(zhí)行一次連續(xù)段清理,再?gòu)倪B續(xù)段的下一個(gè)位置重新掃描 log(len) 個(gè)元素,直接結(jié)束對(duì)數(shù)掃描。
ThreadLocalMap#set 示意圖
ThreadLocal.java
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 1、散列值轉(zhuǎn)數(shù)組下標(biāo)
int i = key.threadLocalHashCode & (len-1);
// 線性探測(cè)
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 2、命中,將舊 Value 替換為新 Value
e.value = value;
return;
}
if (k == null) {
// 3、清理無效節(jié)點(diǎn),并插入鍵值對(duì)
replaceStaleEntry(key, value, i);
return;
}
}
// 4、如果未探測(cè)到目標(biāo)節(jié)點(diǎn)或無效節(jié)點(diǎn),則創(chuàng)建并添加新節(jié)點(diǎn)
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots:清理部分?jǐn)?shù)據(jù)
// 5、添加新節(jié)點(diǎn)后調(diào)用 cleanSomeSlots() 方法清理部分?jǐn)?shù)據(jù)
if (!cleanSomeSlots(i, sz /*有效數(shù)據(jù)個(gè)數(shù)*/) && sz >= threshold)
// 6、如果沒有發(fā)生清理并且達(dá)到擴(kuò)容閾值,則觸發(fā) rehash() 擴(kuò)容
rehash();
}
// -> 3、清理無效節(jié)點(diǎn),并插入鍵值對(duì)
// key-value:插入的鍵值對(duì)
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge:記錄清理的起點(diǎn)
int slotToExpunge = staleSlot;
// 3.1 向前探測(cè)找到連續(xù)段中的第一個(gè)無效節(jié)點(diǎn)
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 3.2 向后探測(cè)目標(biāo)節(jié)點(diǎn)
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 3.2.1 命中,將目標(biāo)節(jié)點(diǎn)替換到 staleSlot 位置
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 3.2.2 如果連續(xù)段在 staleSlot 之前沒有無效節(jié)點(diǎn),則從 staleSlot 的下一個(gè)無效節(jié)點(diǎn)開始清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 3.2.3 如果連續(xù)段中還有其他無效節(jié)點(diǎn),則清理
// expungeStaleEntry:連續(xù)段清理
// cleanSomeSlots:對(duì)數(shù)式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果連續(xù)段在 staleSlot 之前沒有無效節(jié)點(diǎn),則從 staleSlot 的下一個(gè)無效節(jié)點(diǎn)開始清理
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 3.3 創(chuàng)建新節(jié)點(diǎn)并插入 staleSlot 位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 3.4 如果連續(xù)段中還有其他無效節(jié)點(diǎn),則清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len /*數(shù)組長(zhǎng)度*/);
}
// 5、對(duì)數(shù)式清理
// i:起點(diǎn)
// n:數(shù)組長(zhǎng)度或有效數(shù)據(jù)個(gè)數(shù)
private boolean cleanSomeSlots(int i, int 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) {
// 發(fā)現(xiàn)無效節(jié)點(diǎn),重新探測(cè) log2(len)
n = len;
removed = true;
// 連續(xù)段清理
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // 探測(cè) log2(len)
return removed;
}
}
4.6 ThreadLocalMap 的擴(kuò)容方法
ThreadLocalMap 的擴(kuò)容方法相對(duì)于添加方法比較好理解。在添加方法中,如果添加鍵值對(duì)后散列值的長(zhǎng)度超過擴(kuò)容閾值,就會(huì)調(diào)用 rehash() 方法擴(kuò)容,主體流程分為 3步:
- 1、先完整掃描散列表清理無效數(shù)據(jù),清理后用較低的閾值判斷是否需要擴(kuò)容;
- 2、創(chuàng)建新數(shù)組;
- 3、將舊數(shù)組上無效的節(jié)點(diǎn)忽略,將有效的節(jié)點(diǎn)再散列到新數(shù)組上。
ThreadLocaoMap#rehash 示意圖
ThreadLocal.java
static class ThreadLocalMap {
// 擴(kuò)容(在容量到達(dá) threshold 擴(kuò)容閾值時(shí)調(diào)用)
private void rehash() {
// 1、全數(shù)組清理
expungeStaleEntries();
// 2、用較低的閾值判斷是否需要擴(kuò)容
if (size >= threshold - threshold / 4)
// 3、真正執(zhí)行擴(kuò)容
resize();
}
// -> 1、完整散列表清理
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)
// 很奇怪為什么不修改 j 指針
expungeStaleEntry(j);
}
}
// -> 3、真正執(zhí)行擴(kuò)容
private void resize() {
Entry[] oldTab = table;
// 擴(kuò)容為 2 倍
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) {
// 清除無效鍵值的 Value
e.value = null; // Help the GC
} else {
// 將舊數(shù)組上的鍵值對(duì)再散列到新數(shù)組上
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 計(jì)算擴(kuò)容后的新容量和新擴(kuò)容閾值
setThreshold(newLen);
size = count;
table = newTab;
}
}
4.7 ThreadLocalMap 的移除方法
ThreadLocalMap 的移除方法是添加方法的逆運(yùn)算,ThreadLocalMap 也沒有做動(dòng)態(tài)縮容。
與常規(guī)的移除操作不同的是,ThreadLocalMap 在刪除時(shí)會(huì)執(zhí)行 expungeStaleEntry() 清除無效節(jié)點(diǎn),并對(duì)連續(xù)段中的有效節(jié)點(diǎn)做再散列,所以 ThreadLocalMap 是 “真刪除”。
ThreadLocal.java
static class ThreadLocalMap {
// 移除
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 散列值轉(zhuǎn)數(shù)組下標(biāo)
int i = key.threadLocalHashCode & (len-1);
// 線性探測(cè)
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 清除弱引用關(guān)系
e.clear();
// 清理連續(xù)段
expungeStaleEntry(i);
return;
}
}
}
}
4.8 ThreadLocalMap 復(fù)雜度分析
總結(jié)下 ThreadLocalMap 的時(shí)間復(fù)雜度,以下 K 為連續(xù)段的長(zhǎng)度,N 是數(shù)組長(zhǎng)度。
- 獲取方法: 平均時(shí)間復(fù)雜度為 O(K);
- 添加方法: 平均時(shí)間復(fù)雜度為 O(K),在觸發(fā)擴(kuò)容的添加操作中時(shí)間復(fù)雜度為 O(N),基于攤還分析后時(shí)間復(fù)雜度依然是 O(K);
- 移除方法: 移除是 “真刪除”,平均時(shí)間復(fù)雜度為 O(K)。
4.9 訪問 ThreadLocal 一定會(huì)清理無效數(shù)據(jù)嗎?
不一定。只有擴(kuò)容會(huì)觸發(fā)完整散列表清理,其他情況都不能保證清理,甚至不會(huì)觸發(fā)。
5. 總結(jié)
- 1、ThreadLocal 是一種特殊的無鎖線程安全方式,通過為每個(gè)線程分配獨(dú)立的資源副本,從根本上避免發(fā)生資源沖突;
- 2、ThreadLocal 在所有線程間隔離,InheritableThreadLocal 在創(chuàng)建子線程時(shí)會(huì)拷貝父線程中 InheritableThreadLocal 的有效鍵值對(duì);
- 3、雖然 ThreadLocal 提供了自動(dòng)清理數(shù)據(jù)的能力,但是自動(dòng)清理存在滯后性。為了避免內(nèi)存泄漏,在業(yè)務(wù)開發(fā)中應(yīng)該及時(shí)調(diào)用 remove 清理無效的局部存儲(chǔ);
- 4、ThreadLocal 是采用線性探測(cè)解決散列沖突的散列表。
ThreadLocal 思維導(dǎo)圖

參考資料
- 數(shù)據(jù)結(jié)構(gòu)與算法分析 · Java 語(yǔ)言描述(第 5 章 · 散列)—— [美] Mark Allen Weiss 著
- 算法導(dǎo)論(第 11 章 · 散列表)—— [美] Thomas H. Cormen 等 著
- 《阿里巴巴Java開發(fā)手冊(cè)》 楊冠寶 編著
- 數(shù)據(jù)結(jié)構(gòu)與算法之美(第 18~22 講) —— 王爭(zhēng) 著,極客時(shí)間 出品
- ThreadLocal 和 ThreadLocalMap源碼分析 —— KingJack 著
- Why 0x61c88647? —— Dr. Heinz M. Kabutz 著