相关问题

  1. ThreadLocal 是什么?它解决什么问题?
  2. ThreadLocal 和加锁有什么区别?
  3. ThreadLocal 的数据到底存在哪里?
  4. ThreadLocalMap 是什么?为什么每个线程都有自己的 ThreadLocalMap?
  5. ThreadLocal.set() 的完整流程是什么?
  6. ThreadLocal.get() 的完整流程是什么?
  7. ThreadLocalMap 为什么不是普通的 HashMap?
  8. ThreadLocalMap 发生哈希冲突时怎么处理?
  9. 为什么 ThreadLocalMap 的 key 是弱引用?
  10. 为什么 key 用弱引用以后,仍然可能发生内存泄漏?
  11. ThreadLocal 内存泄漏到底泄漏的是什么?
  12. 为什么使用完 ThreadLocal 通常要调用 remove()?
  13. 在线程池里使用 ThreadLocal 为什么风险更大?
  14. ThreadLocal 的 key 被 GC 以后,value 是怎么被清理的?
  15. ThreadLocalMap 什么时候会清理过期 Entry?
  16. ThreadLocal 能实现父子线程之间的数据传递吗?
  17. InheritableThreadLocal 是怎么实现的?
  18. 为什么在线程池里 InheritableThreadLocal 也可能失效?
  19. TransmittableThreadLocal 解决了什么问题?
  20. ThreadLocal 的典型使用场景有哪些?
  21. ThreadLocal 有哪些不适合使用的场景?
  22. ThreadLocal 是线程安全的吗?为什么?
  23. 一个线程可以有多个 ThreadLocal 吗?它们的数据怎么区分?
  24. 同一个 ThreadLocal 被多个线程使用时,为什么不会互相覆盖?
  25. 如果让你手写一个简化版 ThreadLocal,核心结构会怎么设计?

ThreadLocal 的作用

ThreadLocal 用来为每个线程保存一份独立的数据。

它本身不直接保存业务数据,而更像一个访问入口。调用 set() 或 get() 时,ThreadLocal 会先找到当前线程,再访问这个线程自己的 ThreadLocalMap。

每个线程都有自己独立的 ThreadLocalMap,其中:

1
2
key   = ThreadLocal 对象
value = 当前线程对应的数据

例如:

1
2
3
4
5
6
7
Thread A
└── ThreadLocalMap
    └── ThreadLocal1 → valueA

Thread B
└── ThreadLocalMap
    └── ThreadLocal1 → valueB

虽然两个线程使用的是同一个 ThreadLocal1,但它们访问的是各自线程内部不同的 Map,所以拿到的值也不同。

因此 ThreadLocal 的核心作用就是:

把数据绑定到当前线程,让不同线程之间的数据互相隔离。

常见操作主要有:

1
2
3
threadLocal.set(value);   // 保存当前线程的数据
threadLocal.get();        // 获取当前线程的数据
threadLocal.remove();     // 删除当前线程的数据

它比较适合保存当前线程相关的上下文信息,比如用户信息、请求上下文、事务上下文等。

ThreadLocalMap

ThreadLocalMap 是每个线程内部用来保存 ThreadLocal 数据的结构。

它不是普通的 HashMap,而是 ThreadLocal 自己实现的一套哈希表。

每个 Entry 主要保存两部分:

1
2
key   = ThreadLocal 对象
value = 当前线程对应的数据

例如:

1
2
3
4
5
Thread
└── ThreadLocalMap
    ├── ThreadLocal1 → value1
    ├── ThreadLocal2 → value2
    └── ThreadLocal3 → value3

当调用:

1
threadLocal.set(value);

实际上就是以当前这个 ThreadLocal 对象作为 key,把 value 放进当前线程自己的 ThreadLocalMap。

get() 时也是一样,用当前 ThreadLocal 作为 key 去查对应的 Entry。

ThreadLocalMap 底层使用数组保存 Entry,发生哈希冲突时不像 HashMap 那样使用链表或红黑树,而是采用开放寻址法中的线性探测。

