java多线程之ThreadLocal
2026/8/7 17:09:28 网站建设 项目流程

ThreadLocal介绍及使用场景

并发编程中存在两种情况:

1、所有线程共享一个变量,且任意一线程对该变量的执行结果的改变会影响其他线程,此种情况下需对该变量加锁,防止脏数据的产生。(例如:多窗口购买火车票,每个窗口的火车票剩余数量的改变会直接影响其他窗口)

2、所有线程用一个变量名,但各个线程对该变量结果的改变不影响其他线程,使改变的结果保持在当前线程内。例如我整个项目每个方法都会用到userId(用户ID),这样我每一个方法的入参都需要附带一个userId,由于每个用户对应不同的线程,且每个用户的userId不同,彼此隔离没有任何关系,这种情况适用ThreadLocal,可为每一个线程提供一个线程级别的全局变量副本。使用ThreadLocal后,不再需要方法体提供userId入参,而是在全局想哪里获取就哪里获取,增加了代码的灵活性,减少了冗余。




ThreadLocal理解

1、ThreadLocal不同于共享变量,其在多个线程中对应多个副本,且每个副本之间没有关联。

2、ThreadLocal不适用于线程同步,其出现的目的只是为了各个线程维护自己的变量。

ThreadLocal是线程的局部变量,是每一个线程所单独持有的,其他线程不能对其进行访问,通常是类中的private static字段。

ThreadLocal方法

ThreadLocal 的几个方法: ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。

publicTget(){}// 用来获取ThreadLocal在当前线程中保存的变量副本publicvoidset(Tvalue){}//set()用来设置当前线程中变量的副本publicvoidremove(){}//remove()用来移除当前线程中变量的副本protectedTinitialValue(){}//initialValue()是一个protected方法,一般是用来在使用时进行重写的// 来初始化变量的方法,在创建每一个ThreadLocal变量时,// 我们最好将其初始化为我们认为的初始值,不然他默认初始化值为NULL,后续操作时可能会出现问题;publicstatic<S>ThreadLocal<S>withInitial(Supplier<?extendsS>supplier)

原理/实现思路

✅ Thread 里面有 ThreadLocalMap
✅ Key 是 ThreadLocal 对象
✅ Value 是实际存的数据

Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的成员变量 threadLocals,每个线程都有一个属于自己的 ThreadLocalMap。

ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。

每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。

ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。

  • 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;

  • ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;

ThreadLocal.ThreadLocalMapthreadLocals=null;ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;

源码

protectedTinitialValue(){returnnull;}

该方法是一个 protected 的方法,显然是为了让子类重写而设计的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第一次调用 get() 时才执行,并且仅执行1次(即:线程第一次使用 get() 方法访问变量的时候才执行。如果线程先于 get() 方法调用 set(T) 方法,则不会在线程中再调用 initialValue() 方法)。ThreadLocal 中的缺省实现直接返回一个null。

publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}returnsetInitialValue();}

方法里面第一行获取当前线程,然后通过 getMap(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。

然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例,而不是当前线程 t。如果获取成功,则返回 value 值。

如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。


get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}

在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。

