Skip to content

ThreadLocal介绍

1. ThreadLocal 是什么

ThreadLocal 可以理解为:

  • 给每个线程单独准备一份变量副本

它解决的问题不是“多个线程共享同一个变量如何加锁”,而是:

  • 多个线程根本不要共享,各用各的

所以它的核心思想是:

  • 把“共享变量 + 同步访问”
  • 变成“线程私有变量 + 无需竞争”

典型使用场景:

  • 保存当前线程的用户上下文
  • 保存事务上下文
  • 保存请求 trace 信息
  • 保存日期格式化器、数据库连接等线程内复用对象

2. ThreadLocal 和普通变量、共享变量有什么区别

2.1 普通成员变量

如果一个对象被多个线程共享,那么它的成员变量通常也是共享的。

这时多个线程同时读写,可能就会有:

  • 可见性问题
  • 竞态条件
  • 线程安全问题

2.2 ThreadLocal 变量

ThreadLocal<T> 表面上像一个变量容器,但它本身并不真正存值

它更像一个“key”或“索引入口”,真正的值存放在:

  • 当前线程对象内部维护的 ThreadLocalMap

所以同一个 ThreadLocal

  • 在线程 A 里取到的是 A 自己的值
  • 在线程 B 里取到的是 B 自己的值

两边互不影响。

3. ThreadLocal 最典型的使用方式

public class UserContextHolder {

    private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();

    public static void set(Long userId) {
        USER_ID.set(userId);
    }

    public static Long get() {
        return USER_ID.get();
    }

    public static void clear() {
        USER_ID.remove();
    }
}

典型调用方式:

try {
    UserContextHolder.set(1001L);
    // 当前线程后续调用链里都可以直接拿到 userId
    Long userId = UserContextHolder.get();
} finally {
    UserContextHolder.clear();
}

这个模式在 Web 项目里非常常见。

4. ThreadLocal 的底层原理

4.1 一个最重要的结论

很多人以为:

  • ThreadLocal 自己内部维护了一份 “线程 -> 值” 的 Map

这不准确。

更准确的说法是:

  • 每个 Thread 内部维护了一个 ThreadLocalMap
  • ThreadLocal 自己只是作为这个 Map 的 key

也就是:

  • 不是 ThreadLocal 持有线程的数据
  • 而是 Thread 持有属于自己的 ThreadLocal 数据

4.2 数据存在哪里

在 JDK 里,Thread 对象内部大致有两个字段:

  • threadLocals
  • inheritableThreadLocals

其中:

  • threadLocals 用来存普通 ThreadLocal
  • inheritableThreadLocals 用来存 InheritableThreadLocal

所以 ThreadLocal 的数据最终是挂在:

  • Thread 对象

上的。

4.3 set/get 的本质过程

可以把它理解成下面这套逻辑。

4.3.1 set(value)

当你执行:

threadLocal.set(value);

大致会发生:

  1. 先拿到当前线程 Thread.currentThread()
  2. 找这个线程内部的 threadLocals
  3. 如果没有,就创建一个 ThreadLocalMap
  4. 以当前 threadLocal 作为 key,把 value 放进去

4.3.2 get()

当你执行:

threadLocal.get();

大致会发生:

  1. 拿到当前线程
  2. 找到线程内部的 ThreadLocalMap
  3. 用当前 threadLocal 作为 key 查找
  4. 找到就返回对应 value
  5. 找不到则返回 null,或者触发 initialValue()

4.4 一个简化心智模型

可以用这段伪代码理解:

class Thread {
    ThreadLocalMap threadLocals;
}

class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        t.threadLocals.put(this, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        return (T) t.threadLocals.get(this);
    }
}

这不是源码原样,但足够帮助建立正确认知。

5. ThreadLocalMap 是什么

5.1 它不是 java.util.HashMap

ThreadLocalMapThreadLocal 的一个静态内部类,不是通用 Map。

