ThreadLocal 超強(qiáng)圖解,這次終于懂了~

前言

大家好,我是小彭。

在前面的文章里,我們聊到了散列表的開放尋址法和分離鏈表法,也聊到了 HashMapLinkedHashMapWeakHashMap 等基于分離鏈表法實(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ǔ)和查詢操作。

散列表示意圖

Untitled 1.png

在從鍵值對(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#getThreadLocal#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 示意圖

Untitled 1.png

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 示意圖

Untitled 2.png

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)系示意圖

Untitled 3.png

然而,自動(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í)案例:

android.os.Looper.java

// /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è)置缺省值。
  • 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)圖


參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容