在分析ThreadLocal源码之前,我们先从概念入手,由浅入深。
一、谈谈对ThreadLocal的理解以及它与synchronized的区别
一句话总结: ThreadLocal 提供线程局部变量,通过线程隔离机制,确保每个线程拥有变量的独立副本,实现了“以空间换时间”的线程安全。
与 synchronized 的区别:
synchronized:以时间换空间。用于共享数据的同步,通过锁机制让线程排队访问。
ThreadLocal:以空间换时间。用于数据隔离,每个线程独享一份数据,无需加锁。
二、 底层源码与数据结构 (面试高频)
误区提醒:很多人误以为 ThreadLocal 内部维护了一个 Map 来存数据,这是错的。
1. 真实的引用关系
- 谁持有谁? 数据实际上是存储在 Thread 线程对象 内部的。
- 成员变量:每个 Thread 对象内部都有一个 ThreadLocalMap 类型的成员变量 (threadLocals)。
- Key 和 Value:
- Map容器:ThreadLocalMap(它是 ThreadLocal 的静态内部类)。
- Key:ThreadLocal 对象本身(确切地说是 this)。
- Value:我们要存储的对象。
❓ 高频考点:为什么要设计成“Thread持有Map”,而不是“ThreadLocal持有Map”?
- 生命周期绑定:如果 Map 在 ThreadLocal 中,当线程销毁时,Map 难以自动回收(因为 ThreadLocal 可能还存在)。
- 由 Thread 持有:当线程销毁时,其内部的 threadLocals 也会随之销毁,自动减少内存占用。
2. ThreadLocalMap 的实现细节
- 数据结构:它没有实现 java.util.Map 接口,而是一个定制的哈希表。它内部维护了一个 Entry 数组。
- Hash 冲突解决:
- HashMap:使用的是 链地址法 (数组+链表/红黑树)。
- ThreadLocalMap:使用的是 开放寻址法(线性探测)。
- 原理:如果计算出的位置有数据了,就向后找下一个空位,直到找到为止。
- 优点:适合数据量较小的情况。
- 魔数 0x61c88647:
- 源码中使用了 Fibonacci Hashing,每次 hash 递增这个魔数。
- 作用:能让哈希码在 2^n 大小的数组中分布非常均匀,减少冲突。
三、 内存泄漏问题 (核心痛点)
这是面试中关于 ThreadLocal 最重要 的考点。
1. 根本原因:弱引用 (WeakReference)
ThreadLocalMap 的 Entry 继承自 WeakReference。
- Key (ThreadLocal):使用 弱引用 指向。
- Value (Object):使用 强引用 指向。
2. 泄漏流程
- 业务代码执行完毕,外部对 ThreadLocal 对象的强引用断开。
- GC 发生:由于 Key 是弱引用,ThreadLocal 对象会被回收。
- 结果:Map 中的 Entry 变成了 Key = null,但 Value = Object (强引用) 依然存在。
- 致命点:如果线程是线程池中的核心线程(生命周期很长):
- 这个 Value 对象将永远无法被访问(Key丢了)。
- 但也无法被回收(引用链:Thread -> ThreadLocalMap -> Entry -> Value)。
- 后果:日积月累,导致 OOM (内存溢出)。
3. 官方的补救措施 (探测式清理)
ThreadLocal 在调用 set()、get()、remove() 方法时,会尝试遍历并清理 Key 为 null 的 Entry(将其 Value 置为 null,断开强引用)。
- 局限性:这是一种“惰性”清理。如果你不调用这些方法,或者线程长时间不结束,泄漏依然存在。
4. 最佳实践 (标准答案)
必须在使用完 ThreadLocal 后,显式调用 remove() 方法。通常配合 try-finally 代码块使用。- try {
- threadLocal.set(value);
- // 业务逻辑
- } finally {
- threadLocal.remove(); // 防止内存泄漏
- }
复制代码 四、 父子线程传递 (InheritableThreadLocal)
- 场景:父线程设置了值,希望子线程能读取到(如 TraceId 传递)。
- 类:InheritableThreadLocal。
- 原理:在创建子线程(new Thread())时,子线程会深拷贝父线程的 inheritableThreadLocals Map。
- 缺陷:在使用 线程池 时失效。
- 因为线程池中的线程是复用的,不是每次都重新创建,所以无法同步父线程最新的值。
- 解决方案:使用阿里开源的 TTL (TransmittableThreadLocal)。
- 它通过装饰器模式修饰线程池,在任务提交时抓取当前上下文,任务执行时回放上下文。
五、 典型应用场景
- 数据库连接/Session管理:
- 如 Hibernate 的 Session,MyBatis 的 SqlSession,Spring 的事务管理(DataSourceTransactionManager)。
- 利用 ThreadLocal 保证同一个线程(同一个事务)获取到的是同一个数据库连接。
- 解决线程不安全工具类的并发问题:
- SimpleDateFormat:它是线程不安全的。
- 可以通过 ThreadLocal 给每个线程创建一个单独的 SimpleDateFormat 实例,避免每次 new 的开销,又避免了并发冲突。
- 全链路追踪/上下文传递:
- 在微服务或 Web 框架中,使用 ThreadLocal 存储 RequestId、CurrentUser 等信息,避免在方法参数中层层传递。
六、 总结
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |