作为后端博主,我猜你一定遇到过这种场景:
写接口时,Controller 要要 userId,Service 要要 userId,甚至 Mapper 里也得要 userId------ 结果就是从前端传过来后,一路public void doSomething(Long userId, ...)往下传,像个 "传话筒" 一样,烦到想摔键盘🤯!
今天要讲的ThreadLocal,就是帮你解决这个问题的 "神器"。但在讲实战前,先给它正个名:很多人以为它是 "容器",其实它只是个 "钥匙"!
一、先给 ThreadLocal「正名」:我不是容器!
面试时经常有人被问:"ThreadLocal 是用来存数据的容器吗?"
答案是:不是!不是!不是! (重要的事情说三遍)
它的真实身份是「线程数据工具」------ 真正存数据的,是线程(Thread类)自己带的一个Map容器(叫ThreadLocalMap)。
你可以这么理解:
- 每个线程(比如处理一次 HTTP 请求的线程)就像一个 "快递员",身上背了个「专属背包」(ThreadLocalMap);
- ThreadLocal就是这个背包的「专属钥匙」------ 只有这把钥匙能打开这个背包,别人的钥匙(其他 ThreadLocal 实例)打不开;
- 你存的数据(比如 userId),其实是放在 "快递员" 的背包里,而不是钥匙里。
用一张图就能看明白这个关系:
css
[Thread线程] → 背着 [ThreadLocalMap背包]
↓ (键是ThreadLocal实例,值是你要存的数据)
[key: 你的ThreadLocal对象 → value: userId=123]
所以记住:ThreadLocal 不存数据,它只是帮你把数据 "挂" 到当前线程上,而且只有当前线程能拿到!
二、ThreadLocal 常用 API:三步搞定「存 / 取 / 清」
ThreadLocal的 API 特别简单,核心就 3 个方法,加上 1 个 "初始化",记不住的话直接存这张 "操作手册":
API 方法 | 作用 | 通俗理解 |
---|---|---|
initialValue() | 初始化默认值(可选) | 给背包预设一张 "空白纸条" |
set(T value) | 往当前线程存数据 | 把数据放进背包 |
get() | 从当前线程取数据 | 从背包里拿数据 |
remove() | 从当前线程删数据 | 把背包里的东西清掉 |
举个简单的例子(伪代码):
kotlin
// 1. 定义一个ThreadLocal(相当于配一把"专属钥匙")
private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<Long>() {
// 可选:初始化默认值(没存数据时get()会返回这个)
@Override
protected Long initialValue() {
return 0L; // 比如默认返回0,表示"未登录"
}
};
// 2. 存数据(比如拦截器里解析token后存userId)
Long userId = 123L; // 从token里解析出来的用户ID
CURRENT_USER_ID.set(userId); // 把userId放进当前线程的"背包"
// 3. 取数据(Controller/Service/Mapper随便哪层)
Long currentUserId = CURRENT_USER_ID.get(); // 直接从"背包"拿,不用传参!
// 4. 清数据(关键!用完一定要清,不然会内存泄漏)
CURRENT_USER_ID.remove();
⚠️ 这里划重点:remove()一定要调用!后面讲面试题时会说为什么 ------ 简单说就是 "快递员(线程)会复用(线程池),不清理背包,下次别人用的时候会拿到上个人的东西!"
三、实战场景:用 ThreadLocal 搞定「当前用户」
结合你说的 "登录→token→拦截器→获取用户" 流程,我们一步一步看怎么落地:
整体流程(带图更直观):
关键代码实现(伪代码):
1. 先写一个「ThreadLocal 工具类」(统一管理)
csharp
/**
* 用户上下文工具类:专门管ThreadLocal的存/取/清
*/
public class UserContextHolder {
// 定义ThreadLocal实例(钥匙)
private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
// 存userId
public static void setUserId(Long userId) {
USER_ID_HOLDER.set(userId);
}
// 取userId(没存的话返回null,也可以加默认值)
public static Long getUserId() {
return USER_ID_HOLDER.get();
}
// 清userId(关键!一定要调用)
public static void clearUserId() {
USER_ID_HOLDER.remove();
}
}
2. 写「拦截器」:解析 token 并绑定 userId
拦截器是 "绑定数据" 的关键,要在请求进入 Controller 前完成操作,还要在请求结束后清理数据:
java
/**
* 登录拦截器:负责解析token,把userId绑到ThreadLocal
*/
public class LoginInterceptor implements HandlerInterceptor {
// 1. 请求处理前:解析token,存userId
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头拿token(比如前端存Authorization里)
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
response.setStatus(401); // 未登录
return false;
}
// 解析token(这里用JWT举例,实际看你项目用的框架)
Claims claims = Jwts.parser().setSigningKey("你的密钥").parseClaimsJws(token).getBody();
Long userId = claims.get("userId", Long.class); // 从token里拿userId
// 把userId绑到当前线程(调用工具类)
UserContextHolder.setUserId(userId);
return true; // 放行,进入Controller
}
// 3. 请求结束后:清理userId(防止内存泄漏!)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContextHolder.clearUserId(); // 关键步骤!
}
}
3. 随便哪层拿 userId:不用传参啦!
不管是 Controller、Service 还是 Mapper,直接调用工具类就行,爽到飞起:
less
// Controller层
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public Result createOrder(@RequestBody OrderDTO orderDTO) {
// 直接拿userId,不用从前端参数里要!
Long userId = UserContextHolder.getUserId();
return orderService.createOrder(userId, orderDTO);
}
}
// Service层
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public Result createOrder(Long userId, OrderDTO orderDTO) {
// 甚至这里都不用传userId,直接拿!
// Long userId = UserContextHolder.getUserId();
Order order = new Order();
order.setUserId(userId);
order.setOrderNo(UUID.randomUUID().toString());
// ... 其他逻辑
orderMapper.insert(order);
return Result.success();
}
}