staticclassThreadLocalMap{/** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型)。

ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i,对此 set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。

删除线程中对应的值,remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除map中的value的值,不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象

作用及使用方式

先解释一下,在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。

那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。

使用方式

  • 场景1:initialValue
    如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制

  • 场景2:set
    如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,我们用set方法放进去,再用get方法取出来;

问题

ThreadLocal内存泄漏问题

这张图证明了线程隔离

上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏(内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放(这里指value回收不了了),造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。)

解决办法:使用完ThreadLocal后,执行remove(从ThreadLocal对象中删除一个值,根据键删除,键: 当前线程对象)操作,避免出现内存溢出内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。情况。

内存泄漏:某个对象不再有用,但是占用的内存不能被回收

staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}

我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。

如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,如果线程一直存在(比如在线程池中),那么 value 值将一直被引用,不能被回收。因为存在一条引用链的关系:Thread–>ThreadLocalMap–>Entry–>Value。造成内存泄漏,甚至有可能造成内存溢出OOM

这样会导致一种现象:key为null,value有值。
key为空的话value是无效数据,久而久之,value累加就会导致内存泄漏。


弱引用的特点:如果这个对象只被弱引用关联,那么这个对象就可以被回收,弱引用不会阻止GC。

强引用:通常 一个对象等于什么,比如 下面的 value = v;

ThreadLocalMap的每个Entry都是一个对key的弱引用,和一个对value的value的强引用。

正常情况下,当线程终止,保存在Threadlocal里面的Value会被垃圾回收,因此没有任何强引用了。

但是,如果线程不终止,保持很久,那么key对应的value就不能被回收,因此 有以下调用链:

Thread -> ThreadLocalMap -> Entry(key为null) -> value

因为value 和Thread之间还存在强引用链路,所以导致value无法回收,就可能出现oom

JDK已经考虑到这个问题,所以set,remove,rehash方法中会扫描key为null的Entry,并且把对应的value设置为null,这样value就可以被对象回收,下面的考虑,把强引用链给断掉。

if (k == null) { e.value = null; // Help the GC }

但是如果一个ThreadLocal不被使用,那么实际上set, remove,rehash方法也不会被调用,如果同时线程又不停止,那么用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄漏

  1. ThreadLocal会自动清除key为null的value
    ThreadLocal的get()、set()、remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

  2. 当使用完了对应的ThreadLocal,主动调用remove方法删除。
    remove方法会主动将当前的key和value(Entry)进行清除。

  3. 把ThreadLocal设置为全局变量
    ThreadLocal设置为全局变量使得它无法被GC回收(如果在成员变量 中使用就将修饰符设置为public static,ThreadLocal不会被回收也就不会存在key为null的情况, 也就不会内存泄漏)

第三种方案不太理解,是因为static归属于类所以不会被回收么?

注意点

1.如果可以不适用ThreadLoca就解决问题,那么不要强行使用

例如任务很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

2.优先使用框架的支持,而不是自己的创造

例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏

Entry的key可以设置为强引用吗?

我自己的理解其实是可以的,因为随着线程的释放就释放了嘛,但实际上多线程的话,生产用的都是线程池,那就永远释放不掉了,因为线程池的线程是可复用的。

不可以!

当ThreadLocalMap的key为强引用,回收ThreadLocal时因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏

要知道,ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。

为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉,也就是说:假如某个用户A执行方法时产生了一份threadlocalA,释放了threadlocalA后,作为弱引用,它会在下次垃圾回收时被清理掉。

而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。

Entry的value可以设置为弱引用吗?

不可以!

假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。

你使用ThreadLocal的目的就是要把这个value存储到当前线程中,并且希望在你需要的时候直接从当前线程中拿出来,那么意味着你的value除了当前线程对它持有强引用外,理论上来说,不应该再有其他强引用,否则你也不会把value存储进当前线程。但是一旦你把本应该强引用的value设计成了弱引用,那么只要jvm执行一次gc操作,你的value就直接被回收掉了。当你需要从当前线程中取值的时候,最终得到的就是null。

解决ThreadLocal父子线程的继承问题

ThreadLocal是没有继承的,父线程如果创建了一个子线程,子线程是接不到父线程的ThreadLocal的。

一般生产环境会出现两种父子线程的情况,一种是异步,另一种是一个请求内多个异步任务并行(CompletableFuture)

  1. 异步
packagecom.awu.springboot.code1.threadlocal.userinfo;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.task.TaskDecorator;importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;importjava.util.concurrent.Executor;importjava.util.concurrent.ThreadPoolExecutor;@ConfigurationpublicclassAwuThreadPoolConfig{@Bean("awuTaskExecutor")// 给这个线程池起个名字叫 "awuTaskExecutor",方便 @Async 引用publicExecutorawuTaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();// ========== 线程池基本参数配置 ==========// 核心线程数:即使空闲也保持存活的线程数,相当于"常驻员工"executor.setCorePoolSize(10);// 最大线程数:忙的时候最多能扩展到多少个线程,相当于"最多招多少临时工"executor.setMaxPoolSize(20);// 队列容量:当核心线程都在忙时,新任务先排队的最大数量executor.setQueueCapacity(100);// 线程名前缀:方便在日志中识别出这是异步任务线程executor.setThreadNamePrefix("awu-async-");// ========== 最关键的一步:装饰器 ==========// 作用:在提交任务时"拦截"一下,把父线程的数据拷贝给子线程// 好比:父线程把"工作证"复印一份交给子线程,让子线程知道"你是谁"executor.setTaskDecorator(newAwuContextCopyDecorator());// ========== 拒绝策略 ==========// 当线程池和队列都满了,新任务怎么办?// CallerRunsPolicy:让调用者(主线程)自己同步执行,不让任务丢失executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());// 初始化线程池(让配置生效)executor.initialize();returnexecutor;}/** * 上下文拷贝装饰器 * * 作用:把主线程中的 AwuRequestContext 数据,传递给异步子线程 * * 为什么要这么做? * 因为 ThreadLocal 是线程隔离的,父线程的数据子线程默认拿不到, * 但我们的业务需要子线程也能知道"当前用户是谁"、"traceId 是什么", * 所以必须手动复制一份给子线程。 */staticclassAwuContextCopyDecoratorimplementsTaskDecorator{@OverridepublicRunnabledecorate(Runnablerunnable){// ===== 第一步:在父线程中"捕获"数据 =====// 注意:这里的代码执行在调用 @Async 方法的线程(通常是处理请求的主线程)// 把父线程 ThreadLocal 里的用户信息和链路信息拿出来,存到局部变量中AwuUserInfoparentUser=AwuRequestContext.getUser();AwuTraceInfoparentTrace=AwuRequestContext.getTrace();// ===== 第二步:返回一个"包装后的"任务 =====// 这个返回的 Runnable 会提交给子线程执行,而不是直接执行原始的 runnablereturn()->{try{// ===== 第三步:在子线程中"粘贴"数据 =====// 注意:这里的代码运行在异步子线程中// 把刚才从父线程捕获的数据,设置到子线程自己的 ThreadLocal 中// 这样子线程里调用 AwuRequestContext.getUser() 就能拿到数据了AwuRequestContext.setUser(parentUser);AwuRequestContext.setTrace(parentTrace);// ===== 第四步:执行真正的业务逻辑 =====// 这就是你写的 @Async 方法里的代码,比如 sendLog()runnable.run();}finally{// ===== 第五步:清理子线程的 ThreadLocal =====// 为什么必须清理?// 因为线程池会复用线程,如果不清理,下一个任务会拿到上一个任务的脏数据!// 放在 finally 中保证即使业务代码抛异常,也一定会执行清理AwuRequestContext.clear();}};}}}@ServicepublicclassAwuAsyncService{@Async("awuTaskExecutor")publicvoidsendLog(){StringrealName=AwuRequestContext.getUserRealName();StringtraceId=AwuRequestContext.getTraceId();System.out.println("异步线程 - 用户: "+realName+", traceId: "+traceId);}
  1. 一个请求内多个异步任务并行(CompletableFuture)
@RestControllerpublicclassReportController{@GetMapping("/report")publicMap<String,Object>getReport(){// 父线程:Tomcat 工作线程AwuRequestContext.setUser(newAwuUserInfo("张三"));AwuRequestContext.setTrace(newAwuTraceInfo("trace-123"));// 3 个子线程并行执行(都继承父线程的上下文)CompletableFuture<String>future1=CompletableFuture.supplyAsync(()->{// 子线程1:需要用户信息AwuUserInfouser=AwuRequestContext.getUser();returnqueryUserData(user);},customExecutor);// 使用你配置的线程池CompletableFuture<String>future2=CompletableFuture.supplyAsync(()->{// 子线程2:需要 traceIdAwuTraceInfotrace=AwuRequestContext.getTrace();returnqueryOrderData(trace);},customExecutor);// 等待所有子线程完成CompletableFuture.allOf(future1,future2).join();returnresult;}}@RestControllerpublicclassReportController{@AutowiredprivateExecutorawuTaskExecutor;// 注入你配置的线程池@GetMapping("/report")publicMap<String,Object>getReport(){AwuRequestContext.setUser(newAwuUserInfo("张三"));// ✅ 使用你的线程池,装饰器生效!CompletableFuture<String>future=CompletableFuture.supplyAsync(()->{AwuUserInfouser=AwuRequestContext.getUser();// ✅ 拿到了!returnqueryData(user);},awuTaskExecutor);// ← 传入你的线程池returnfuture.join();}}

InheritableThreadLocal

基本不会用在生产环境。

InheritableThreadLocal 的核心机制是:只在创建新线程时,从父线程拷贝一次数据。但线程池的核心是线程复用,线程早就被创建好了,并不会每次执行任务都新建一个。

值传递失效:你期望子线程能拿到最新的父线程数据,但因为它复用的是旧线程,拿到的还是第一次创建时拷贝的老数据。比如你希望传递一个 trace-002,子线程拿到的却永远是第一次拷贝的 trace-001

TransmittableThreadLocal(阿里开源)

packagecom.awu.springboot.code1.threadlocal.transmittablethreadlocal;importcom.alibaba.ttl.threadpool.TtlExecutors;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;importjava.util.concurrent.Executor;importjava.util.concurrent.ThreadPoolExecutor;@ConfigurationpublicclassAwuThreadPoolConfig{@Bean("awuTaskExecutor")publicExecutorawuTaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();// ... 你的核心线程数、队列等配置保持不变 ...executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(100);executor.setThreadNamePrefix("awu-async-");executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());executor.initialize();// ★★★ 关键改动:用 TtlExecutors 包装一下,替代之前的 TaskDecorator ★★★returnTtlExecutors.getTtlExecutor(executor);}}

你之前的TaskDecorator是在提交任务时手动捕获和粘贴数据。而 TTL 的思路是:

  1. TransmittalbeThreadLocal会自己“记住”它被设置的值。

  2. TtlExecutors.getTtlExecutor()包装后,每次提交任务,它都会自动帮你做两件事:

    • 捕获:在父线程中捕获所有TransmittableThreadLocal的值。

    • 还原与清理:在子线程执行前,把这些值设置进去;执行完毕后,再自动清理掉。

这样一来,你就不需要再维护那个 AwuContextCopyDecorator 了,所有被包装过的线程池都能自动实现上下文传递,代码更简洁,也更不容易出错。

总结

ThreadLocal

  • 解决问题:

    • 维护了线程和value的关系
  • 存在问题:

    • 无法解决父线程向子线程传递ThreadLocal值的问题

InheritableThreadLocal

  • 解决问题:

    • JDK提供的,在通过new Thread创建子线程时,复制父线程的ThreadLocal值至子线程,触发开关为创建全新的子线程。
  • 存在问题:

    • 但线程池的线程复用机制打破了创建全新子线程的时机,导致子线程继承到错误的父线程ThreadLocal(还是复用前的子线程那个值)

TransmittableThreadLocal

  • 解决问题:

    • 阿里开源的TTL包,配合TtlExecutors、TtlRunnable、TtlCallable实现在子线程执行时的ThreadLocal值传递。

部分内容引用自:
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
https://www.cnblogs.com/dreamroute/p/5034726.html
https://blog.csdn.net/qq_35029061/article/details/86495625
https://baijiahao.baidu.com/s?id=1653790035315010634&wfr=spider&for=pc
https://www.cnblogs.com/sxkgeek/p/9406463.html
https://blog.csdn.net/Qynwang/article/details/131197107
https://blog.csdn.net/weixin_42201180/article/details/130028183
https://blog.csdn.net/qq_46119575/article/details/131565307
https://blog.csdn.net/adminBfl/article/details/130299939
https://www.jb51.net/article/214671.htm
https://blog.csdn.net/qq_26012495/article/details/86475725
https://blog.csdn.net/qq_26012495/article/details/104379137

每次我对你诉说,你从未做出过回应,
但考虑到我们相互鄙视,我不能责怪你的沉默

纸牌屋第一季

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询