ThreadLocal 原理与内存泄漏
2026/6/6 19:03:15 网站建设 项目流程

ThreadLocal很容易被一句话讲浅:它是线程本地变量。

这句话没错,但不够。面试里真正要讲清楚的是:

  1. 它解决什么问题。
  2. 值到底存在哪里。
  3. 为什么会有内存泄漏风险。
  4. 为什么用完要remove()

ThreadLocal 解决什么问题

ThreadLocal的核心作用是:让每个线程都有自己独立的一份变量副本,避免多个线程争用同一份共享变量。

比如 JDBC 场景里,每个线程可以把自己的Connection放到 ThreadLocal 中。这样线程 A 不会误用或关闭线程 B 的连接。

ThreadLocal

线程 A

线程 B

线程 C

资源副本 A

资源副本 B

资源副本 C

它也常用于保存线程上下文,比如登录用户信息、traceId、租户信息等。

但注意,ThreadLocal不是用来解决所有线程安全问题的。它解决的是“每个线程各用各的”这一类问题。如果多个线程本来就需要共同修改同一份数据,ThreadLocal不适合。

基本用法

PPT 里的例子很直接:

staticThreadLocal<String>threadLocal=newThreadLocal<>();publicstaticvoidmain(String[]args){newThread(()->{Stringname=Thread.currentThread().getName();threadLocal.set("itcast");print(name);System.out.println(name+"-after remove : "+threadLocal.get());},"t1").start();newThread(()->{Stringname=Thread.currentThread().getName();threadLocal.set("itheima");print(name);System.out.println(name+"-after remove : "+threadLocal.get());},"t2").start();}staticvoidprint(Stringstr){System.out.println(str+" :"+threadLocal.get());threadLocal.remove();}

常用方法就三个:

方法作用
set(value)设置当前线程对应的值
get()获取当前线程对应的值
remove()移除当前线程对应的值

值到底存在 ThreadLocal 里吗

这是关键点。

值不是存在ThreadLocal对象自己里面,而是存在当前线程对象的ThreadLocalMap里。

每个Thread内部都有一个ThreadLocalMap。这个 Map 的 key 是ThreadLocal对象,value 是你放进去的线程本地值。

作为 key

Thread-1

ThreadLocalMap

Entry

key: ThreadLocal 弱引用

value: 线程本地值 强引用

ThreadLocal 对象

所以threadLocal.set(value)的真实含义是:

ThreadLocal自己作为 key,把 value 放进当前线程的ThreadLocalMap

set 方法大致怎么走

简化一下源码:

publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){map.set(this,value);}else{createMap(t,value);}}

流程图如下:

存在

不存在

ThreadLocal.set(value)

获取当前线程 Thread

获取线程的 ThreadLocalMap

Map 是否存在

以当前 ThreadLocal 为 key 存 value

创建 ThreadLocalMap

get()也是类似逻辑:

  1. 获取当前线程。
  2. 找到当前线程的ThreadLocalMap
  3. 用当前ThreadLocal作为 key 查 Entry。
  4. 返回 Entry 里的 value。

为什么会内存泄漏

ThreadLocalMap.Entry的 key 是弱引用。

源码形态大概是:

staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}

弱引用的特点是:GC 发现后就可以回收它指向的对象。

问题来了:

如果ThreadLocal对象没有外部强引用了,GC 会把 key 回收掉。于是 Entry 变成:

key = null value = 业务对象

value 还是强引用,仍然被当前线程的ThreadLocalMap持有。

如果这个线程是线程池里的长期存活线程,那 value 也可能长期无法释放。这就是内存泄漏风险。

线程池工作线程长期存活

ThreadLocalMap 长期存在

Entry

key: ThreadLocal 弱引用

value: 业务对象 强引用

ThreadLocal 外部强引用消失

GC 回收 key

key 变成 null

value 仍被 Entry 强引用

内存泄漏风险

这里还有一个面试里常见的追问:

为什么 key 要设计成弱引用?

如果 key 是强引用,ThreadLocalMap会一直强引用 ThreadLocal。即使业务代码不再使用这个 ThreadLocal,它也无法被回收。弱引用至少能让 key 被回收,后续set/get/remove时,Map 有机会清理这些 stale entry。

但这不代表你可以不remove()。因为清理时机不一定马上发生。

为什么用完必须 remove

线程池场景下,线程会被复用。

如果请求 A 在 ThreadLocal 里放了用户 A 的信息,但没有清理,线程回到线程池后又被请求 B 复用。请求 B 可能读到请求 A 的上下文。

这已经不是内存泄漏,而是数据串了。

请求 BThreadLocalMap线程池线程请求 A请求 BThreadLocalMap线程池线程请求 A使用线程set 用户 A请求结束但未 remove复用同一线程get 读到旧值用户 A

正确写法是放在finally

try{USER_CONTEXT.set(user);// do business}finally{USER_CONTEXT.remove();}

只要是 Web 请求、线程池任务、异步任务里的 ThreadLocal,都应该养成这个习惯。

ThreadLocal 和 synchronized 的区别

这两个经常被放在一起问,但它们解决问题的方式完全不同。

对比点synchronizedThreadLocal
思路多线程共享同一份数据时,加锁排队访问每个线程各保存一份数据
解决的问题共享变量并发修改线程隔离、线程上下文
数据是否共享共享不共享
典型场景计数、库存、临界区用户上下文、连接对象、traceId

一句话:

synchronized 是让大家排队用同一份东西,ThreadLocal 是给每个线程各发一份东西。

面试怎么答

可以这样回答:

ThreadLocal用来实现线程隔离。它会让每个线程拥有自己独立的变量副本,避免多个线程争用同一个对象,也可以在线程内共享上下文,比如用户信息、traceId、数据库连接等。

它的值不是存储在ThreadLocal对象本身,而是存储在当前线程的ThreadLocalMap中。ThreadLocalMap的 key 是 ThreadLocal,value 是线程本地变量。

内存泄漏风险来自ThreadLocalMap.Entry的 key 是弱引用,value 是强引用。当 ThreadLocal 没有外部强引用时,key 可能被 GC 回收成 null,但 value 仍然被线程持有。如果线程是线程池里的长期存活线程,value 可能长期无法释放。

所以使用 ThreadLocal 后,尤其在线程池和 Web 请求场景中,一定要在finally里调用remove(),避免内存泄漏和上下文串数据。

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

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

立即咨询