ThreadLocal原理最全详解(看这篇就够了)

ThreadLocal在大厂经常被涉及和考察,下面我详解ThreadLocal原理@mikechen

一、ThreadLocal核心原理

ThreadLocal的核心作用是:让每个线程都拥有自己独立的一份变量副本。

ThreadLocal原理最全详解(看这篇就够了)-mikechen

例如:

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("mikechen");

表面上看,是ThreadLocal保存了数据。

实际上并不是。

真正保存数据的是当前线程对象内部的 ThreadLocalMap

可以简单理解为:

Thread
  │
  └── ThreadLocalMap
        ├── ThreadLocal → Value
        ├── ThreadLocal → Value
        └── ThreadLocal → Value

因此,不同线程调用同一个ThreadLocal时,最终访问的是自己线程内部的Map,所以不会互相干扰。

这也是ThreadLocal实现线程隔离的核心。


二、ThreadLocal到底是怎么工作的?

ThreadLocal原理最全详解(看这篇就够了)-mikechen

ThreadLocal最核心的三个方法就是:

set()
get()
remove()

执行set()时,ThreadLocal会获取当前线程,然后找到线程内部的ThreadLocalMap,再以当前ThreadLocal对象作为Key保存Value。

执行get()时,也是先获取当前线程,然后从它自己的ThreadLocalMap中查找对应Value。

remove()则会主动删除当前ThreadLocal对应的数据。

ThreadLocalMap中的Entry比较特殊:

Entry
 ├── Key:ThreadLocal弱引用
 └── Value:真正保存的数据

这里有一个非常关键的设计:Key是弱引用,而Value是强引用。

为什么这么设计?

因为ThreadLocal对象本身如果已经没有其他地方引用,那么GC时可以回收ThreadLocal对象,避免ThreadLocal对象本身长期存在。

但问题也由此产生。


三、为什么ThreadLocal会导致内存泄漏?

ThreadLocal原理最全详解(看这篇就够了)-mikechen

假设代码如下:

ThreadLocal<User> threadLocal = new ThreadLocal<>();
threadLocal.set(new User());

当方法执行结束以后,如果外部已经没有任何地方引用这个ThreadLocal,那么GC可能回收ThreadLocal对象。

此时ThreadLocalMap可能变成:

ThreadLocalMap

Entry
Key   → null
Value → User对象

也就是所谓的脏Entry

问题在于:Key已经没有了,但是Value依然被ThreadLocalMap强引用。

如果当前线程是普通短生命周期线程,那么线程结束后,整个ThreadLocalMap都会被回收,问题通常不大。

但如果是Tomcat、Netty等服务器中的线程池线程,线程可能存活很长时间。

于是就可能出现:

线程长期存在
    ↓
ThreadLocalMap长期存在
    ↓
Value长期存在
    ↓
大量对象无法及时回收
    ↓
内存占用不断增加

这就是ThreadLocal内存泄漏问题的核心。

需要注意:ThreadLocal并不是“必然导致内存泄漏”

JDK中的ThreadLocalMap在执行set()get()等操作时,会尝试清理部分失效Entry。但这种清理并不能保证所有Value立即被清理。

所以最可靠的方式仍然是主动调用remove()


四、大厂生产环境如何正确使用?

ThreadLocal原理最全详解(看这篇就够了)-mikechen

最推荐的方式就是:

try {
    threadLocal.set(user);

    // 业务代码
} finally {
    threadLocal.remove();
}

为什么一定要放在finally

因为无论业务代码正常执行还是抛出异常,都可以确保ThreadLocal最终被清理。

尤其在线程池环境中,这是非常重要的工程实践。

ThreadLocal比较适合保存:

  • 用户上下文
  • TraceId
  • Request上下文
  • 数据库连接相关上下文
  • 当前线程的一些临时状态

但不建议在ThreadLocal中保存超大的对象,更不能把ThreadLocal当成全局缓存使用。

从架构师角度来看,ThreadLocal真正的价值并不是“存变量”,而是在共享线程执行环境中实现线程级数据隔离

记住一句话:

ThreadLocal解决的是线程隔离问题,而remove()解决的是线程生命周期问题。

陈睿|mikechen

10年+大厂架构经验,资深技术专家,就职于阿里等一线大厂。

评论交流
    说说你的看法