首页 > 编程语言 >【Java 并发编程】(四) ThreadLocal 源码解读

【Java 并发编程】(四) ThreadLocal 源码解读

时间:2024-08-19 16:52:08浏览次数:13  
标签:set Java value ThreadLocal 源码 key Entry null

 

介绍

  • 每个Thread对象, 内部有一个ThreadLocalMap threadLocals, 这是一个哈希表, 底层是一个Node[ ] table;

  • 当在某个线程中调用ThreadLocal的set方法时, 会使用Thread.currentThread获取当前先线程的thread对象, 然后将ThreadLocal对象作为key, 将set方法的参数作为value, 构建一个Entry, 将此Entry保存到thead对象的ThreadLocalMap中;

  • 注意, Entry继承了WeakReference, 在构造方法中, 将ThreadLocal对象传给了WeakReference构造方法, 也就是说ThreadLocalMap中的ThreadLocal作为key, 是被弱引用指向的;

  • 这样做, 能保证在主程序中ThreadLocal的引用被置为null后, 对应的threadLocal对象就会被回收, 防止内存泄漏.

  • 但是仅仅这样, 还不能防止内存泄漏, ThreadLocal被回收以后, key的值变为null, 但是value还没被回收, 并且value一直有强引用指向; 所以需要调用remove方法, 删除整个Entry;

内存泄漏: 已经不再使用的对象仍然被持有引用,导致垃圾回收器无法回收这些对象的内存,从而导致内存无法释放,逐渐耗尽可用内存。我们在程序中, 应该尽量避免静态的大对象, 避免资源使用不释放, 例如输入输出流

例子

  1. 考虑一个SpringBoot 后端应用;

  2. 用户的请求里携带了用户的ID, 我希望在各种不同的地方都能方便地拿到这个Id, 比如在各个Controller里;

  3. 如果每次都去 Request 中取, 有点麻烦, 我希望能把这个值存起来;

  4. 做一个Filter, 里面设置一个静态变量, 请求来到的时候, 用这个静态变量来保存用户Id; 以后我直接用 Filter.id 就能拿到了;

  5. 单线程没问题, 多线程的情况下, 明显有问题: 不同的线程处理不同的请求都会使用同一个 filter 对象, 导致一会存的是这个请求中的userId, 一会又是另一个请求中的userId;

  6. 这时候, 就可以在 Filter 里设置一个 static ThreadLocal<UserID>

  7. 假设当前有两个线程; 线程 A 在 Filter 的时候调用 threadLocal.set, 把用户A的Id 保存到了自己 ThreadLocalMap;

    // 伪代码
     static ThreadLocal<Integer> id = new ThreadLocal<>();
     id.set(request.getUserId);
     chain.doFilter();
     id.remove();
  8. 线程B同理, 把用户B 的 Id 保存到了自己的 TheadLocalMap;

  9. 虽然不同线程用了同一个threadLocal对象作为key, 但 ThreadLocalMap 和 value 是各个线程自己的;

  10. 不同的线程使用相同的 threadLocal对象去 get, 拿到的是当前线程独有的 value;

  11. 在其他地方就可以通过 Filter.threadLocal.get 去获取本线程的用户Id;

  12. 不过需要在用完了以后及时回收; 比如在 Filter 里面请求返回的时候调用 threadLocal.remove, 把这次的键值对删除;

  13. 否则这个线程被分给下个请求的时候, 上个请求的键值对还在; 不过在这个场景下, 不 remove 并不会对内存产生太大危害, 这里 remove 是防止后面的请求查到上一个请求的数据;

    下个请求调用 threadLocal.set的时候, 用的还是同一个 threadLocal对象, 新的 Value 会覆盖旧的Value, 旧的Value没有引用指向就回收了

  14. 内存泄漏主要是考虑到线程长时间存在, 并且运行过程中不断往ThreadLocalMap 里加不同的 ThreadLocal; 如果只有若干个静态的ThreadLocal的话, 其实没啥问题;

  15. 为什么不直接让用户自己插入键值对到 ThreadLocalMap? 因为自动帮你做了一个 WeakReference;

初始化

  1. 初始时, 一个Thread对象的ThreadLocalMap threadLocals对象为null, 在该线程中首次调用threadLocal.set或get方法时, 会创建一个Capacity为16的哈希表, 装填因子为2 / 3;

  2. 如果时threadLocal.get方法触发的创建, 则会放入一个<threadLocal, null>的键值对

  3. 如果以set方法触发创建, 则放入的是<threadLocal, set方法的参数>

