面试官,ThreadLocal 你要这么问,我就挂了

网站建设4年前发布
30 0 0

说到底,你真的会造火箭吗?,常说面试造火箭,入职拧螺丝。但你真的有造火箭的本事吗,大部分都是不敢承认自己的知识盲区和技术瓶颈以及经验不足的自嘲。,面试时,所以,从不是CRUD选择了你,也不是造螺丝让你成为工具人。而是你的技术能力决定你的眼界,眼界又决定了你写出的代码!,谢飞机,小记 还没有拿到 offer 的飞机,早早起了床,吃完两根油条,又跑到公司找面试官取经!,2023030613461393b33c769519bf55608097259fff6ecc78645f257,灵魂画手 & 老纪,面试官:飞机,听坦克说,你最近贪黑起早的学习呀。,谢飞机:嗯嗯,是的,最近头发都快掉没了!,面试官:那今天我们聊聊 ThreadLocal,一般可以用在什么场景中?,谢飞机:嗯,ThreadLocal 要解决的是线程内资源共享 (This class provides thread-local variables.),所以一般会用在全链路监控中,或者是像日志框架 MDC 这样的组件里。,面试官:飞机不错哈,最近确实学习了。那你知道 ThreadLocal是怎样的数据结构吗,采用的是什么散列方式?,谢飞机:数组?嗯,怎么散列的不清楚...,面试官:那 ThreadLocal 有内存泄漏的风险,是怎么发生的呢?另外你了解在这个过程的,探测式清理和启发式清理吗?,谢飞机:这...,盲区了,盲区了,可乐我放桌上了,我回家再看看书!​,ThreadLocal,作者:Josh Bloch and Doug Lea,两位大神,如果仅是日常业务开发来看,这是一个比较冷门的类,使用频率并不高。并且它提供的方法也非常简单,一个功能只是潦潦数行代码。,如果深挖实现部分的源码,就会发现事情并不那么简单。这里涉及了太多的知识点,包括;数据结构拉链存储斐波那契散列神奇的0x61c88647弱引用Reference过期key探测清理和启发式清理等等。,接下来,我们就逐步学习这些盲区知识。本文涉及了较多的代码和实践验证图稿,欢迎关注公众号:bugstack虫洞栈,回复下载得到一个链接打开后,找到ID:19获取!*,你写过这样的代码吗?如果还在这么写,那就已经犯了一个线程安全的错误。SimpleDateFormat,并不是一个线程安全的类。,1.1.1 线程不安全验证,这是一个多线程下 SimpleDateFormat 的验证代码。当 equals 为false 时,证明线程不安全。运行结果如下;,为了线程安全最直接的方式,就是每次调用都直接 new SimpleDateFormat。但这样的方式终究不是最好的,所以我们使用 ThreadLocal ,来优化这段代码。,如上我们把 SimpleDateFormat ,放到 ThreadLocal 中进行使用,即不需要重复new对象,也避免了线程不安全问题。测试结果如下;,近几年基于谷歌Dapper论文实现非入侵全链路追踪,使用的越来越广了。简单说这就是一套监控系统,但不需要你硬编码的方式进行监控方法,而是基于它的设计方案采用 javaagent + 字节码 插桩的方式,动态采集方法执行信息。如果你想了解字节码插桩技术,可以阅读我的字节码编程专栏:https://bugstack.cn/itstack-demo-agent/itstack-demo-agent.html,重点,动态采集方法执行信息。这块是主要部分,跟 ThreadLocal 相关。字节码插桩解决的是非入侵式编程,那么在一次服务调用时,在各个系统间以及系统内多个方法的调用,都需要进行采集。这个时候就需要使用 ThreadLocal 记录方法执行ID,当然这里还有跨线程调用使用的也是增强版本的 ThreadLocal,但无论如何基本原理不变。,1.2.1 追踪代码,这里举例全链路方法调用链追踪,部分代码,1.2.2 测试结果,测试方法,配置参数:-javaagent:E:\itstack\GIT\itstack.org\itstack-demo-agent\itstack-demo-agent-06\target\itstack-demo-agent-06-1.0.0-SNAPSHOT.jar=testargs,运行结果,咳咳,除此之外所有需要活动方法调用链的,都需要使用到 ThreadLocal,例如 MDC 日志框架等。接下来我们开始详细分析 ThreadLocal 的实现。,了解一个功能前,先了解它的数据结构。这就相当于先看看它的地基,有了这个根本也就好往后理解了。以下是 ThreadLocal 的简单使用以及部分源码。,new ThreadLocal<String>().set("小傅哥");,从这部分源码中可以看到,ThreadLocal 底层采用的是数组结构存储数据,同时还有哈希值计算下标,这说明它是一个散列表的数组结构,演示如下图;,2023030613463025c7e3631c51838aec904784e0b74b8f588b9c526,小傅哥 & threadLocal 数据结构,如上图是 ThreadLocal 存放数据的底层数据结构,包括知识点如下;,既然 ThreadLocal 是基于数组结构的拉链法存储,那就一定会有哈希的计算。但我们翻阅源码后,发现这个哈希计算与 HashMap 中的散列求数组下标计算的哈希方式不一样。如果你忘记了HashMap,可以翻阅文章《HashMap 源码分析,插入、查找》、《HashMap 扰动函数、负载因子》,当我们查看 ThreadLocal 执行设置元素时,有这么一段计算哈希值的代码;,看到这里你一定会有这样的疑问,这是什么方式计算哈希?这个数字怎么来的?,讲到这里,其实计算哈希的方式,绝不止是我们平常看到 String 获取哈希值的一种方式,还包括;除法散列法平方散列法斐波那契(Fibonacci)散列法随机数法等。,而 ThreadLocal 使用的就是 斐波那契(Fibonacci)散列法 + 拉链法存储数据到数组结构中。之所以使用斐波那契数列,是为了让数据更加散列,减少哈希碰撞。具体来自数学公式的计算求值,公式f(k) = ((k * 2654435769) >> X) << Y对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28,第二个问题,数字 0x61c88647,是怎么来的?,其实这是一个哈希值的黄金分割点,也就是 0.618,你还记得你学过的数学吗?计算方式如下;,既然,Josh Bloch 和 Doug Lea,两位老爷子选择使用斐波那契数列,计算哈希值。那一定有它的过人之处,也就是能更好的散列,减少哈希碰撞。,接下来我们按照源码中获取哈希值和计算下标的方式,把这部分代码提出出来做验证。,如上,源码部分采用的是 AtomicInteger,原子方法计算下标。我们不需要保证线程安全,只需要简单实现即可。另外 ThreadLocal 初始化数组长度是16,我们也初始化这个长度。,测试代码部分,采用的就是斐波那契数列,同时我们加入普通哈希算法进行比对散列效果。当然String 这个哈希并没有像 HashMap 中进行扰动,测试结果,发现没?,斐波那契散列的非常均匀,普通散列到15个以后已经开发生产碰撞。这也就是斐波那契散列的魅力,减少碰撞也就可以让数据存储的更加分散,获取数据的时间复杂度基本保持在O(1)。,new ThreadLocal<>(),初始化的过程也很简单,可以按照自己需要的泛型进行设置。但在 ThreadLocal 的源码中有一点非常重要,就是获取 threadLocal 的哈希值的获取,threadLocalHashCode。,如源码中,只要实例化一个 ThreadLocal ,就会获取一个相应的哈希值,则例我们做一个例子。,因为 threadLocalHashCode ,是一个私有属性,所以我们实例化后通过上面的方式进行获取哈希值。,这个值的获取,也就是计算 ThreadLocalMap,存储数据时,ThreadLocal 的数组下标。只要是这同一个对象,在setget时,就可以设置和获取对应的值。,new ThreadLocal<>().set("小傅哥");,设置元素的方法,也就这么一句代码。但设置元素的流程却涉及的比较多,在详细分析代码前,我们先来看一张设置元素的流程图,从图中先了解不同情况的流程之后再对比着学习源码。流程图如下;,2023030613461492798542851168beffe182dcfaf39c07d382ec508,小傅哥 & 设置元素流程图,乍一看可能感觉有点晕,我们从左往右看,分别有如下知识点; 0. 中间是 ThreadLocal 的数组结构,之后在设置元素时分为四种不同的情况,另外元素的插入是通过斐波那契散列计算下标值,进行存放的。,在有了上面的图解流程,再看代码部分就比较容易理解了,与之对应的内容包括,如下;,综上,就是元素存放的全部过程,整体结构的设计方式非常赞,极大的利用了散列效果,也把弱引用使用的非常6!,只要使用到数组结构,就一定会有扩容,在我们阅读设置元素时,有以上这么一块代码,判断是否扩容。,探测式清理和校验,resize() 扩容,以上,代码就是扩容的整体操作,具体包括如下步骤;,new ThreadLocal<>().get();,同样获取元素也就这么一句代码,如果没有分析源码之前,你能考虑到它在不同的数据结构下,获取元素时候都做了什么操作吗。我们先来看下图,分为如下种情况;,202303061346157417c1b88a8f3fc5f6b018e910632f0bc4cdd2934,小傅哥 & 获取元素图解,按照不同的数据元素存储情况,基本包括如下情况;,好了,这部分就是获取元素的源码部分,和我们图中列举的情况是一致的。expungeStaleEntry,是发现有 key == null 时,进行清理过期元素,并把后续位置的元素,前移。,探测式清理,是以当前遇到的 GC 元素开始,向后不断的清理。直到遇到 null 为止,才停止 rehash 计算Rehash until we encounter null。,expungeStaleEntry,以上,探测式清理在获取元素中使用到; new ThreadLocal<>().get() -> map.getEntry(this) -> getEntryAfterMiss(key, i, e) -> expungeStaleEntry(i),启发式清理,有这么一段注释,大概意思是;试探的扫描一些单元格,寻找过期元素,也就是被垃圾回收的元素。当添加新元素或删除另一个过时元素时,将调用此函数。它执行对数扫描次数,作为不扫描(快速但保留垃圾)和与元素数量成比例的扫描次数之间的平衡,这将找到所有垃圾,但会导致一些插入花费O(n)时间。,while 循环中不断的右移进行寻找需要被清理的过期元素,最终都会使用 expungeStaleEntry 进行处理,这里还包括元素的移位。

© 版权声明

相关文章