四、面试常问:ThreadLocal 那些「坑」和「考点」
讲完实战,再聊聊面试高频题 ------ 毕竟咱们写博客不仅要解决问题,还得帮读者应付面试~
1. 面试官:ThreadLocal 是线程安全的吗?
答案:是,但有前提!
ThreadLocal 的线程安全,是因为它让 "每个线程用自己的数据"------ 线程 A 存的数,线程 B 拿不到,自然不会有线程间竞争。
但要注意:如果存的是「共享对象」(比如new ArrayList<>()),多个线程通过 ThreadLocal 拿到同一个对象实例,那还是会有线程安全问题!
比如:
dart
// 错误示范:存共享对象
private static final ThreadLocal<List<String>> LIST_HOLDER = ThreadLocal.withInitial(ArrayList::new);
// 线程A往列表加数据
List<String> listA = LIST_HOLDER.get();
listA.add("A");
// 线程B如果拿到同一个列表实例(比如误把list设为静态共享)
List<String> listB = LIST_HOLDER.get();
listB.add("B");
// 此时线程A的列表也会有"B",因为是同一个对象!
2. 面试官:ThreadLocal 会导致内存泄漏吗?怎么避免?
答案:会!但只要调用 remove () 就能避免。
先解释为什么会泄漏:
ThreadLocalMap(线程的 "背包")里,key是 ThreadLocal 实例的「弱引用」(GC 时会回收),但value是「强引用」(GC 不会回收)。
如果线程一直不销毁(比如线程池里的核心线程),key被回收后,value就变成了 "无主数据",一直占着内存,久而久之就内存泄漏了。
解决办法:用完就调用 remove ()
就像我们在拦截器的afterCompletion里做的那样 ------ 请求结束后,不管成功失败,都把value清掉,让 GC 能回收。
3. 面试官:ThreadLocal 和 Synchronized 有啥区别?
简单说:两者都是解决线程安全,但思路完全相反 ------
对比维度 | ThreadLocal | Synchronized |
---|---|---|
核心思路 | 「各玩各的」:每个线程用自己的数据 | 「排队玩」:多个线程抢同一个数据,加锁排队 |
数据共享 | 不共享(线程私有) | 共享(多线程共用) |
适用场景 | 避免参数传递(如存 userId) | 多线程修改共享数据(如计数器) |
4. 面试官:父子线程能共享 ThreadLocal 的数据吗?
默认不能!
因为 ThreadLocal 是 "线程私有" 的,父线程的 ThreadLocalMap 和子线程的没关系。
如果想共享,要用InheritableThreadLocal------ 它会把父线程的 ThreadLocal 数据,复制给子线程(注意是复制,不是共享同一个)。
比如:
scss
// 用InheritableThreadLocal
private static final ThreadLocal<Long> PARENT_CHILD_SHARE = new InheritableThreadLocal<>();
// 父线程存数据
PARENT_CHILD_SHARE.set(123L);
// 子线程拿数据
new Thread(() -> {
Long value = PARENT_CHILD_SHARE.get();
System.out.println(value); // 输出123,能拿到父线程的数据
}).start();
总结:ThreadLocal 就是后端的「效率神器」
用一句话总结 ThreadLocal:
它是帮你把 "线程私有数据" 绑定到线程上的工具,能解决 "到处传参" 的痛点,实战中常用它存当前用户、请求 ID 等信息,只要记得 "用完调用 remove ()",就能避免内存泄漏,还能轻松应对面试~
你们项目里用 ThreadLocal 还做过哪些骚操作?比如存请求日志、链路追踪 ID?评论区聊聊,互相抄作业呀~😉