從TL、ITL到TT

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/07/17/17560467.html
-Advertisement-
Play Games

ThreadLocal(TL)、InheritableThreadLocal(ITL)和TransmittableThreadLocal(TTL)在不同場景下有不同用途,本文我們來分析一下 ...


1、概述

ThreadLocal(TL)是Java中一種線程局部變數實現機制,他為每個線程提供一個單獨的變數副本,保證多線程場景下,變數的線程安全。經常用於代替參數的顯式傳遞。

InheritableThreadLocal(ITL)是JDK提供的TL增強版,而TransmittableThreadLocal(TTL)是阿裡開源的ITL增強版

這些ThreadLocal在不同場景下有不同用途,我們來分析一下:

2、ThreadLocal

ThreadLocal主要的方法有四個:initialValue、set、get、remove

2.1、初始化——initialValule

當線程首次訪問該ThreadLocal時(ThreadLocal.get()),會進行初始化賦值。我們常用兩種方法初始化ThreadLocal

2.1.1、重寫initialValue

ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "";
    }
};

2.1.2、調用ThreadLocal.withInitial

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");

他會創建一個SuppliedThreadLocal內部類

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

該類重寫了initialValue方法

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        //當該線程首次訪問ThreadLocal時,會間接調用lambda表達式初始化
        return supplier.get();
    }
}

⚠️ITL並沒有重新實現withInitial,如果使用withInitial則會創建STL,失去自己增強的特性

2.2、賦值——set

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

這裡出現了一個關鍵屬性ThreadLocalMap,類定義在ThreadLocal中,是Thread的成員變數

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocalMap內部還有一個內部類Entry,是存值的地方

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            //ThreadLocal的引用是“key”
            super(k);
            //線程局部變數是value
            value = v;
        }
    }
    //Entry數組
    //value具體放在哪個index下,是由ThreadLocal的hashCode算出來的
    private Entry[] table;
}

2.3、取值——get

public T get() {
    Thread t = Thread.currentThread();
    //1、獲取線程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //2、根據ThreadLocal的hashCode,獲取對應Entry下的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //3、如果沒有賦過值,則初始化
    return setInitialValue();
}

2.4、清空——remove

 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         //會將對應Entry、包括他的key、value手動置null
         m.remove(this);
 }

3、InheritableThreadLocal

3.1、TL在父子線程場景下存在的問題

我們先來看一個例子

public static void main(String[] args) throws InterruptedException {
    ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A");
    threadLocal.set("B");
    Thread thread = new Thread(() -> {
        System.out.println("子線程ThreadLocal:" + threadLocal.get());
    }, "子線程");
    thread.start();
    thread.join();
}

列印結果如下,可見子線程的ThreadLocal是初始值,並沒有使用父線程修改後的值:

子線程ThreadLocal:A

線程的ThreadLocalMap是首次訪問時創建的,所以子線程使用ThreadLocal的時候,會初始化一個新的ThreadLocal,線程局部變數為預設值

⚠️所以,TL不具有遺傳性

3.2、ITL的解決方案

為瞭解決TL子線程遺傳性的問題,JDK引入了ITL

他繼承ThreadLocal,重寫了childValue、getMap、createMap三個方法

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

這裡出現了inheritableThreadLocals,他存儲的就是從父線程拷貝過來的ThreadLocal,這個值是在父線程首次修改ThreadLocal的時候賦值的,然後在子線程創建時拷貝過來的

//父線程部分:
public void set(T value) {
    Thread t = Thread.currentThread();
    //該方法被ITL重寫,訪問inheritableThreadLocals為null
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    //該方法同樣被ITL重寫,創建一個ThreadLocalMap賦值給inheritableThreadLocals
        createMap(t, value);
}

//子線程部分:
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    //省略一些代碼...

    //獲取當前線程(父線程、也就是創建子線程的線程)
    Thread parent = currentThread();
    //1、允許ThreadLocal遺傳(這個預設為true)
    //2、inheritableThreadLocals不為空,因為父線程調用set了
    //父線程不調用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用進該分支
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

//createInheritedMap使用該構造函數,根據父線程的inheritableThreadLocals進行深拷貝
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    //深拷貝父線程ThreadLocalMap
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //childValue被ITL重寫,返回父線程ThreadLocal的值
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

使用ITL的效果