它是专门为 ThreadLocal 设计的轻量结构。

它有几个关键特点:

  • 键是 ThreadLocal
  • 使用数组存储 Entry
  • 采用开放地址法处理冲突
  • 不是链表法

5.2 Entry 的 key 为什么是弱引用

ThreadLocalMap.Entry 大致可以理解为:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
}

也就是说:

  • key 是 WeakReference<ThreadLocal<?>>
  • value 是普通强引用对象

6. 为什么 key 要设计成弱引用

6.1 先看如果不用弱引用会怎样

假设 key 是强引用:

  • 外部代码已经不再持有某个 ThreadLocal 对象
  • ThreadLocalMap 里还强引用着它

那么这个 ThreadLocal 永远不会被回收。

这会导致:

  • key 泄漏

6.2 弱引用的作用

把 key 设计成弱引用后:

  • 外部如果没有强引用再指向这个 ThreadLocal
  • GC 时,这个 key 就可以被回收

这样至少不会出现:

  • ThreadLocal 对象本身永远活着

所以弱引用设计的目标是:

  • 降低 key 无法回收的风险

但注意,这并不等于“彻底避免内存泄漏”。

6.3 为什么 value 不能也设计成弱引用

key 能用弱引用的原因

// 典型使用方式
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
                 ↑
            这里是强引用!

ThreadLocal 对象本身通常由: - 静态字段持有 - 成员变量持有
- 业务代码的本地引用持有

所以:

  • 即使 ThreadLocalMap 中 key 是弱引用
  • ThreadLocal 对象依然被其他强引用保活
  • 弱引用只是在特殊场景(ThreadLocal 本身生命周期有限)才有用

value 必须是强引用的原因

threadLocal.set(someObject);  // someObject 存进去了

// 此后业务代码通常不会反复持有这个对象的强引用
// 只能通过 threadLocal.get() 来访问

// ❌ 如果 value 是弱引用
byte[] largeData = new byte[1024 * 1024];
holder.set(largeData);  
// largeData 这个局部变量的作用域结束了
// 没有其他强引用指向它
// GC 可能立刻回收它,即使它仍在被使用中!

byte[] retrieved = holder.get();  // 可能得到 null!业务崩溃


// ✅ value 是强引用
byte[] largeData = new byte[1024 * 1024];
holder.set(largeData);  
// 即使 largeData 局部变量消失
// ThreadLocalMap 里的强引用保证它活着

byte[] retrieved = holder.get();  // 一定能取到值 ✓

对比

对象 通常被谁持有
强引用
是否需要
强引用
理由
key
(ThreadLocal)
静态字段、成员变量等
业务代码本身
✓ 已经有了 ThreadLocalMap 不需要额外保护
value
(线程私有值)
无(业务代码
通常不持有)
✓ 必须有 没有其他地方保护,
否则立刻被 GC 回收

本质区别:key 的生命周期已被其他强引用保证,value 的生命周期只能依赖 ThreadLocalMap 的强引用。

7. 为什么 ThreadLocal 仍然可能内存泄漏

7.1 泄漏根因不是弱引用本身,而是 value 还在

最经典的问题是:

  • key 被 GC 回收后,变成了 null
  • 但 value 仍然被 ThreadLocalMap.Entry 强引用着

也就是说,Entry 可能变成:

  • key = null
  • value = 某个大对象

只要这个线程对象还活着,这个 value 就还可能活着。

这就形成了所谓的:

  • ThreadLocal value 泄漏

7.2 为什么在线程池里特别危险

如果线程是短生命周期线程:

  • 线程结束后,整个 Thread 对象会被回收
  • 连带 ThreadLocalMap 一起回收

问题通常不大。

但在线程池里:

  • 线程会长期复用
  • Thread 对象长期存活
  • ThreadLocalMap 也长期存活

