ThreadLocal深入剖析
前言
在并发编程中,当多个线程同时操作一个共享变量,就会出现线程安全问题。常见的解决方案是加锁,但锁会带来性能开销,尤其在高并发场景下。今天要讲的ThreadLocal,提供了另一种思路:让每个线程拥有自己独立的变量副本,从根本上避免竞争。
🏠个人主页:你的主页
文章目录
- ThreadLocal深入剖析
- 一、线程安全问题的两种场景
- 二、ThreadLocal的正确使用场景
- 三、ThreadLocal核心API
- 四、底层数据结构演进
- 五、ThreadLocal内存泄漏问题
- 六、实战最佳实践
- 七、InheritableThreadLocal
- 八、总结
一、线程安全问题的两种场景
在讨论 ThreadLocal 之前,我们先搞清楚:什么场景该用锁,什么场景该用 ThreadLocal。
1.1 场景一:多线程竞争修改同一个值
场景描述:电商系统的商品秒杀,100 件商品,多个用户同时抢购。
publicclassSeckillService{privateintstock=100;// 库存,共享变量publicvoidseckill(){if(stock>0){stock--;// 扣减库存System.out.println(Thread.currentThread().getName()+" 抢购成功,剩余库存:"+stock);}else{System.out.println(Thread.currentThread().getName()+" 抢购失败,库存不足");}}}问题:两个线程同时判断stock > 0,都通过了,然后都执行stock--,导致超卖。
线程A:读取 stock = 1,判断 > 0,准备扣减 线程B:读取 stock = 1,判断 > 0,准备扣减 线程A:stock-- → stock = 0 线程B:stock-- → stock = -1 ❌ 超卖了!解决方案:加锁
publicsynchronizedvoidseckill(){if(stock>0){stock--;System.out.println(Thread.currentThread().getName()+" 抢购成功");}}能用 ThreadLocal 吗?不能!
因为 ThreadLocal 会给每个线程创建独立副本,每个线程都有自己的stock = 100,那每个人都能抢 100 件,这显然不对。
结论:当多个线程需要竞争修改同一个值时,必须用锁,不能用 ThreadLocal。
1.2 场景二:每个线程需要独立的上下文数据
场景描述:Web 系统中,每个请求需要携带当前登录用户的信息,在整个请求链路中随时可以获取。
用户A发起请求 → 拦截器解析Token获取用户A信息 → Controller → Service → DAO 用户B发起请求 → 拦截器解析Token获取用户B信息 → Controller → Service → DAO如果用普通的共享变量存储用户信息:
publicclassUserContext{publicstaticUsercurrentUser;// 共享变量}问题:
线程A:设置 currentUser = 用户A 线程B:设置 currentUser = 用户B ← 覆盖了! 线程A:获取 currentUser → 拿到的是用户B ❌ 数据错乱!解决方案:ThreadLocal
publicclassUserContext{privatestaticfinalThreadLocal<User>currentUser=newThreadLocal<>();publicstaticvoidsetUser(Useruser){currentUser.set(user);}publicstaticUsergetUser(){returncurrentUser.get();}}每个线程都有自己独立的 User 副本,互不干扰。
结论:当每个线程需要独立的上下文数据,且这个数据在整个线程生命周期内需要被多处访问时,用 ThreadLocal。
1.3 两种场景对比
| 场景 | 特点 | 解决方案 |
|---|---|---|
| 秒杀库存 | 多线程竞争修改同一个值 | 加锁(synchronized/Lock) |
| 用户上下文 | 每个线程需要独立的值 | ThreadLocal |
一句话总结:
- 锁:让多个线程排队操作同一个变量
- ThreadLocal:让每个线程各自操作自己的变量
二、ThreadLocal的正确使用场景
2.1 场景一:请求上下文传递
这是 ThreadLocal 最经典的使用场景。
需求:在 Web 应用中,用户登录后,整个请求链路都需要获取当前用户信息。
传统做法:把用户信息作为参数层层传递
// ControllerpublicvoidcreateOrder(Useruser,OrderDTOorderDTO){orderService.createOrder(user,orderDTO);}// ServicepublicvoidcreateOrder(Useruser,OrderDTOorderDTO){// 业务逻辑orderDao.insert(user,order);logService.log(user,"创建订单");}问题:参数传递太繁琐,代码侵入性强。
ThreadLocal 做法:
// 定义用户上下文publicclassUserContextHolder{privatestaticfinalThreadLocal<User>userHolder=newThreadLocal<>();publicstaticvoidsetUser(Useruser){userHolder.set(user);}publicstaticUsergetUser(){returnuserHolder.get();}publicstaticvoidclear(){userHolder.remove();}}// 拦截器中设置publicclassAuthInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringtoken=request.getHeader("Authorization");Useruser=tokenService.parseToken(token);UserContextHolder.setUser(user);// 存入 ThreadLocalreturntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){UserContextHolder.clear();// 请求结束,清理 ThreadLocal}}// 业务代码中随时获取publicvoidcreateOrder(OrderDTOorderDTO){Useruser=UserContextHolder.getUser();// 直接获取,无需传参// 业务逻辑...}2.2 场景二:数据库连接管理
需求:保证同一个线程内的多次数据库操作使用同一个连接,实现事务。
publicclassConnectionManager{privatestaticfinalThreadLocal<Connection>connectionHolder=newThreadLocal<>();publicstaticConnectiongetConnection()throwsSQLException{Connectionconn=connectionHolder.get();if(conn==null){conn=dataSource.getConnection();connectionHolder.set(conn);}returnconn;}publicstaticvoidcloseConnection(){Connectionconn=connectionHolder.get();if(conn!=null){try{conn.close();}catch(SQLExceptione){e.printStackTrace();}connectionHolder.remove();}}}Spring 的@Transactional底层就是用 ThreadLocal 来保证同一事务内使用同一个数据库连接。
2.3 场景三:日期格式化工具
问题:SimpleDateFormat不是线程安全的。
// 错误示范:多线程共享 SimpleDateFormatpublicclassDateUtil{privatestaticfinalSimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd");publicstaticStringformat(Datedate){returnsdf.format(date);// 多线程调用会出问题!}}ThreadLocal 解决:
publicclassDateUtil{privatestaticfinalThreadLocal<SimpleDateFormat>dateFormatHolder=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd"));publicstaticStringformat(Datedate){returndateFormatHolder.get().format(date);// 每个线程用自己的实例}}2.4 场景四:链路追踪 TraceId
需求:分布式系统中,一个请求会经过多个服务,需要用 TraceId 串联整个调用链路。
publicclassTraceContext{privatestaticfinalThreadLocal<String>traceIdHolder=newThreadLocal<>();publicstaticvoidsetTraceId(StringtraceId){traceIdHolder.set(traceId);}publicstaticStringgetTraceId(){returntraceIdHolder.get();}publicstaticvoidclear(){traceIdHolder.remove();}}// 在日志中自动带上 TraceIdpublicvoiddoSomething(){log.info("[{}] 开始处理业务",TraceContext.getTraceId());// 业务逻辑...}三、ThreadLocal核心API
3.1 基本方法
ThreadLocal<String>threadLocal=newThreadLocal<>();// 设置值threadLocal.set("hello");// 获取值Stringvalue=threadLocal.get();// "hello"// 移除值(重要!防止内存泄漏)threadLocal.remove();3.2 初始值设置
方式一:重写 initialValue 方法
ThreadLocal<Integer>counter=newThreadLocal<Integer>(){@OverrideprotectedIntegerinitialValue(){return0;}};方式二:使用 withInitial(推荐,Java 8+)
ThreadLocal<Integer>counter=ThreadLocal.withInitial(()->0);ThreadLocal<List<String>>listHolder=ThreadLocal.withInitial(ArrayList::new);3.3 完整示例
publicclassThreadLocalDemo{privatestaticfinalThreadLocal<Integer>counter=ThreadLocal.withInitial(()->0);publicstaticvoidmain(String[]args){// 线程1newThread(()->{for(inti=0;i<3;i++){intvalue=counter.get();counter.set(value+1);System.out.println(Thread.currentThread().getName()+": "+counter.get());}},"Thread-A").start();// 线程2newThread(()->{for(inti=0;i<3;i++){intvalue=counter.get();counter.set(value+1);System.out.println(Thread.currentThread().getName()+": "+counter.get());}},"Thread-B").start();}}输出(顺序可能不同,但每个线程独立计数):
Thread-A: 1 Thread-A: 2 Thread-A: 3 Thread-B: 1 Thread-B: 2 Thread-B: 3两个线程各自从 0 开始计数,互不影响。
四、底层数据结构演进
4.1 Java 8 之前的设计
ThreadLocal 对象 └── ThreadLocalMap ├── Entry(Thread-A, value-A) ├── Entry(Thread-B, value-B) └── Entry(Thread-C, value-C)特点:
- ThreadLocal 维护一个 Map
- Key 是 Thread 对象
- Value 是变量副本
问题:
- 需要对 Map 加锁,因为多个线程会同时操作这个 Map
- 线程销毁后,对应的 Entry 不会自动清理
4.2 Java 8 之后的设计
Thread-A 对象 └── ThreadLocalMap ├── Entry(ThreadLocal-1, value-1) ├── Entry(ThreadLocal-2, value-2) └── Entry(ThreadLocal-3, value-3) Thread-B 对象 └── ThreadLocalMap ├── Entry(ThreadLocal-1, value-1) └── Entry(ThreadLocal-2, value-2)特点:
- 每个 Thread 对象内部维护一个 ThreadLocalMap
- Key 是 ThreadLocal 对象
- Value 是变量副本
优势:
- 无需加锁,因为每个线程只操作自己的 Map
- 线程销毁时,ThreadLocalMap 随之销毁,Entry 自动回收
4.3 源码分析
Thread 类中的字段:
publicclassThreadimplementsRunnable{// 每个线程都有自己的 ThreadLocalMapThreadLocal.ThreadLocalMapthreadLocals=null;}ThreadLocal.set() 方法:
publicvoidset(Tvalue){Threadt=Thread.currentThread();// 获取当前线程ThreadLocalMapmap=getMap(t);// 获取当前线程的 ThreadLocalMapif(map!=null){map.set(this,value);// Key 是当前 ThreadLocal 对象}else{createMap(t,value);// 首次使用,创建 Map}}ThreadLocalMapgetMap(Threadt){returnt.threadLocals;// 返回线程的 threadLocals 字段}ThreadLocal.get() 方法:
publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){return(T)e.value;}}returnsetInitialValue();// 没有值则返回初始值}4.4 ThreadLocalMap 的结构
ThreadLocalMap 是 ThreadLocal 的静态内部类,使用数组 + 线性探测法解决哈希冲突。
staticclassThreadLocalMap{staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);// Key 是弱引用value=v;}}privateEntry[]table;// Entry 数组}关键点:Entry 的 Key(ThreadLocal 对象)是弱引用。
五、ThreadLocal内存泄漏问题
5.1 为什么会内存泄漏
先看 Entry 的结构:
staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;// Value 是强引用}- Key(ThreadLocal):弱引用,GC 时会被回收
- Value:强引用,不会被自动回收
泄漏场景:
1. ThreadLocal 对象被设为 null 2. GC 回收 ThreadLocal 对象(因为是弱引用) 3. Entry 的 Key 变成 null,但 Value 还在 4. 如果线程一直存活(比如线程池),这个 Value 永远无法被回收Thread └── ThreadLocalMap └── Entry ├── Key: null(已被 GC 回收) └── Value: 还在!无法回收!← 内存泄漏5.2 为什么 Key 要设计成弱引用
如果 Key 是强引用:
ThreadLocal tl = new ThreadLocal(); tl.set("value"); tl = null; // 想释放 ThreadLocal // 但是 Entry 还持有 ThreadLocal 的强引用 // ThreadLocal 对象无法被回收 ← 更严重的内存泄漏!设计成弱引用后,至少 ThreadLocal 对象可以被回收,只是 Value 还在。
5.3 ThreadLocal 的自我清理机制
ThreadLocal 在get()、set()、remove()时会顺便清理Key 为 null 的 Entry:
privateintexpungeStaleEntry(intstaleSlot){Entry[]tab=table;// 清理 Key 为 null 的 Entrytab[staleSlot].value=null;tab[staleSlot]=null;// ...}但这个清理是被动的,如果一直不调用这些方法,泄漏的 Value 就一直存在。
5.4 正确的使用姿势
原则:用完必须调用 remove()
publicclassUserContextHolder{privatestaticfinalThreadLocal<User>userHolder=newThreadLocal<>();publicstaticvoidsetUser(Useruser){userHolder.set(user);}publicstaticUsergetUser(){returnuserHolder.get();}// 关键:提供 clear 方法publicstaticvoidclear(){userHolder.remove();}}// 使用时try{UserContextHolder.setUser(user);// 业务逻辑...}finally{UserContextHolder.clear();// 必须清理!}在 Web 应用中:
publicclassAuthInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(...){UserContextHolder.setUser(user);returntrue;}@OverridepublicvoidafterCompletion(...){UserContextHolder.clear();// 请求结束时清理}}六、实战最佳实践
6.1 完整的上下文工具类
publicclassRequestContext{privatestaticfinalThreadLocal<Map<String,Object>>CONTEXT=ThreadLocal.withInitial(HashMap::new);// 设置属性publicstaticvoidset(Stringkey,Objectvalue){CONTEXT.get().put(key,value);}// 获取属性@SuppressWarnings("unchecked")publicstatic<T>Tget(Stringkey){return(T)CONTEXT.get().get(key);}// 获取属性,带默认值@SuppressWarnings("unchecked")publicstatic<T>Tget(Stringkey,TdefaultValue){Tvalue=(T)CONTEXT.get().get(key);returnvalue!=null?value:defaultValue;}// 移除属性publicstaticvoidremove(Stringkey){CONTEXT.get().remove(key);}// 清空所有(重要!)publicstaticvoidclear(){CONTEXT.remove();}// 常用快捷方法publicstaticvoidsetUserId(LonguserId){set("userId",userId);}publicstaticLonggetUserId(){returnget("userId");}publicstaticvoidsetTraceId(StringtraceId){set("traceId",traceId);}publicstaticStringgetTraceId(){returnget("traceId");}}6.2 配合 Spring 拦截器使用
@ComponentpublicclassContextInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){// 生成 TraceIdStringtraceId=UUID.randomUUID().toString().replace("-","");RequestContext.setTraceId(traceId);// 解析用户信息Stringtoken=request.getHeader("Authorization");if(StringUtils.hasText(token)){LonguserId=tokenService.parseUserId(token);RequestContext.setUserId(userId);}returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){// 请求结束,必须清理RequestContext.clear();}}6.3 线程池场景的注意事项
线程池中的线程是复用的,如果不清理 ThreadLocal,下一个任务会拿到上一个任务的数据。
ExecutorServiceexecutor=Executors.newFixedThreadPool(2);executor.submit(()->{try{RequestContext.setUserId(100L);// 业务逻辑...}finally{RequestContext.clear();// 必须清理!}});更优雅的方式:封装任务包装器
publicclassContextAwareRunnableimplementsRunnable{privatefinalRunnabletask;privatefinalMap<String,Object>context;publicContextAwareRunnable(Runnabletask){this.task=task;// 捕获提交任务时的上下文this.context=newHashMap<>(RequestContext.getAll());}@Overridepublicvoidrun(){try{// 恢复上下文RequestContext.setAll(context);task.run();}finally{// 清理上下文RequestContext.clear();}}}// 使用executor.submit(newContextAwareRunnable(()->{// 可以获取到父线程的上下文LonguserId=RequestContext.getUserId();}));七、InheritableThreadLocal
7.1 问题:子线程无法获取父线程的 ThreadLocal
ThreadLocal<String>threadLocal=newThreadLocal<>();threadLocal.set("父线程的值");newThread(()->{System.out.println(threadLocal.get());// null!}).start();子线程无法获取父线程设置的值。
7.2 解决方案:InheritableThreadLocal
InheritableThreadLocal<String>inheritableThreadLocal=newInheritableThreadLocal<>();inheritableThreadLocal.set("父线程的值");newThread(()->{System.out.println(inheritableThreadLocal.get());// "父线程的值"}).start();原理:创建子线程时,会把父线程的 InheritableThreadLocal 值复制一份给子线程。
7.3 局限性
InheritableThreadLocal 只在创建线程时复制,对于线程池场景不适用:
ExecutorServiceexecutor=Executors.newFixedThreadPool(1);inheritableThreadLocal.set("任务1的值");executor.submit(()->{System.out.println(inheritableThreadLocal.get());// "任务1的值" ✅});inheritableThreadLocal.set("任务2的值");executor.submit(()->{System.out.println(inheritableThreadLocal.get());// 还是"任务1的值" ❌});因为线程池复用线程,第二个任务用的是已经创建好的线程,不会重新复制。
解决方案:使用阿里开源的TransmittableThreadLocal(TTL)。
八、总结
8.1 核心要点
| 要点 | 说明 |
|---|---|
| 适用场景 | 每个线程需要独立的上下文数据 |
| 不适用场景 | 多线程竞争修改同一个值 |
| 底层结构 | 每个 Thread 持有 ThreadLocalMap,Key 是 ThreadLocal |
| 内存泄漏 | Key 是弱引用会被回收,Value 是强引用不会自动回收 |
| 最佳实践 | 用完必须调用 remove() |
8.2 使用场景总结
| 场景 | 示例 |
|---|---|
| 请求上下文 | 用户信息、租户信息、TraceId |
| 数据库连接 | 同一事务内复用连接 |
| 日期格式化 | SimpleDateFormat 线程安全问题 |
| 分布式追踪 | 链路追踪 TraceId 传递 |
8.3 ThreadLocal vs 锁
| 对比项 | ThreadLocal | 锁(synchronized/Lock) |
|---|---|---|
| 解决思路 | 空间换时间,每个线程一份副本 | 时间换安全,排队访问 |
| 适用场景 | 线程隔离,各自操作各自的数据 | 线程同步,共同操作同一份数据 |
| 性能 | 无竞争,性能高 | 有竞争,性能相对低 |
| 数据一致性 | 不保证(各自独立) | 保证(同一份数据) |
记住:ThreadLocal 不是用来解决共享变量的线程安全问题的,而是用来实现线程隔离的。
热门专栏推荐
- Agent小册
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