public static void main(String[] args) throws InterruptedException {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
            @Override
            protected String initialValue() {
                return "A";
            }
        };
        threadLocal.set("B");
        Thread thread = new Thread(() -> {
            System.out.println("子線程ThreadLocal:" + threadLocal.get());
        }, "子線程");
        thread.start();

        thread.join();
}

列印結果如下,子線程拷貝了父線程ThreadLocal:

子線程ThreadLocal:B

總結一下,ITL解決父子線程遺傳性的核心思路是,將可遺傳的ThreadLocal放在父線程新的ThreadLocalMap中,在子線程首次使用時進行拷貝

4.、TransmittableThreadLocal

4.1、ITL線上程復用場景下存在的問題

我們再從一個簡單的例子說起

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "A";
        }
    };
    threadLocal.set("B");
    ExecutorService executorService = Executors.newFixedThreadPool(1);
    //1、子線程第一次獲取ThreadLocal
    executorService.submit(() -> System.out.println("子線程ThreadLocal:"+threadLocal.get())).get();
    Thread.sleep(1000);
    //2、父線程修改ThreadLocal
    threadLocal.set("C");
    System.out.println("父線程修改ThreadLocal為"+threadLocal.get());
    //3、子線程第二次獲取ThreadLocal
    executorService.submit(() -> System.out.println("子線程ThreadLocal:"+threadLocal.get())).get();
}

列印結果如下,子線程在第二次列印時,並沒有拷貝父線程的ThreadLocal,使用的還是首次拷貝的值:

子線程ThreadLocal:B
父線程修改ThreadLocal為C
子線程ThreadLocal:B

⚠️可復用的子線程不會感知父線程ThreadLocal的變化

4.2、TTL的解決方案

4.2.1、TTL的使用

TTL在ITL上做了稍微複雜的封裝,我們從使用開始瞭解

引入依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>latest</version>
</dependency>

在使用TTL時,線程需要經過TTL封裝,線程池同理

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "A";
        }
    };
    threadLocal.set("B");
    ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
    executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get())).get();
    Thread.sleep(1000);
    threadLocal.set("C");
    System.out.println("父線程修改ThreadLocal為" + threadLocal.get());
    executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get())).get();
    Thread.sleep(1000);
    executorService.submit(() -> {
        threadLocal.set("D");
        System.out.println("子線程修改ThreadLocal為" + threadLocal.get());
    });
    Thread.sleep(1000);
    executorService.submit(() -> System.out.println("子線程ThreadLocal:" + threadLocal.get()));
    Thread.sleep(1000);
}

列印結果如下,子線程每次都會獲取父線程的ThreadLocal

子線程ThreadLocal:B
父線程修改ThreadLocal為C
子線程ThreadLocal:C
子線程修改ThreadLocal為D
子線程ThreadLocal:C

從使用上看,TTL要求將任務封裝,那我們就從ThreadLocal和ExecutorService兩部分入手

4.2.2、TTL對ThreadLocal的封裝

下麵是TTL的取值和賦值邏輯,都涉及一個關鍵方法addThisToHolder,對應的屬性holder會線上程池執行任務時用到

//TransmittableThreadLocal.addThisToHolder()
private void addThisToHolder() {
    //InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder
    if (!holder.get().containsKey(this)) {
        //holder是靜態變數,他會把TTL存到當前線程的map中
        //value是null,他其實是把Map當Set用
        //主線程賦值時,會獲取主線程的holderMap,然後把TTL存進去
        holder.get().put((TransmittableThreadLocal<Object>) this, null);
    }
}

@Override
public final void set(T value) {
    if (!disableIgnoreNullValueSemantics && null == value) {
        remove();
    } else {
        super.set(value);
        //當主線程賦值時,會將自己的TTL放到自己的map中
        addThisToHolder();
    }
}

@Override
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || null != value) 
        addThisToHolder();
    return value;
}

4.2.3、TTL對任務的封裝

//我們通過TtlExecutors.getTtlExecutorService()對線程池進行封裝
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
    if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
        return executorService;
    }
    //入參是線程池,通過包裝類代理線程池的操作
    return new ExecutorServiceTtlWrapper(executorService);
}

//ExecutorServiceTtlWrapper.submit()
public Future<?> submit(@NonNull Runnable task) {
    //將提交的任務進行封裝
    return executorService.submit(TtlRunnable.get(task));
}

4.2.3.1、任務構建

TtlRunnable構造方法

