多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

ThreadLocal探秘(末尾已附上示例代码)

ThreadLocal探秘(末尾已附上示例代码) 一、核心定义ThreadLocal是 Java 并发编程工具类为每个线程单独创建一份独立的变量副本多个线程共用同一个ThreadLocal对象但各自存取的数据互不干扰、线程隔离天然规避多线程共享变量的并发竞争问题。简单一句话数据绑定在线程身上线程私有线程间互不共享。二、底层存储原理每个Thread线程内部都自带一个容器ThreadLocalMap自定义哈希表ThreadLocalMap的 key弱引用存储的ThreadLocal对象实例ThreadLocalMap的 value线程真正存放的变量副本存取流程线程调用 threadLocal.set(obj) → 获取当前线程 Thread → 取出线程内部的 ThreadLocalMap → 以当前ThreadLocal为key存入value 线程调用 threadLocal.get() → 获取当前线程 → 拿到ThreadLocalMap → 根据ThreadLocal key 取出专属value关键数据并不存在 ThreadLocal 对象里全都存在线程自身的 ThreadLocalMap 中三、常用API// 1. 创建ThreadLocal对象ThreadLocalStringtlnewThreadLocal();// 2. 设置当前线程专属值tl.set(张三);// 3. 获取当前线程的值Stringnametl.get();// 4. 移除当前线程存储的数据非常重要tl.remove();// 5. 设置初始默认值Java8推荐ThreadLocalIntegernumThreadLocal.withInitial(()-0);四、典型使用场景1. 链路追踪 / 全局上下文传递一次接口请求贯穿多个方法不用频繁传参把用户信息、请求ID存入ThreadLocal全链路任意位置都能获取Spring MVCRequestContextHolder底层就是 ThreadLocal保存Request、Response2. 数据库事务管理Spring 事务同一个线程多次数据库操作需要共用同一个 Connection 连接用 ThreadLocal 绑定数据库连接保证事务一致性3. 日期格式化 SimpleDateFormatSimpleDateFormat线程不安全不用每次新建对象用 ThreadLocal 每个线程持有一个格式化实例兼顾安全与性能五、优缺点优点完美实现线程私有数据隔离无需加锁无并发竞争开销简化跨层级参数传递避免方法层层传参的臃肿代码轻量便捷缺点 两大坑高频面试考点坑1内存泄漏ThreadLocalMap的 key 是弱引用ThreadLocal对象没有外部强引用时GC会自动回收keyvalue 是强引用不会自动回收场景线程池复用线程Tomcat线程池线程执行完毕不会销毁会放回线程池常驻如果没有手动调用remove()value一直被强引用无法GC堆积后造成内存泄漏✅ 解决方案线程业务结束前必须调用threadLocal.remove()坑2线程池复用导致数据污染线程池会循环复用线程上一个线程存入的数据没调用remove清空下一个任务拿到旧数据引发数据错乱。例线程A存了用户A信息任务结束没清空线程池复用该线程执行任务B直接读到用户A的数据。六、ThreadLocal vs synchronized对比ThreadLocalsynchronized原理空间换时间每个线程独立副本无竞争时间换空间加锁排队共用一份资源并发方式线程隔离不存在共享资源多线程争抢同一资源适用场景保存线程私有上下文、参数传递多线程共享资源需要同步计算性能无锁效率更高存在锁竞争效率偏低七、衍生InheritableThreadLocal普通 ThreadLocal子线程无法继承父线程的数据InheritableThreadLocal创建子线程时会把父线程的ThreadLocal数据拷贝一份给子线程适合场景异步任务需要继承主线程上下文异步日志、用户信息传递八、ThreadLocal 完整测试代码示例包含 4 段案例基础线程隔离演示线程池复用不 remove数据污染问题调用 remove解决污染避免内存泄漏InheritableThreadLocal 父子线程传值1、基础示例多线程数据互不干扰publicclassThreadLocalDemo1{// 创建ThreadLocal存放线程私有字符串privatestaticThreadLocalStringthreadLocalnewThreadLocal();publicstaticvoidmain(String[]args){// 线程1newThread(()-{threadLocal.set(线程一号的数据);System.out.println(Thread.currentThread().getName()threadLocal.get());threadLocal.remove();// 使用完毕清空},线程A).start();// 线程2newThread(()-{threadLocal.set(线程二号的数据);System.out.println(Thread.currentThread().getName()threadLocal.get());threadLocal.remove();},线程B).start();}}运行结果线程A线程一号的数据 线程B线程二号的数据两个线程存取互不影响。2、坑线程池不调用 remove → 数据污染高频面试题线程池会复用线程上次残留数据会被下一个任务读取importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassThreadLocalDemo2{privatestaticThreadLocalIntegeruserIdnewThreadLocal();// 固定大小线程池2个线程反复复用privatestaticExecutorServicepoolExecutors.newFixedThreadPool(2);publicstaticvoidmain(String[]args){// 提交4个任务线程只有2个必然复用for(inti1;i4;i){finalintidi;pool.submit(()-{// 设置当前用户iduserId.set(id);System.out.println(Thread.currentThread().getName() 存入用户id读取到userId.get());// 重点没有执行 remove()});}pool.shutdown();}}错误现象示例输出pool-1-thread-1 存入用户1读取到1 pool-1-thread-2 存入用户2读取到2 pool-1-thread-1 存入用户3读取到3 pool-1-thread-2 存入用户4读取到4看似正常调整代码逻辑模拟场景先取值再赋值漏洞立刻暴露pool.submit(()-{// 先读取System.out.println(Thread.currentThread().getName() 读取旧数据userId.get());// 再赋值userId.set(id);System.out.println(Thread.currentThread().getName() 存入用户id\n);});会出现上一轮的用户数据被新任务读到造成业务错乱。3、正确写法finally 中执行 remove()无论正常结束还是异常都清空数据杜绝数据污染内存泄漏importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassThreadLocalDemo3{privatestaticThreadLocalIntegeruserIdnewThreadLocal();privatestaticExecutorServicepoolExecutors.newFixedThreadPool(2);publicstaticvoidmain(String[]args){for(inti1;i4;i){finalintidi;pool.submit(()-{try{System.out.println(Thread.currentThread().getName() 初始读取userId.get());userId.set(id);System.out.println(Thread.currentThread().getName() 设置后读取userId.get()\n);}finally{// 必执行清空当前线程ThreadLocalMap数据userId.remove();}});}pool.shutdown();}}运行结果每次初始读取都是null没有残留脏数据。4、InheritableThreadLocal子线程继承父线程数据普通 ThreadLocal 子线程拿不到父线程值InheritableThreadLocal 新建子线程时会拷贝一份数据过去publicclassThreadLocalDemo4{// 普通ThreadLocalprivatestaticThreadLocalStringnormalTlnewThreadLocal();// 可继承ThreadLocalprivatestaticInheritableThreadLocalStringinheritableTlnewInheritableThreadLocal();publicstaticvoidmain(String[]args){// 主线程赋值normalTl.set(主线程普通TL数据);inheritableTl.set(主线程可继承TL数据);// 创建子线程newThread(()-{System.out.println(子线程获取普通TLnormalTl.get());System.out.println(子线程获取可继承TLinheritableTl.get());}).start();}}输出子线程获取普通TLnull 子线程获取可继承TL主线程可继承TL数据核心使用规范总结业务代码写在tryfinally必须调用threadLocal.remove()线程池环境下严禁忘记清空父子线程需要传递上下文用InheritableThreadLocal不要存大对象减少内存占用。
返回列表