冲突解决

  1. 和HashMap不同, 使用的是线性探查法, 添加一个Entry时, 如果冲突, 则向后(循环)寻找到第一个null位置, 放到该位置;

ThreadLocal.set

  1. 计算应该放到的下标, 开始线性探查, 如果过程中遇到key==的结点, 进行value替换

  2. 如果探查到一个位置为null, 则放入新的Entry;

    private void set(ThreadLocal<?> key, Object value) {
         Entry[] tab = table;
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1); // 获取下标
         // 线性探查
         for (Entry e = tab[i];
              e != null;
              e = tab[i = nextIndex(i, len)]) {
             ThreadLocal<?> k = e.get();
             // 找到一个key相 == 的进行value替换, return
             if (k == key) {
                 e.value = value;
                 return;
             }
             // 如果过程中遇到一个Entry, 其弱引用已经被释放, 那么进行一些清除和rehash的工作, 并替换这个Entry, 返回
             if (k == null) {
                 /**
                 1.这个方法不需要看懂每一行
                 2.先向前寻找Entry != null 但 key为 null 的元素, 如果遇到 entry == null, 停止
                 3.如果向前一个都没找到, 尝试向后寻找, 记录下标;
                 4.替换弱引用已经被释放的这个位置
                 5.根据记录的下标, 清除一些key == null的结点
                     1.从记录的下标向后遍历, 遇到的key == null的就清除这个entry, 遇到key != null的尝试重新用线性探查rehash
                     2.遇到entry == null 就停止, 不追求全部处理一遍, 处理一部分就好, 因为每次set都会做
                 */
                 replaceStaleEntry(key, value, i);
                 return;
             }
         }
         // 找到null位置
         tab[i] = new Entry(key, value);
         // 更新size
         int sz = ++size;
         // 进行清除, 重新hash, 扩容检查
         if (!cleanSomeSlots(i, sz) && sz >= threshold)
             rehash();
     }
  3. 流程:

    ThreadLocal的set方法获取currentThread, 拿到currentThread的ThreadLocalMap, 调用这个Map的set方法

ThreadLocal.get

  1. 线性探查

    private Entry getEntry(ThreadLocal<?> key) {
         int i = key.threadLocalHashCode & (table.length - 1);
         Entry e = table[i];
         if (e != null && e.get() == key)
             return e;
         else
         // 线性探查法继续向后寻找, 找到第一个null的位置时, 说明不存在, 返回null               
         return getEntryAfterMiss(key, i, e);
     }
  2. 流程总结:

    ThreadLocal的get方法获取currentThread, 拿到其threadLocals, 调用它的getEntry方法, 如果getEntry找到了对应的key, 返回对应的value; 如果getEntry返回null, 没找到, 那么将<threadLocal, null>放入ThreadLocalMap, 并返回null;

扩容

  1. size >= threshold时扩容

  2. 容量 * 2; 对以前的元素重新hash;

防止内存泄漏

  • 两重保险, 一个是弱引用, 一个是remove

  • 手动remove, 会直接清除掉整个Entry, table[i] = null;

  • 如果没有remove, 但是threadLocal的强引用改为null了, 那么ThreadLocalMap中的弱引用不会阻值垃圾回收, threadLocal对象将被回收, ThreadLocalMap中对应的Entry的key指向null;

  • 后续调用ThreadLocalMap的set方法时, 每次都会清除一部分 key == null 的Entry;

  • 面试题: 为什么value不设置为弱引用? 一是不能, 二是没必要

    1. 不能: 当我们通过 threadLocal.set 去 set 一个value 的时候, 很有可能是没有给 value 设置强引用的, 比如 threadLocal.set(new Object()), 如果 value 也是弱引用, 这时就会出现 key 还在, value 已经被回收的情况, 这样通过 get 方法就只能获取到 null ;

    2. 没必要: 因为调用 threadLocal.set 和 get 时, 进一步调用到 ThreadLocalMap 的 set get 方法, 会对 key == null 的 Entry 进行清除工作, 这样 value 也就一并被清除了;

    3. 没必要: 提供了remove方法, 只要我们使用规范, 及时 remove, 就能避免造成内存泄漏;

  • 面试题: 既然弱引用指向的对象一发生gc就被回收, 我怎么保证用到 threadLocal 对象的时候它还在?

        陷阱问题, "弱引用指向的对象一发生gc就被回收"这句话是错误的; 当一个对象没有被强引用指向, 只被弱引用或虚引用指向的时候, gc 才会回收它; 

