ThreadLocal源码阅读三:散列算法,魔数 0x61c88647 带来的疑问与思考

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

背景

  1. 推荐阅读魔数的基础知识。魔数 0x61c88647学习

  2. 疑问:用这个魔数来解决hash碰撞,可行吗?

  3. 疑问:环形数组设计对产生hash碰撞有什么影响吗?如果有影响是积极的还是消极的?

过程

  • 魔数工作过程及其特性
  1. 测试代码 在这里插入图片描述

  2. 结果 在这里插入图片描述

  3. 结论

    需要注意计算方式: hashCode & (length - 1)

    长度是16,其中0-15,散列的结果是,均匀的、完美的、没有重复的。 而16,17发现从头开始循环。

  • 两个ThreadLocal实例是怎么运作的
  1. 代码

在这里插入图片描述

  1. 说明

    上图我们展示了两个ThreadLocal实例,分别是num和num2。其中,初始值,我们都设定为0。

  2. 思考

    我们都是采用new ThreadLocal()的方式创建的num和num2,这两个实例肯定是不一样的,这是毋庸置疑的。如果是这样的话,那么num的属性threadLocalHashCode的值是0,而num2的属性threadLocalHashCode的值也是0。这是我们最常规和满怀信心的结论。可事实真的是这样吗?

  3. 验证

在这里插入图片描述 在这里插入图片描述

  1. 验证的结果

    结果告诉我们,并不是按照我们预期所想的。而上图的运算结果,也是ThreadLocal源码真实产生的数字。可是为什么会这样呢?

    当我们的jvm主动使用ThreadLocal类的时候,也就是执行new ThreadLocal(),jvm加载ThreadLocal类,链接,初始化。ThreadLocal类被加载到jdk1.8元数据空间中。当执行初始化操作的时候,这个num的属性threadLocalHashCode就被设置成了0,并且这个nextHashCode中的AtomicInteger的值就被设定为1640531527了。

    当我们第二次执行new ThreadLocal()的时候,jvm就不需要加载ThreadLocal类,因为元数据空间已经有了。这个时候,执行初始化操作。这个num2的属性threadLocalHashCode就被设置成了1640531527,而nextHashCode中的AtomicInteger的值就被设定为-1013904242。

  2. 这里解决的问题是,虽然每次都是new ThreadLocal(),但是计算的值都不是从初始值0开始的。而是每次都增加1640531527。

  • 用这个魔数来解决hash碰撞,可行吗?

    使用算法threadLocalHashCode & (length - 1)。完全可行的。因为从做的测试来说,是均匀完美散列的。完美散列是指16长度,然后产生0-15中的每个数字。

    确实可以满足解决hash碰撞。但是,作者把ThreadLocal设计成了弱引用。在实际使用过程中,可能存在有的ThreadLocal已经完成了职能,被GC掉了,这个时候,entry[]中的某个具体的entry就不在占用空间了。而我们知道魔数的特性是循环的,那么一定会产生hash碰撞的。

    因此,作者这样的实现是一定会存在hash碰撞的。那又是怎么解决hash碰撞呢?线性探测法。只要理解怎么解决hash碰撞的原理即可,它叫什么概念,不重要,但是我们又需要跟人讨论,所以,还是得理解概念。

  • 那作者为什么又要使用环形数组呢?

    因为环形数组可以节约内存空间,重复利用已经分配好的空间。这里的本质原因是希望减少扩容的次数。因为每次扩容,都需要分配原来的2倍空间,把旧的entry[]中的内容完全拷贝到新的entry[],需要一些计算过程,会耗性能。如果能够减少扩容的频次,那么性能就会很高。这里当然是考虑平均性能,因为发生扩容的概念是小的。

    如果没有环形数组,扩容的频次一定会增加的,而且内存占用会更多。

    如果使用了环形数组,不但节约了内存空间,还减少了扩容的次数。

    作者的思考和设计是非常有功底的。

  • 环形数组会增大hash碰撞的概率吗?

    是。

    假如我们的GC都是非常及时的,而我们的魔数相关算法又是循环的。所以一定会产生碰撞的,而我们的存储空间又是环状的,魔数和环形数组一起使用确实会增大hash碰撞的概率的。这是一种推测,在科学领域,只要存在一个可能的反面,那么这样的推测就是合理的。

    当然,也有可能我们GC不及时,那么一些无用的entry无法被及时清除掉,而又占用了table的空间,这个时候,就可能会发生扩容,而扩容一旦发生,那么发生的hash碰撞的概率就会减少。也就是length越长发生的概率就会越低。

小结

  • 魔数的相关算法和hash碰撞相互之间的关系剖析。

  • 魔数的相关算法已经完美散列了,但是作者使用了一种保护措施,担心内存泄漏,那些不在使用的ThreadLocal实例未被及时GC掉,于是设计ThreadLocal为弱引用。解决了一个问题,就会引入另个问题,魔数的散列与被清除的entry,会出现重复的情况,所以作者需要解决hash碰撞。

  • 大师之所以是大师,是因为他有权衡的智慧。他认为节约空间并且减少扩容频次带来的性能提升和资源利用的好处是大于增加hash碰撞概率缺点的。因为,无论怎样,作者的代码设计一定是需要解决hash碰撞的。也就是说,作者无论是否使用环形数组,都需要写解决hash碰撞的逻辑。

  • 考虑实际情况对ThreadLocal的使用,很少遇见使用了多个ThreadLocal实例的情况,如果一定需要使用多个ThreaddLocal实例的时候,我们完全可以定义一个引用对象,让这个对象持有相应的变量即可。也许作者对这个ThreadLocal有更加深层次的考虑。希望这样的设计能够满足各种各样使用场景。记得,某个大学是由某种树木修建起来的,设计者考虑到未来几百年后,可能会检修大学,于是提前栽种好了相应的树木。

  • 作者想节约内存空间和减少扩容频次,于是设计了环形数组。这个环形数组带来好处,却加大了hash碰撞的概率。人世间的东西,就是这样的。一个事物他浑然天成。他享受着自己的特长带来的好处,却也承受着自己的缺陷带来的坏处。无论生活中的任何人还是物,我们选择接受,那么就需要有这样的心态。享受好的,也包容不好的。这是一种事实,一种无法改变的事实。唯一的办法是什么?接受。如果没有好的呢?不接受。

  • 突然想到,在《斗罗大陆》中,超级魂斗罗唐昊说:“锻造凡铁,刚开始锻造,用力不能过大,不然凡铁会断裂,因为凡铁带大量杂质。锻造到一定程度,凡铁的密度变大,能够承受更大的力度,因此需要加大力度。打铁,不是使用的力度越大越好,而是掌握好什么时候用什么样的力度”。这句话在说明什么?行成于思

猜你喜欢

转载自juejin.im/post/7082900162665775135