最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java ThreadLocal取不到值的三種原因及解決方法

 更新時間:2026年02月03日 08:48:24   作者:程序員越  
在實際開發(fā)中,我們經(jīng)常會遇到 ThreadLocal 取值為 null 的情況,排查起來往往無從下手,本文將詳細(xì)拆解 ThreadLocal 取不到值的三種核心原因,結(jié)合原理、場景和代碼示例,幫助大家快速定位并解決問題,需要的朋友可以參考下

一、三種核心原因

最常見原因:多個線程操作 ThreadLocal

這是開發(fā)中最易出現(xiàn)的情況,本質(zhì)是對 ThreadLocal 的核心設(shè)計理念理解不到位——ThreadLocal 的變量副本是綁定到具體線程的,每個線程都有自己獨立的副本,線程之間的副本相互隔離、互不影響。

原理解析: ThreadLocal 內(nèi)部維護(hù)了一個 ThreadLocalMap,這個 Map 是線程(Thread)的私有屬性(threadLocals),而非 ThreadLocal 自身的屬性。當(dāng)我們調(diào)用 threadLocal.set(value) 時,是向當(dāng)前線程(Thread.currentThread())的 ThreadLocalMap 中存入鍵值對(鍵為當(dāng)前 ThreadLocal 對象,值為變量副本);調(diào)用 threadLocal.get() 時,也是從當(dāng)前線程的 ThreadLocalMap 中根據(jù)當(dāng)前 ThreadLocal 對象取對應(yīng)的值。

問題場景: 如果線程 A 調(diào)用 set 存入值,線程 B 調(diào)用 get 取值,由于線程 B 的 ThreadLocalMap 中沒有線程 A 存入的鍵值對,自然會返回 null。這種問題常出現(xiàn)在多線程任務(wù)、線程池復(fù)用場景中。

代碼示例:

public class ThreadLocalMultiThreadTest {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 線程1:存入值
        new Thread(() -> {
            threadLocal.set("線程1的變量副本");
            System.out.println("線程1取值:" + threadLocal.get()); // 正常輸出:線程1的變量副本
        }).start();

        // 線程2:嘗試取值(未存入)
        new Thread(() -> {
            System.out.println("線程2取值:" + threadLocal.get()); // 輸出:null
        }).start();
    }
}

易忽略原因:類加載器不同導(dǎo)致取不到值

這種原因相對隱蔽,很多開發(fā)者會忽略類加載器的影響,其本質(zhì)是:不同類加載器加載同一個類(全限定名相同),會生成兩個不同的 Class 對象;而 ThreadLocal 作為變量的載體,若其所在的類被不同類加載器加載,會產(chǎn)生多個 ThreadLocal 實例,導(dǎo)致 set 和 get 操作的不是同一個 ThreadLocal 對象,最終取不到值。

原理解析: Java 中,一個類的唯一性由「類加載器 + 類全限定名」共同決定。假設(shè)我們有一個類 A,里面定義了 static ThreadLocal 變量,當(dāng)類加載器1(如系統(tǒng)類加載器)加載類 A 時,會創(chuàng)建 ClassA1 和 ThreadLocal1;當(dāng)類加載器2(如自定義類加載器)加載類 A 時,會創(chuàng)建 ClassA2 和 ThreadLocal2。此時,ThreadLocal1 和 ThreadLocal2 是兩個完全獨立的對象,調(diào)用 ThreadLocal1.set() 存入的值,在 ThreadLocal2.get() 中無法獲?。ㄒ驗榇嫒氲氖钱?dāng)前線程 ThreadLocalMap 中 ThreadLocal1 對應(yīng)的鍵值對,ThreadLocal2 作為不同的鍵,自然找不到對應(yīng)值)。

問題場景: 常見于Spring Boot DevTools 熱部署等場景。

代碼示例:

import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.function.Supplier;

public class StaticClassLoaderTest implements Supplier<Integer> {

    // 靜態(tài)ThreadLocal變量(類加載器不同時,會產(chǎn)生多個該變量實例)
    protected static final ThreadLocal<Integer> local = new ThreadLocal<Integer>();

    public StaticClassLoaderTest() {
    }

