相关问题
ThreadLocal是什么?它解决什么问题?ThreadLocal和加锁有什么区别?ThreadLocal的数据到底存在哪里?ThreadLocalMap是什么?为什么每个线程都有自己的ThreadLocalMap?ThreadLocal.set()的完整流程是什么?ThreadLocal.get()的完整流程是什么?ThreadLocalMap为什么不是普通的HashMap?ThreadLocalMap发生哈希冲突时怎么处理?- 为什么
ThreadLocalMap的 key 是弱引用? - 为什么 key 用弱引用以后,仍然可能发生内存泄漏?
ThreadLocal内存泄漏到底泄漏的是什么?- 为什么使用完
ThreadLocal通常要调用remove()? - 在线程池里使用
ThreadLocal为什么风险更大? ThreadLocal的 key 被 GC 以后,value 是怎么被清理的?ThreadLocalMap什么时候会清理过期 Entry?ThreadLocal能实现父子线程之间的数据传递吗?InheritableThreadLocal是怎么实现的?- 为什么在线程池里
InheritableThreadLocal也可能失效? TransmittableThreadLocal解决了什么问题?ThreadLocal的典型使用场景有哪些?ThreadLocal有哪些不适合使用的场景?ThreadLocal是线程安全的吗?为什么?- 一个线程可以有多个
ThreadLocal吗?它们的数据怎么区分? - 同一个
ThreadLocal被多个线程使用时,为什么不会互相覆盖? - 如果让你手写一个简化版
ThreadLocal,核心结构会怎么设计?
ThreadLocal 的作用
ThreadLocal 用来为每个线程保存一份独立的数据。
它本身不直接保存业务数据,而更像一个访问入口。调用 set() 或 get() 时,ThreadLocal 会先找到当前线程,再访问这个线程自己的 ThreadLocalMap。
每个线程都有自己独立的 ThreadLocalMap,其中:
|
|
例如:
|
|
虽然两个线程使用的是同一个 ThreadLocal1,但它们访问的是各自线程内部不同的 Map,所以拿到的值也不同。
因此 ThreadLocal 的核心作用就是:
把数据绑定到当前线程,让不同线程之间的数据互相隔离。
常见操作主要有:
|
|
它比较适合保存当前线程相关的上下文信息,比如用户信息、请求上下文、事务上下文等。
ThreadLocalMap
ThreadLocalMap 是每个线程内部用来保存 ThreadLocal 数据的结构。
它不是普通的 HashMap,而是 ThreadLocal 自己实现的一套哈希表。
每个 Entry 主要保存两部分:
|
|
例如:
|
|
当调用:
|
|
实际上就是以当前这个 ThreadLocal 对象作为 key,把 value 放进当前线程自己的 ThreadLocalMap。
get() 时也是一样,用当前 ThreadLocal 作为 key 去查对应的 Entry。
ThreadLocalMap 底层使用数组保存 Entry,发生哈希冲突时不像 HashMap 那样使用链表或红黑树,而是采用开放寻址法中的线性探测。
也就是说,如果目标位置已经被占用,就继续往后找:
|
|
直到找到合适的位置。
所以 ThreadLocalMap 可以理解成:
线程内部专门用来保存 ThreadLocal 数据的一张轻量级哈希表。
ThreadLocalMap
ThreadLocalMap 是每个线程内部用来保存 ThreadLocal 数据的结构。
它底层不是普通的 HashMap,而是自己维护了一套基于数组的哈希表。每个元素都是一个 Entry:
|
|
底层使用 Entry[] 数组保存这些数据。ThreadLocal 自身会有一个哈希值,通过:
|
|
计算出数组下标,因此不会因为哈希值很大就产生超大的稀疏数组。
ThreadLocal 的哈希值不是简单递增,而是每次增加一个固定增量,使多个 ThreadLocal 在长度为 2 的幂的数组中尽量均匀分布,减少冲突。
如果发生哈希冲突,ThreadLocalMap 不会像 HashMap 那样使用链表或红黑树,而是采用线性探测:
|
|
当元素数量达到一定程度时,ThreadLocalMap 也会扩容。不过扩容之前会先清理已经失效的 Entry,如果清理之后空间已经足够,就可能不再扩容。
为什么 key 使用弱引用
ThreadLocalMap 中的 key 并不是对 ThreadLocal 的普通强引用,而是弱引用。
如果 key 使用强引用,那么即使业务代码已经不再使用某个 ThreadLocal,只要线程还活着,就会一直存在:
|
|
这样这个 ThreadLocal 对象就无法被 GC。
使用弱引用以后,当业务代码不再持有这个 ThreadLocal 时,GC 可以把它回收。对应的 Entry 就会变成:
|
|
这种 Entry 会被视为过期数据,ThreadLocalMap 在后续执行 get()、set()、remove() 等操作时,会尝试顺便清理。
不过弱引用并不能彻底避免内存泄漏,因为 value 仍然是强引用。
只要线程还活着,就可能继续存在:
|
|
所以使用完 ThreadLocal 后,仍然应该主动调用:
|
|
remove() 是正常的清理方式,而弱引用更像是一层兜底:即使开发者忘了 remove,至少 ThreadLocal 对象本身还能被 GC,并且 Map 后续还能识别出 key == null 的过期 Entry 并尝试清理。
InheritableThreadLocal 和 TransmittableThreadLocal
普通 ThreadLocal 的数据只属于当前线程,父线程里的值不会自动传给子线程。
InheritableThreadLocal 用来解决这个问题。创建子线程时,会把父线程中的相关数据复制一份给子线程,所以子线程可以读取到父线程创建它时的上下文。
但它在线程池里并不好用,因为线程池中的工作线程通常早就已经创建好了。后续提交任务时,并不会重新创建子线程,因此也不会重新复制父线程上下文。
TransmittableThreadLocal,通常简称 TTL,主要就是为了解决线程池、异步任务这类线程复用场景下的上下文传递问题。
它的核心思路是:在提交任务时捕获当前线程的上下文,在线程池真正执行任务时再把这些上下文恢复到工作线程中,执行结束后再清理或还原。
所以可以理解为:
|
|
实际项目中,TTL 常用于链路追踪、日志 TraceId、用户上下文等需要跨线程传递的信息。