标签:set,Java,value,ThreadLocal,源码,key,Entry,null
From: https://blog.csdn.net/wdx7770/article/details/141251347

相关文章

  • java学习第八周
    临近开学,本周的任务完成情况不够好,平常乱七八糟的事情比较多,所以放在学习上的心思比较少。平均每天放在JAVA学习的时间约1个小时,放在编程的时间约半小时,解决问题的时间约1小时。下一个星期就要开学了,回看自己暑期的JAVA学习情况感觉比之前的暑期有很大的进步,在家中能拿出大量的时......
  • HTML5服装电商网上商城模板源码
    文章目录1.设计来源1.1主界面1.2购物车界面1.3电子产品界面1.4商品详情界面1.5联系我们界面1.6各种标签演示界面2.效果和源码2.1动态效果2.2源代码源码下载万套模板,程序开发,在线开发,在线沟通         【博主推荐】:前些天发现了一个巨牛的人工智能......
  • Java异常处理
    Java异常处理java:Compilationfailed:internaljavacompilererrorjava:Compilationfailed:internaljavacompilererror原因:idea的jdk版本和项目配置的不同。比对idea中三处关于jdk版本配置:setting-Build,Execution,Deployment-Compiler-JavaCompilerProj......
  • Java中的可达性分析算法图解,以及哪些对象可以作为GCRoots
    可达性分析算法图示:解释:因为在GCRoots中存在对于对象A的引用,而A又持有对对象B和对象C的引用,所以这一串都是有用的引用链,需要保留。对于对象D和对象E,他们只是相互进行引用,并没有和GCRoots中的对象有任何的关联,所以可以安全的回收。哪些对象可以作为GCRoots虚拟机栈(栈帧中的......
  • 一个专门用于Java服务端图片合成的工具,支持图片、文本、矩形等多种素材的合成,功能丰富
    前言在数字化营销的当下,企业对于图片处理的需求日益增长。然而,传统的图片处理方式往往需要复杂的操作和专业的技术,这不仅增加了工作量,也提高了时间成本。为了处理这一问题,一款能够简化图片合成流程的软件应运而生。介绍ImageCombiner是一款面向Java服务端的图片合成工具,以......
  • 一款专为IntelliJ IDEA用户设计的插件,极大简化Spring项目中的API调试过程,功能强大(附源
    前言在软件开发过程中,尤其是SpringMVC(Boot)项目中,API调试调用是一项常见但繁琐的任务。现有的开发工具虽然提供了一些支持,但往往存在效率不高、操作复杂等问题。为了处理这些痛点,提升开发效率,一款新的工具应运而生。介绍CoolRequest是一款专为IntelliJIDEA用户设计的插......
  • 基于python个性化旅游线路推荐系统(源码+文档+调试+讲解)
    收藏关注不迷路!!......
  • C# 小区物业管理系统的设计与实现 C# 物业管理 毕业设计 (源码)
    目录一.研究目的二.系统需求分析2.1功能需求2.2非功能需求三.数据库实现四.系统页面展示五.留言(源码获取方式一.研究目的本研究旨在设计并实现一套基于Web的小区物业管理系统,以提高小区物业管理效率、改善小区居民服务体验、促进信息共享和智能化管理、推动智慧小区......
  • java.lang.IllegalArgumentException: Comparison method violates its general contr
    代码:publicstaticvoidwbsSort(List<SendMessageEntity>sendMessageEntityList){Collections.sort(sendMessageEntityList,(o1,o2)->{StringwbsCode1Temp=o1.getWbsCode();StringwbsCode2Temp=o2.getWbsCode();......
  • ZoneJs 源码解析
    ZoneJs源码解析ZoneJs是什么,它能干什么,它是怎么做到的?Zone是为js的执行提供了一个共享的数据上下文。为js函数执行维护了一个逻辑上的调用栈。同时提供了对于函数执行方法的拦截,在函数执行前后,添加一些通用的逻辑(例如日志,异常处理)。统一的任务模型,提供对于宏任务/微任务/......