這裡都是主線程在操作,因為任務是主線程提交的

private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    this.capturedRef = new AtomicReference<Object>(capture());
    this.runnable = runnable;
    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}

這裡有一個關鍵屬性capturedRef,他是一個原子引用,存了TTL

//TrasmitableThreadLocal.Transmitter
public static Object capture() {
    //獲取ttl的值構建快照
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
    HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
    for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
        //將主線程TTL的值存到當前任務中
        ttl2Value.put(threadLocal, threadLocal.copyValue());
    }
    return ttl2Value;
}

4.2.3.2、任務執行

任務執行的代碼如下,在任務執行前回放ThreadLocal,在任務執行後恢復ThreadLocal:

這裡都是子線程在操作,因為任務都是子線程執行的

@Override
public void run() {
    Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    //1、備份子線程ThreadLocal
    //2、使用主線程提交任務時構建的ThreadLocal副本,將子線程ThreadLocal覆蓋
    Object backup = replay(captured);
    try {
        //3、任務執行
        runnable.run();
    } finally {
        //3、使用之前備份的子線程ThreadLocal進行恢復
        restore(backup);
    }
}

總結一下,TTL讓子線程感知父線程變化的核心思路是,主線程在任務提交時構建ThreadLocal副本,在子線程執行任務時供其使用

⚠️提交和執行任務會對TTL進行若幹操作,理論上對性能有一點點影響,官方性能測試結論說損耗可忽略

TTL官方性能測試

作者:京東物流 劉朝永

來源:京東雲開發者 自猿其說


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對於這兩個十分接近的設計模式,確實容易產生困惑,代理模式和裝飾器模式看起來十分相似,都是由兩個類實現相同的介面,然後一個類套另一個類。這件事足足困擾了我5分鐘之久,在此總結一下它們的差別。 ## 目的不同 當你想要增強你的類,給你的類增加新功能的話,你就該使用裝飾器模式了。 裝飾器模式由於添加了功能 ...
  • # 【RabbitMQ】當隊列中消息數量超過最大長度的淘汰策略 ## 說明 最近在研究RabbitMQ如何實現延時隊列時發現消息進入死信隊列的情況之一就是當消息數量超過隊列設置的最大長度時會被丟入死信隊列,看到這時我就產生了一個疑問,到底是最後插入的消息還是最早插入的消息會被丟入死信隊列呢?遺憾的是 ...
  • 淺克隆(shallow clone)和深克隆(deep clone)是兩種不同的對象複製方法。 淺克隆會創建一個新對象,然後將原始對象的所有欄位複製到新對象中。如果欄位是基本類型,則它們的值將被直接複製。如果欄位是引用類型,則只會複製引用,而不會複製引用指向的對象。這意味著原始對象和克隆對象中的引用 ...
  • 在最近發佈的Spring 6.1 M2版本中,推出了一個全新的同步HTTP客戶端:`RestClient`。用一句話來讓Spring開發者認識`RestClient`的話:像`WebClient`一樣具備流暢API的`RestTemplate`。所以,`RestClient`的使命就是淘汰已經有14 ...
  • ## 背景 一直以來習慣用docker配置一些本地學習環境,許多教程配置activiti的方式都是通過複製activiti的war包部署在tomcat中,我嘗試了一下通過docker的方式遇到了一些不易察覺的錯誤。 ## 使用方式描述 1. 使用docker安裝tomcat9.0 `docker r ...
  • # 1. 迴圈依賴 ## 1.1 什麼是迴圈依賴 首先,什麼是迴圈依賴?這個其實好理解,就是兩個 Bean 互相依賴,類似下麵這樣: """ @Service public class AService { @Autowired BService bService; } @Service publi ...
  • Go語言流媒體開源項目 [LAL](https://github.com/q191201771/lal) 今天發佈了v0.36.7版本。 > LAL 項目地址:https://github.com/q191201771/lal 老規矩,簡單介紹一下: ▦ Customize Sub,我有的都給你 這 ...
  • 哈嘍大家好,我是鹹魚 我們知道,python 在自動化領域中被廣泛應用,可以很好地自動化處理一些任務 就比如編寫 Python 腳本自動化執行重覆性的任務,如文件處理、數據處理、系統管理等需要運行其他程式或者與操作系統交互的任務 那麼今天我們來看一下在 python 中如何運行 shell 命令來與 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...