也就是说,如果目标位置已经被占用,就继续往后找:

1
2
3
4
下标 3 被占用
→ 看下标 4
→ 还被占用
→ 看下标 5

直到找到合适的位置。

所以 ThreadLocalMap 可以理解成:

线程内部专门用来保存 ThreadLocal 数据的一张轻量级哈希表。

ThreadLocalMap

ThreadLocalMap 是每个线程内部用来保存 ThreadLocal 数据的结构。

它底层不是普通的 HashMap,而是自己维护了一套基于数组的哈希表。每个元素都是一个 Entry:

1
2
3
Entry
├── key   = ThreadLocal
└── value = 当前线程对应的数据

底层使用 Entry[] 数组保存这些数据。ThreadLocal 自身会有一个哈希值,通过:

1
hash & (table.length - 1)

计算出数组下标,因此不会因为哈希值很大就产生超大的稀疏数组。

ThreadLocal 的哈希值不是简单递增,而是每次增加一个固定增量,使多个 ThreadLocal 在长度为 2 的幂的数组中尽量均匀分布,减少冲突。

如果发生哈希冲突,ThreadLocalMap 不会像 HashMap 那样使用链表或红黑树,而是采用线性探测:

1
2
3
4
当前位置被占用
→ 看下一个位置
→ 还被占用
→ 继续向后找

当元素数量达到一定程度时,ThreadLocalMap 也会扩容。不过扩容之前会先清理已经失效的 Entry,如果清理之后空间已经足够,就可能不再扩容。

为什么 key 使用弱引用

ThreadLocalMap 中的 key 并不是对 ThreadLocal 的普通强引用,而是弱引用。

如果 key 使用强引用,那么即使业务代码已经不再使用某个 ThreadLocal,只要线程还活着,就会一直存在:

1
2
3
4
Thread
→ ThreadLocalMap
→ Entry
→ ThreadLocal

这样这个 ThreadLocal 对象就无法被 GC。

使用弱引用以后,当业务代码不再持有这个 ThreadLocal 时,GC 可以把它回收。对应的 Entry 就会变成:

1
2
key   = null
value = 原来的业务数据

这种 Entry 会被视为过期数据,ThreadLocalMap 在后续执行 get()、set()、remove() 等操作时,会尝试顺便清理。

不过弱引用并不能彻底避免内存泄漏,因为 value 仍然是强引用。

只要线程还活着,就可能继续存在:

1
2
3
4
Thread
→ ThreadLocalMap
→ Entry
→ value

所以使用完 ThreadLocal 后,仍然应该主动调用:

1
threadLocal.remove();

remove() 是正常的清理方式,而弱引用更像是一层兜底:即使开发者忘了 remove,至少 ThreadLocal 对象本身还能被 GC,并且 Map 后续还能识别出 key == null 的过期 Entry 并尝试清理。

InheritableThreadLocal 和 TransmittableThreadLocal

普通 ThreadLocal 的数据只属于当前线程,父线程里的值不会自动传给子线程。

InheritableThreadLocal 用来解决这个问题。创建子线程时,会把父线程中的相关数据复制一份给子线程,所以子线程可以读取到父线程创建它时的上下文。

但它在线程池里并不好用,因为线程池中的工作线程通常早就已经创建好了。后续提交任务时,并不会重新创建子线程,因此也不会重新复制父线程上下文。

TransmittableThreadLocal,通常简称 TTL,主要就是为了解决线程池、异步任务这类线程复用场景下的上下文传递问题。

它的核心思路是:在提交任务时捕获当前线程的上下文,在线程池真正执行任务时再把这些上下文恢复到工作线程中,执行结束后再清理或还原。

所以可以理解为:

1
2
3
4
5
6
7
8
ThreadLocal
→ 当前线程自己使用

InheritableThreadLocal
→ 创建子线程时传递上下文

TransmittableThreadLocal
→ 在线程池 / 异步任务中传递上下文

实际项目中,TTL 常用于链路追踪、日志 TraceId、用户上下文等需要跨线程传递的信息。