浅聊一下ThreadLocal
大家好,今天咱们来好好聊聊 ThreadLocal—— 这个在多线程开发中超实用,但也容易踩坑的组件。不管是日常开发里的线程数据隔离,还是面试中常被问到的原理和内存泄露问题,本篇文章都会给你讲清楚。
一、ThreadLocal 是什么?一句话说透
ThreadLocal 翻译过来叫 “线程局部变量”,核心作用就两个:
- 线程内共享数据:同一个线程里,不同类、不同方法之间可以轻松拿取数据,不用靠参数来回传;
- 线程间隔离数据:每个线程存的数据只属于自己,其他线程看不到也改不了,天然避免多线程并发安全问题。
它底层是靠 Thread 类里的 ThreadLocalMap
来存数据的 —— 简单理解就是,每个线程都有自己专属的 “小账本”(ThreadLocalMap),账本里记着该线程通过 ThreadLocal 存的各种数据。
二、ThreadLocalMap 内部结构:到底怎么存数据?
先明确一点:ThreadLocal 本身不存数据,真正存数据的是线程自己的 ThreadLocalMap
。
1. 核心结构
ThreadLocalMap 底层是一个 Entry
类型的数组,每个 Entry 就是一个键值对:
- Key:ThreadLocal 对象(注意是弱引用,后面讲内存泄露会用到);
- Value:我们实际要存的数据(比如用户信息、SqlSession 等)。
关系可以这么理解:
Thread
→ 有一个 ThreadLocalMap
→ ThreadLocalMap
里是 Entry[]
数组 → 每个 Entry
对应 “ThreadLocal 对象 + 数据”
三、ThreadLocal 常用方法:3 个核心操作
ThreadLocal 的 API 特别简单,就 3 个常用方法,记熟就行:
1. 存数据:void set (T value)
把数据存到当前线程的 ThreadLocalMap 里。
比如:threadLocal.set(userInfo);
→ 现在当前线程的 “小账本” 里,就有了这条以该 ThreadLocal 为 Key、userInfo 为 Value 的记录。
2. 取数据:T get ()
从当前线程的 ThreadLocalMap 里拿数据。
比如:UserInfo user = threadLocal.get();
→ 自动根据当前 ThreadLocal 对象,找到对应的 Value 并返回。
3. 删数据:void remove ()
这个方法尤其重要! 从当前线程的 ThreadLocalMap 里删除数据。
重点提醒:如果用了线程池(线程会复用),一定要在业务结束后(比如 finally 里)调用 remove()
!不然线程放回线程池时,旧数据还在 “小账本” 里,下次其他任务用这个线程时,可能拿到脏数据。
四、常见问题:面试常问的 4 个点
1. 为什么用 ThreadLocal 做 Key?不能用 Thread 吗?
假设一个线程里只用到 1 个 ThreadLocal,用 Thread 当 Key 好像也行(毕竟一个线程对应一个数据)。但实际开发中,一个线程可能会用多个 ThreadLocal(比如同时存用户信息、请求上下文、SqlSession)。
如果用 Thread 当 Key,多个数据就会 “打架”—— 根本分不清哪个数据对应哪个用途。而用 ThreadLocal 当 Key,每个 ThreadLocal 对应一个数据,精准匹配,不会乱。
2. ThreadLocalMap 怎么找数据?(哈希计算逻辑)
当调用 get ()/set () 时,ThreadLocalMap 会通过 哈希计算 找到 Entry 在数组中的位置,步骤很简单:
- 拿当前 ThreadLocal 对象的
hashCode
; - 用
hashCode & (数组长度 - 1)
计算下标(这个操作和 “取余” 效果一样,但效率更高); - 根据下标找到对应的 Entry,进而拿到 Value。
举个例子:数组长度是 16,ThreadLocal 的 hashCode 是 31,那么 31 & 15 = 15
→ 就去数组下标 15 的位置找数据。
3. 父子线程怎么共享数据?ThreadLocal 不行!
ThreadLocal 的数据是线程隔离的,父线程存的数据,子线程拿不到 —— 因为父子线程是两个不同的 Thread 对象,各有各的 ThreadLocalMap。
这种场景要换 InheritableThreadLocal
,它是 JDK 自带的,继承了 ThreadLocal。原理是:子线程创建时,会把父线程 InheritableThreadLocal 里的数据 “拷贝” 一份到自己的 ThreadLocalMap 里,这样父子线程就能共享数据了。
4. ThreadLocal 怎么避免内存泄露?
先搞清楚为什么会内存泄露:Entry 的 Key 是 ThreadLocal 弱引用,当 ThreadLocal 对象没人用了(比如被 GC 回收),Key 就变成 null 了。但 Value 还是强引用,只要线程没销毁,Value 就一直占着内存,这就是内存泄露。
解决办法就一个:用完 ThreadLocal 后,一定要调用 remove () 方法!
remove()
会把 Entry 的 Key 和 Value 都设为 null,这样 GC 就能及时回收这部分内存,从根源上避免泄露。
推荐写法(在 finally 里调用,即使业务抛异常也能清理):
public void doSomething(UserDto userDto) {UserInfo userInfo = convert(userDto);try {// 存数据CurrentUser.set(userInfo);// 业务逻辑UserInfo current = CurrentUser.get();// ...} finally {// 无论如何都要删CurrentUser.remove();}
}
五、ThreadLocal 应用场景:实际开发用在哪?
1. 线程数据隔离:避免并发安全问题
最典型的场景是 SqlSession 绑定。MyBatis 里,每个线程的 SqlSession 是独立的,用 ThreadLocal 存起来,避免多个线程共用一个 SqlSession 导致的关闭异常或数据混乱:
private static final ThreadLocal<SqlSession> threadSession = new ThreadLocal<>();public static SqlSession getSession() {SqlSession s = threadSession.get();if (s == null) {s = getSqlSessionFactory().openSession();threadSession.set(s); // 绑定到当前线程}return s;
}
2. 跨函数传递数据:减少代码耦合
比如在 SpringMVC 里,我们经常需要获取 HttpServletRequest 对象。如果每个方法都靠参数传,太麻烦了。Spring 用 RequestContextHolder
帮我们搞定,底层就是 ThreadLocal:
// 直接获取 Request,不用传参数
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
看 RequestContextHolder
源码就知道,它内部用了两个 ThreadLocal 存请求上下文:
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
这样一来,同一个请求(对应一个线程)里,不管在哪个类、哪个方法,都能轻松拿到 Request 对象,代码耦合度大大降低。
总结
ThreadLocal 核心就是 “线程专属小账本”,记住 3 个方法、2 个场景、1 个注意点:
- 3 个方法:set () 存、get () 取、remove () 删(重点!);
- 2 个场景:线程数据隔离(防并发)、跨函数传数据(解耦);
- 1 个注意点:线程池场景一定要在 finally 里调用 remove (),避免内存泄露和脏数据。
掌握这些,不管是开发还是面试,ThreadLocal 这块都没问题了~