如果你忘了 remove()

  • 旧任务留下的 value 可能一直挂在线程上
  • 下一次任务还可能读到脏数据
  • 大对象还可能一直无法释放

所以 ThreadLocal 真正危险的地方通常不是“临时线程”,而是:

  • 线程池 + 忘记清理

8. 为什么要强调 remove()

8.1 set(null) 不等于 remove()

很多人会写:

threadLocal.set(null);

这并不完全等价于:

threadLocal.remove();

区别在于:

  • set(null) 只是把 value 设成 null
  • remove() 是把整个 Entry 从 ThreadLocalMap 中清理掉

所以更推荐:

  • 用完就 remove()

8.2 最推荐的写法

try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove();
}

这几乎是 ThreadLocal 使用的标准姿势。

9. ThreadLocalMap 如何处理哈希冲突

9.1 不是链表法,而是开放地址法

ThreadLocalMap 底层是数组,不像 HashMap 那样用链表或红黑树。

发生冲突时,它会:

  • 沿数组继续向后找空位

这属于:

  • 开放地址法

9.2 为什么这么设计

因为 ThreadLocalMap 的使用场景很特殊:

  • 每个线程自己的 map 一般不会特别大
  • 不需要像通用 Map 那样支持复杂高并发
  • 更追求结构简单和访问局部性

9.3 哈希值从哪来

每个 ThreadLocal 会有一个自己的 threadLocalHashCode

这个值不是直接用对象地址,而是按一种特殊增量策略生成,目的是:

  • 尽量让连续创建的 ThreadLocal 在数组里分布更均匀

这能减少冲突概率。

10. 过期 Entry 是怎么清理的

10.1 ThreadLocalMap 不会像后台线程那样主动全量扫描

它的清理更偏“惰性”。

通常会在这些操作中顺便清理:

  • set
  • get
  • remove
  • 扩容或 rehash

也就是说:

  • 发现有 key 已经变成 null
  • 就会尝试把这类 stale entry 清掉

10.2 为什么这意味着不能完全依赖弱引用

因为:

  • key 变成 null
  • 不代表 value 会立刻释放

只有后续某些 map 操作触发清理逻辑时,value 才可能真正断开引用。

所以最佳实践依然是:

  • 不要指望 GC 帮你兜底
  • 自己主动 remove()

11. initialValue()withInitial() 是什么

11.1 initialValue()

你可以继承 ThreadLocal 并重写:

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

这样第一次 get() 时,如果当前线程还没放过值,就会返回初始值。

11.2 withInitial()

JDK 8 以后更常见的是:

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

这个写法更简洁。

12. InheritableThreadLocal 是什么

12.1 它解决什么问题

普通 ThreadLocal 的值:

  • 只属于当前线程
  • 子线程默认拿不到

InheritableThreadLocal 可以让:

  • 子线程创建时继承父线程里的值

12.2 它的局限在哪里

它只对:

  • 新创建的子线程

生效。

在线程池场景下通常不可靠,因为线程池线程往往不是“现创建”的,而是已经存在并被复用的。

所以很多人误以为:

  • InheritableThreadLocal 能自动在线程池里传递上下文

这是错误的。

12.3 为什么很多框架还要搞 TransmittableThreadLocal

因为:

  • 普通 ThreadLocal 不跨线程
  • InheritableThreadLocal 不适合线程池复用场景

于是像阿里开源的 TransmittableThreadLocal(TTL)这类方案,才会去解决:

  • 线程池任务提交时的上下文传递问题

13. ThreadLocal 的典型应用场景

13.1 保存用户上下文

例如:

  • 当前登录用户 id
  • 租户 id
  • trace id

在一次请求的调用链里可以直接取。

13.2 Spring 事务上下文

Spring 事务很多上下文就是绑定在当前线程上的。

所以你会看到:

  • 同线程里事务有效
  • 跨线程后事务上下文丢失

13.3 数据库连接或 Session 绑定

有些框架会把:

  • 数据库连接
  • ORM Session

绑定到当前线程上下文。

13.4 避免非线程安全对象共享

比如早期常见思路是:

  • 每个线程保存一个自己的 SimpleDateFormat

避免多个线程共享同一个非线程安全对象。

不过现代 Java 时间 API 更推荐直接使用线程安全的:

  • DateTimeFormatter

14. ThreadLocal 的典型坑和注意事项

14.1 线程池里必须手动清理

这是最重要的一条。

如果在线程池任务中使用 ThreadLocal,必须在任务结束时清理:

executor.execute(() -> {
    try {
        threadLocal.set("req-123");
        // do work
    } finally {
        threadLocal.remove();
    }
});

否则就容易:

  • 上下文串数据
  • 内存泄漏
  • 脏数据污染后续任务

14.2 不要滥用保存大对象

如果你往 ThreadLocal 里放:

  • 大数组
  • 大缓存对象
  • 大型业务上下文

那一旦忘记清理,影响会非常大。

14.3 不要把 ThreadLocal 当“全局变量替代品”

ThreadLocal 的目标是:

  • 线程隔离

不是:

  • 让所有地方都能随便拿一个隐式变量

过度滥用会导致:

  • 调用链隐式依赖过多
  • 调试困难
  • 测试困难
  • 代码可维护性下降

14.4 异步和跨线程时会失效

ThreadLocal 的数据只属于当前线程。

所以一旦:

  • @Async
  • 线程池切换
  • 手动新开线程

就不能默认指望原线程里的值还能拿到。

14.5 不是用来解决共享变量可见性问题的

ThreadLocal 和 volatile、锁不是一类工具。

  • volatile 解决可见性
  • 锁解决共享竞争
  • ThreadLocal 解决线程隔离

不要混淆。

15. ThreadLocal 的核心特征

维度 ThreadLocal
解决的问题 每线程独立副本
数据最终存放位置 Thread 对象内部的 ThreadLocalMap
key 是什么 ThreadLocal 对象
value 是什么 当前线程自己的变量值
key 引用类型 弱引用
value 引用类型 强引用
是否可能内存泄漏 会,尤其在线程池中
最重要实践 try/finally + remove()
是否自动跨线程传递

16. 面试回答“ThreadLocal 底层原理”

可以直接这样答:

ThreadLocal 的核心思想不是多个线程共享同一个变量,而是让每个线程各自持有一份独立副本。它底层不是 ThreadLocal 自己存值,而是每个 Thread 对象内部维护一个 ThreadLocalMap,ThreadLocal 只是这个 map 的 key。调用 set() 时,本质上是把当前 ThreadLocal 对应的值放到当前线程的 ThreadLocalMap 里;调用 get() 时,再从当前线程自己的 map 里取。
ThreadLocalMap 的 Entry 里,key 是对 ThreadLocal 的弱引用,value 是强引用。这样设计可以避免 key 本身长期无法回收,但如果业务代码忘了 remove(),在线程池这类长生命周期线程里,value 仍然可能因为挂在线程对象上而无法释放,造成内存泄漏或上下文串数据。
所以 ThreadLocal 最重要的注意事项就是:在线程池和请求链场景里,用完必须 try/finallyremove()

17. 常见误区纠正

  • “ThreadLocal 里的值存在线程栈里”

    不对。它通常存在线程对象持有的 ThreadLocalMap 结构中,不是直接放在 Java 虚拟机意义上的栈帧局部变量区。

  • “key 是弱引用,所以一定不会泄漏”

    不对。key 弱引用只能降低 key 无法回收的风险,但 value 仍然可能因为强引用而滞留。

  • set(null)remove() 一样”

    不对。remove() 才是更彻底的清理方式。

  • “InheritableThreadLocal 可以完美解决线程池上下文传递”

    不对。线程池复用线程时,这个假设通常不成立。