    // 存入值:向當(dāng)前線程的ThreadLocalMap中存入local(當(dāng)前類加載器對應(yīng)的實例)和值
    public void setInfo(Integer val) {
        local.set(val);
        System.out.println("set成功,當(dāng)前類加載器:" + this.getClass().getClassLoader());
    }

    public static void main(String[] args) {
        try {
            // 1. 使用系統(tǒng)類加載器(AppClassLoader)加載當(dāng)前類,創(chuàng)建實例并存入值
            StaticClassLoaderTest supplier1 = new StaticClassLoaderTest();
            supplier1.setInfo(2); // 此時存入的是「AppClassLoader加載的local實例」對應(yīng)的值

            // 2. 使用自定義類加載器(cusLoader)加載當(dāng)前類,創(chuàng)建實例并嘗試取值
            // 自定義類加載器加載的StaticClassLoaderTest,與系統(tǒng)類加載器加載的是不同的Class對象
            Supplier<Integer> staticClassLoaderTest2 = (Supplier<Integer>) 
                    Class.forName("gittest.StaticClassLoaderTest", true, new CusLoader())
                          .newInstance();
            
            // 取值失?。簊taticClassLoaderTest2的local是「CusLoader加載的local實例」,與supplier1的local不是同一個對象
            System.out.println("自定義類加載器取值:" + staticClassLoaderTest2.get()); // 輸出:null

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 自定義類加載器:重寫loadClass方法,優(yōu)先加載指定類
    public static class CusLoader extends ClassLoader {
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            // 只攔截當(dāng)前類的加載
            if (name.contains("StaticClassLoaderTest")) {
                // 從類路徑中獲取class文件的輸入流
                InputStream is = Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream(name.replace(".", "/") + ".class");
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                try {
                    // 讀取class文件內(nèi)容
                    IOUtils.copy(is, output);
                    // 定義Class對象(自定義類加載器加載)
                    return defineClass(name, output.toByteArray(), 0, output.toByteArray().length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 其他類仍使用父類加載器加載(遵循雙親委派模型)
            return super.loadClass(name, resolve);
        }
    }

    // 取值方法:從當(dāng)前線程的ThreadLocalMap中獲取local對應(yīng)的值
    @Override
    public Integer get() {
        System.out.println("get操作,當(dāng)前類加載器:" + this.getClass().getClassLoader());
        return StaticClassLoaderTest.local.get();
    }
}

**代碼說明:**運(yùn)行后會發(fā)現(xiàn),supplier1(系統(tǒng)類加載器)和 staticClassLoaderTest2(自定義類加載器)的類加載器不同,對應(yīng)的 local 實例也不同,因此 staticClassLoaderTest2.get() 無法獲取到 supplier1.setInfo(2) 存入的值,最終返回 null。

特殊原因:父子線程間取值

這種原因主要出現(xiàn)在父子線程場景中:父線程調(diào)用 ThreadLocal.set() 存入值后,子線程調(diào)用 ThreadLocal.get() 取值,結(jié)果為 null。核心原因是:父線程和子線程是兩個獨立的線程,各自擁有自己的 ThreadLocalMap,子線程不會繼承父線程的 ThreadLocalMap 中的數(shù)據(jù)

原理解析: ThreadLocal 的變量副本是綁定到具體線程的,父線程的 ThreadLocalMap 是父線程的私有屬性,子線程啟動時,會初始化自己的 ThreadLocalMap(為空),不會自動復(fù)制父線程 ThreadLocalMap 中的鍵值對。因此,即使父線程已經(jīng)存入值,子線程調(diào)用 get() 時,自己的 ThreadLocalMap 中沒有對應(yīng)的數(shù)據(jù),就會返回 null。

問題場景: 常見于線程池中子線程依賴父線程數(shù)據(jù)、異步任務(wù)(如 CompletableFuture、Thread 子類)、Spring @Async 異步方法等場景。

代碼示例:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// 啟動類:必須添加@EnableAsync,開啟Spring異步功能
@SpringBootApplication
@EnableAsync
public class AsyncThreadLocalTest {
    public static void main(String[] args) {
        SpringApplication.run(AsyncThreadLocalTest.class, args);
    }

    // ThreadLocal工具類:模擬存儲父線程(請求線程)的上下文數(shù)據(jù)
    public static class ThreadLocalUtil {
        private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

        public static void setContext(String value) {
            CONTEXT.set(value);
        }

        public static String getContext() {
            return CONTEXT.get();
        }

        public static void remove() {
            CONTEXT.remove();
        }
    }

    // 異步服務(wù)類:@Async標(biāo)注的方法會在獨立線程(子線程)中執(zhí)行
    @Service
    public static class AsyncService {
        // @Async:該方法脫離父線程,在Spring默認(rèn)線程池的子線程中執(zhí)行
        @Async
        public void asyncGetContext() {
            // 子線程(異步方法)嘗試獲取父線程存入的ThreadLocal值
            String context = ThreadLocalUtil.getContext();
            System.out.println("異步方法(子線程)取值:" + context); // 輸出:null
            System.out.println("異步方法(子線程)ID:" + Thread.currentThread().getId());
        }
    }

    // 測試控制器:父線程(請求線程)存入值,調(diào)用異步方法
    @RestController
    public static class TestController {
        private final AsyncService asyncService;

        // 構(gòu)造器注入異步服務(wù)
        public TestController(AsyncService asyncService) {
            this.asyncService = asyncService;
        }

        @GetMapping("/test/async")
        public String testAsync() {
            // 父線程(請求線程):存入值到ThreadLocal
            ThreadLocalUtil.setContext("父線程(請求線程)的上下文數(shù)據(jù)");
            System.out.println("父線程(請求線程)取值:" + ThreadLocalUtil.getContext()); // 正常輸出
            System.out.println("父線程(請求線程)ID:" + Thread.currentThread().getId());

            // 調(diào)用異步方法:觸發(fā)子線程執(zhí)行
            asyncService.asyncGetContext();

            // 清理資源,避免內(nèi)存泄漏
            ThreadLocalUtil.remove();
            return "異步請求測試完成,查看控制臺輸出";
        }
    }
}

解決方案:若需要實現(xiàn) @Async 異步方法(子線程)獲取父線程(請求線程)的 ThreadLocal 數(shù)據(jù),可結(jié)合以下兩種方式:

  1. 簡單場景:使用 Java 自帶的 InheritableThreadLocal 替換 ThreadLocal,它會在子線程啟動時,自動復(fù)制父線程 InheritableThreadLocal 中的數(shù)據(jù)到子線程;但注意,線程池復(fù)用場景下(Spring @Async 默認(rèn)使用線程池),子線程不會重新初始化,復(fù)制邏輯僅執(zhí)行一次,會導(dǎo)致數(shù)據(jù)錯亂。
  2. 實際開發(fā)場景(推薦):使用 TransmittableThreadLocal(TTL)框架,專門解決線程池復(fù)用場景下的 ThreadLocal 數(shù)據(jù)傳遞問題,完美適配 Spring @Async。只需引入 TTL 依賴,替換 ThreadLocal 為 TransmittableThreadLocal,即可實現(xiàn)異步方法正常獲取父線程數(shù)據(jù),無需額外復(fù)雜配置。

二、總結(jié)

ThreadLocal 取不到值,本質(zhì)都是「set 和 get 操作的線程不匹配」或「set 和 get 操作的 ThreadLocal 對象不匹配」,具體可歸納為:

  • 多個線程操作:set 和 get 屬于不同線程,線程的 ThreadLocalMap 相互獨立;
  • 類加載器不同:set 和 get 操作的 ThreadLocal 對象,屬于不同類加載器加載的 Class 實例,是兩個不同對象;
  • 父子線程:父線程和子線程是獨立線程,子線程不會繼承父線程的 ThreadLocal 數(shù)據(jù)。

以上就是Java ThreadLocal取不到值的三種原因及解決方法的詳細(xì)內(nèi)容,更多關(guān)于Java ThreadLocal取不到值的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Netty中解碼器的作用及實現(xiàn)詳解

    Netty中解碼器的作用及實現(xiàn)詳解

    這篇文章主要介紹了Netty中解碼器的作用及實現(xiàn)詳解,本章我們只需要讓客戶端發(fā)送消息的時候循環(huán)發(fā)送100次,服務(wù)端不變,看看服務(wù)端是不是接收到了100條消息,需要的朋友可以參考下
    2023-12-12
  • springMVC實現(xiàn)前臺帶進(jìn)度條文件上傳的示例代碼

    springMVC實現(xiàn)前臺帶進(jìn)度條文件上傳的示例代碼

    本篇文章主要介紹了springMVC實現(xiàn)前臺帶進(jìn)度條文件上傳的示例代碼,具有一定的參考價值,有興趣的可以了解一下。
    2017-01-01
  • metershpere實現(xiàn)調(diào)用自定義jar包中的方法

    metershpere實現(xiàn)調(diào)用自定義jar包中的方法

    在MeterSphere接口測試中,面對多層循環(huán)邏輯和邏輯判斷等復(fù)雜情況,直接編寫測試用例往往顯得混亂不便,本文介紹了一個簡化這一過程的方法:首先使用IDEA創(chuàng)建Maven工程,編寫所需邏輯并生成jar包;然后在MeterSphere中上傳此jar包
    2024-10-10
  • 詳解Java枚舉為什么是單例模式的最佳選擇

    詳解Java枚舉為什么是單例模式的最佳選擇

    這篇文章主要為大家詳細(xì)介紹了Java枚舉為什么是單例模式的最佳選擇,文中通過簡單的示例進(jìn)行了講解,具有一定的學(xué)習(xí)價值,需要的可以參考一下
    2023-05-05
  • SpringCloud Config使用本地倉庫及map注入

    SpringCloud Config使用本地倉庫及map注入

    這篇文章主要介紹了SpringCloud Config使用本地倉庫及map注入,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-09-09
  • 使用Java接收和處理OpenTelemetry數(shù)據(jù)的完整指南

    使用Java接收和處理OpenTelemetry數(shù)據(jù)的完整指南

    在現(xiàn)代分布式系統(tǒng)中,OpenTelemetry 成為了一種常見的標(biāo)準(zhǔn),用于跟蹤和監(jiān)控應(yīng)用程序的性能和行為,OTLP是 OpenTelemetry 社區(qū)定義的一種數(shù)據(jù)傳輸協(xié)議,文將介紹如何使用 Java 編寫代碼來接收和處理 OTLP 數(shù)據(jù),需要的朋友可以參考下
    2024-04-04
  • SpringBoot Actuator跟蹤HTTP請求和響應(yīng)的方法

    SpringBoot Actuator跟蹤HTTP請求和響應(yīng)的方法

    Spring Boot Actuator 是 Spring Boot 提供的生產(chǎn)級監(jiān)控和管理模塊,旨在幫助開發(fā)者實時監(jiān)控應(yīng)用狀態(tài)、收集運(yùn)行時指標(biāo),并提供一系列管理端點,本文給大家介紹了SpringBoot Actuator跟蹤HTTP請求和響應(yīng)的方法,需要的朋友可以參考下
    2025-08-08
  • java文件上傳下載代碼實例

    java文件上傳下載代碼實例

    這篇文章主要介紹了java文件上傳下載,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • 通過JDK源碼分析關(guān)閉鉤子詳解

    通過JDK源碼分析關(guān)閉鉤子詳解

    一個簡單的關(guān)閉鉤子,程序被中斷或者正常退出時會顯示 hook shutdown!非常的優(yōu)雅,有效,巧妙。那么這篇文章就來給大家介紹關(guān)于通過JDK源碼分析關(guān)閉鉤子的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒。
    2017-11-11
  • 理解Java中的內(nèi)存泄露及解決方法示例

    理解Java中的內(nèi)存泄露及解決方法示例

    這篇文章主要介紹了理解Java中的內(nèi)存泄露及解決方法示例,本文講解了Java內(nèi)存管理機(jī)制、Java內(nèi)存泄露、一般情況下內(nèi)存泄漏的避免、復(fù)雜數(shù)據(jù)結(jié)構(gòu)中的內(nèi)存泄露問題等內(nèi)容,需要的朋友可以參考下
    2015-03-03

最新評論

正安县| 江津市| 正定县| 确山县| 石柱| 永顺县| 手机| 介休市| 高碑店市| 灵丘县| 岑巩县| 黎川县| 晋州市| 泰兴市| 鹤岗市| 浦城县| 惠州市| 甘孜| 疏附县| 若羌县| 江都市| 姜堰市| 临湘市| 县级市| 仪陇县| 泗水县| 西华县| 绥中县| 称多县| 佛坪县| 昭通市| 霍城县| 淮滨县| 海南省| 紫阳县| 平阴县| 玉树县| 罗源县| 郎溪县| 固安县| 磐石市|