乐趣区

关于java:这个Map你肯定不知道毕竟存在感确实太低了

这是 why 哥的第 75 篇原创文章

从 Dubbo 的优雅停机说起

好吧,其实本文并不是讲 Dubbo 的优雅停机的。

只是我在 Dubbo 停机办法 DubboShutdownHook 类中,看到了这样的一段代码:

很显著,这个中央最要害的中央是红框框起来的局部。

而这个 addShutdownHook 其实是 JDK 的办法:

java.lang.Runtime#addShutdownHook

最终,把传进来的 hook 放到了 hooks 外面。

你说 hooks 是这个什么玩意?

这个 hooks 调用的是 put 办法,外面放了一个 key,一个 value。

盲猜也晓得:这个 hooks 必定是一个 Map。那么这么多 Map 具体是哪个呢?

来看看答案:

.jpg)

说真的,第一次看到这个 IdentityHashMap 的时候,我都有点愣住了。

一时间竟然想不起来这是个什么玩意了,只是感觉有点眼生。

至于它是干啥的,有啥个性,那就更是摸不分明了。

于是我去理解了一下,发现这玩意,有点意思。 属于学了根本没啥卵用,但如果你晓得,偶然会出奇制胜的货色。

有啥不一样

IdentityHashMap 也是 Map 家族中的一员。只是他的存在感也太低了,很多人都不晓得还有这么一个玩意。

甚至感觉它是一个第三方包外面引进的类,没想到竟然是一个亲儿子。

说到 Map 家族,大家最相熟的也就是 HashMap 了。

那么这个 IdentityHashMap 和 HashMap 有什么区别呢?

先上个代码给大家看看:

先不说后半局部输入什么了。

后面的 hashMap 最终的输入后果你必定晓得吧。

因为屡次 new String(“why”) 进去的字符串对象的 hashCode 是一样的。

所以,最终 hashMap 外面只会留下最初一个值。

这个点,之前的这《why 哥悄悄的给你说几个 HashCode 的破事》篇文章中曾经讲过了。置信不须要我再次补充。

疑难点是 identityHashMap 最终会输入什么呢?

来,看看后果:

OMG,什么鬼?identityHashMap 外面把三个值都存下来啦?这么神奇的吗?怎么做到的?

先不去想它怎么实现的,咱们就把它当个黑盒应用。

那么它在给咱们传递什么样的信息?

咱们能够存多个雷同的 key 到 map 外面了。

比方这样的:

我把后面的示例代码的中的 String 换成 Person 对象。

来,你先通知我,hashMap 外面放了几个对象?一个还是三个?

什么,一个?

你进来,你个假粉丝!你本人看看是几个:

之前的文章外面说过了,hashMap 外面,如果咱们要用对象当做 key。咱们应该怎么办?

必!须!要! 重写对象的 hashCode 和 equals 办法。

HashMap 才会是体现的和咱们预期一样。

所以,当咱们重写了对象的 hashCode 和 equals 办法后,运行后果是这样的:

这两个容器的执行后果,含意是不一样的。

hashMap 只能看到 18 岁的 why。

identityHashMap 能够看到 16 到 18 岁的 why。

总之, 你是否重写了对象的 hashCode 和 equals 办法,identityHashMap 都不关怀。

那么 identityHashMap 是怎么实现这个成果的呢?

咱们去源码中寻找一下答案。

畅游源码 -PUT

在讲源码之前,我先把 identityHashMap 的存储套路给你说一下,你看源码的时候就轻松多了。

不管怎么它还是一个 Map,那么必然就有对应的 hash 办法。

对于 identityHashMap 而言,通过 hash 办法,计算出 key 的下标为 2:

key 放好了,而后 value 间接放到 i+1 的地位:

key 的下一个地位,就是这个 key 的 value 值。 这就是 identityHashMap 的存储套路。它的数据结构不是数组加链表,就完完全全是一个数组。​

记住这个套路,咱们先从 put 办法的源码动手:

java.util.IdentityHashMap#put

在标号为 ① 的中央,就是 hash 办法,入参是咱们传入的对象和 table 的长度。

table 是个什么玩意呢?

是一个 Object 的数组。所以,咱们晓得了 identityHashMap 的数据结构它还是一个数组,而且看正文:这个 table 的长度必须是 2 的整数倍,也就是偶数。

那么数组的默认长度是多少呢:

是的,看起来是 32。

然而当我对程序进行调试的时候我发现,这个 len 竟然是 64:

能够看到这个 table 数组外面什么货色都没有,也就基本不存在触发扩容什么的。

为什么长度是 64 呢?说好的 32 呢?

起初我在构造方法中找到了答案:

卧槽,说好的默认容量 32,你初始化的时候间接翻倍了?

这是什么行为?年轻人,你这代码,不讲武德啊!

然而你转念一想。默认容量 32 是指的 key 的​容量。而一个 key 对应一个 value。key + value 总共不就是 64 的长度吗?

好了,咱们接着看 hash 办法的具体实现:

hash 办法只有两行。然而这两行都十分的要害。

先看第一个 System.identityHashCode,这个是什么货色?

看看 API 上的解释:

就是对于一个对象,不论你有没有重写 hashCode 办法,该办法返回的值都是不会变动的。

看两个示例代码:

留神 Person 对象是没有重写 hashCode 办法的。

程序的最终输入后果是这样的:

咱们分成三个局部去看,咱们能够发现。

当对象(Person)没有重写 hashCode 办法的时候,他们的 hashCode 和 identityHashCode 是一样的。

即便对象(String)重写了 hashCode 办法,对于不同的对象,hashCode 值是一样的,然而 identityHashCode 可能是不一样的。

留神是“可能不一样”。因为 identityHashCode 的底层逻辑是基于一个伪随机数生成的。

这个个性特地有用,然而也别乱用。用错了,就是一个 bug。

比方在 identityHashMap 外面的应用就是一个正确的应用。至于谬误的应用,咱们稍后会讲。

通过后面的剖析咱们晓得了:hash 办法中的第一行代码,对于 new 进去的雷同对象的不同实例,不论是否重写 hashCode 办法,会产生不同的 identityHashCode。

能够说 System.identityHashCode 办法,是整个 identityHashMap 的基石。

而后再看这一行代码:

很多敌人第一眼看到位运算,心里就略微有点冲突。

别这样,我带你剖析一下,很简略的。

首先,我后面画图示意了 identityHashMap 的存储套路,说了:key 的下一个地位就是这个 key 的 value。

那么 key 的地位肯定要是一个偶数。

这一点能不能跟上?跟不上你就多想想再往下看。

而 hash 办法就是计算 key 的地位。

所以,该办法的返回值肯定是一个偶数。

这周密的逻辑,是不是无懈可击。

假如 length 为 64 的话,那么这一行代码的目标是为了生成一个 0 到 63 之间的偶数。

0 到 63 之间的数,是 &(length-1) 保障的。这个没啥说的。

那么为什么肯定会生成一个偶数呢?

h<<1 的最终后果必定是一个偶数吧?

h<<8 的最终后果必定也是一个偶数吧?

那么偶数减去偶数是一个什么数?

什么,你问我会不会溢出?

你管它溢出不溢出,就算它变成正数了,变成 0 了,它也是一个偶数呀!

偶数的二进制的最初一位是不是 0?

length-1 这个数的二进制最初一位不是 0 就是 1,对不对?

0 & 上 0 或者 1,是不是还是 0?

那不就对了。所以,最终后果必定是一个偶数的。

通过后面的剖析,咱们晓得了标号为 ① 的中央返回的 i 必定是一个 0 到 len-1 之间的偶数:

返回的这个偶数 i,在标号为 ② 和 ③ 的中央都有用到。

标号为 ② 的中央是查看传进来的这个 key 是否在数组中曾经存在了,也就是咱们说的是否 hash 抵触。

如果没抵触,持续往下执行。

如果抵触了,且 value 值存在,就替换 value 值,而后返回。

如果抵触了,且 value 值不存在,i 值通过 nextKeyIndex 办法后也产生了变动。

下标 i 是怎么变动的呢?

假如咱们来了一个 key=key2 的元素,通过 hash 计算后,对应数组下标为 2,然而该地位上曾经有了一个 key1,那么就是产生了 hash 抵触:

.jpg)

发生冲突,i+2,也就是找到下一个偶数下标。

代码中是这样的体现的:

当 key2 的 identityHashCode 和 key1 一样,产生 hash 抵触之后,是这样存储的:

那势必会呈现 i+2 的后果比 len 还长的状况:

你发现源码是怎么解决这个问题的吗?

这个 nextkeyIndex 这个办法首尾相接,它是一个圆啊:

这种状况,这个圆,画图是怎么体现的呢?

怎么样,是不是很骚。

执行到编号为 ③ 的中央,就很清晰了:

key 是放在 tab[i] 的地位的。

value 是放在 tab[i+1] 的地位的。

和咱们画图的逻辑统一。

畅游源码 -GET

接下来咱们看看 get 办法:

标号为 ① 的中央,间接取到了对应的 key。

你留神这个中央, 用的是 == 来判断对象是否相等 ,hashMap 用的是 equals。

标号为 ② 的中央,是没有对应的 key,间接返回 null。

走到标号为 ③ 的中央,代表这个 key 产生过 hash 抵触。那么接着找下一个偶数位下标的 key。

比方咱们这里的 key2:

整个过程还是十分清晰的。学习的时候能够和 hashMap 的 get 办法进行比照学习。

你会发现,思维是一个思维,然而解决方案是齐全不同的解决方案。

畅游源码 -REMOVE

接着再看最初一个 remove 办法:

首先,标号为 ① 的中央,你想到了什么货色?

我看到这个 modCount 可太亲切了。围绕着这个玩意,我前前后后大略写了有 3w 多字的文章吧:

是为了抛出 ConcurrentModificationException 服务的。

这里体现的是 fast-fail 的思维。

对于这个异样最经典的一个面试题就是:ArrayList 如果一边遍历,一边删除,会呈现什么状况?

什么?你不会?我也不答复了。

假粉丝,请你回去等告诉吧。

标号为 ② 的中央,把 i 和 i+1 的地位都置为 null。也就是把 key 和对应的 value 都置为 null。

执行完标号为 ② 的中央,remove 的操作也就实现了。

那么按理来说办法就应该完结了。对吗?

你想一想我之前的这个图片:

如果这个时候我要移除 key=key1 的键值对,当标号为 ② 的中央执行实现后,是这个样子的:

发现问题了吗?

如果这个时候我来查问 key2,而 key2 通过 hash 办法后计算出来的 i 还是 2,而对应地位上的值是 null:

这个时候你通知我 key2 查不到,返回一个 null 给我?

key2,啪,没了!

所以,标号为 ③ 的中央就是为了解决这个问题的。

java.util.IdentityHashMap#closeDeletion

你看这个办法标号为 ① 的中央,本人都说了:

敌人,因为咱们这个构造是一个圆,这个办法比拟凌乱。做好心理准备。

而后就是一个异样简单的 if 判断。

这个我是看懂了,然而属于只可意会不可言传的那种,所以就不给大家剖析了。大家有趣味的本人去看看。

只有你抓准了它的存储机制和办法性能,了解起来应该不算很吃力。

再看标号为 ② 的中央,了解起来就很容易了,把之前因为 hash 抵触导致的地位偏移的数据,一个个的往前挪:

意思就是下面图片的意思。

先把 key1 从 i=2 的地位移走。而后把 i=4 的 key2 往前挪动 2 位。

这样,下次来查问 key2 的时候,就能失去正确的返回了。

这里留下一个疑难,假如上面这个场景:

key1 和 key2 是有 hash 抵触的,然而 key3 是失常的​。

那么移除掉 key1 之后的图应该是这样的:

代码是怎么管制或者说怎么晓得 key2 和 key1 是有抵触的,所以移走 key1 之后,须要把 key2 往前挪动。而 key3 和 key2 是没有关系的,所以 key3 放着不动。

答案其实就藏在 closeDeletion 办法的源码外面,就看你有没有彻底了解这个办法了。​

好了,到这里对于 identityHashMap 增删改查咱们就分享结束了。

老规矩,源码导读,点到为止。

就像传统功夫,都是点到为止。年轻人,不讲武德,耗子尾汁 …

马老师可真是我最近一段时间的高兴源泉啊。

咦,偏了偏了,说编程呢,怎么说到马老师那边去了。

难道我不经意间发现了:万物皆可马保国定律?

identityHashCode 的谬误应用

后面说了,IdentityHashMap 的外围点在于 System.identityHashCode 办法。

说到这个 identityHashCode 我又想到了已经在 Dubbo 中的看到的一段源码。

位于一致性哈希负载平衡算法中:

org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance#doSelect

下面的源码是 2.7.8 版本。

假如有五个可用的服务提供者,这里的 invokers 汇合外面装的就是一个个服务提供者。

而后调用了 invokers,也就是 list 的 hashCode 办法。

因为一致性哈希的负载平衡的思维就是当服务产生了高低线之后,咱们须要对哈希环进行调整。

如果服务没有产生高低线,那么是不须要进行哈希环调整的。

具体到这个 list 来说就是:

当 list 外面的元素产生了变动,那么阐明有服务高低线的状况产生。

至于你装元素的 list 是否和原来的不一样,那我是不关怀的。

所以作者在这里还写了一个备注: 咱们应该只留神 list 外面的元素就能够了。

话中有话就是我刚刚说的:装元素的 list 是否产生了变动,我是不关怀的。

依照开源框架的尿性,这中央专门写了一行正文,阐明这个中央已经是有问题的。

那咱们看看这个中央的提交记录:

果然,在 2019 年 12 月 11 日,有人提交了代码。

提交的代码如下:

你看,原来的代码是 System.identityHashCode 办法。

起初批改为调用 list 的 hashCode 办法。

单单看着一行代码,咱们就晓得,之前的代码是关注 list 这个容器了,导致了某些 bug 的呈现。

具体什么起因,咱们能够看看这次提交对应的 pr:

也就是编号为 5429 的 issue:

https://github.com/apache/dubbo/issues/5429

哎呀,我去,这谁啊?看着眼生啊?这不就是 why 哥吗?这不是巧了吗,这不是?

是的,这个 bug 就是我发现并提出的对应的 issue。

而且这个 bug 其实是十分好发现的,只有你把环境一搭,代码一跑,场景一模仿。是个必现的问题。

而产生这个 bug 的起因,堪称是蝴蝶效应。在离这段源码很远的,毫不相干的一次需要中,人不知; 鬼不觉的就影响到了这段代码。

而且连开发者本人都不晓得,本人的批改会影响到一致性哈希负载平衡算法。所以,基本也就谈不上什么测试用例了。

如果你想更进一步理解这个 bug 的前因后果。能够看看这篇文章:

《够强!一行代码就修复了我提的 Dubbo 的 Bug》

如果你想更进一步的理解 Dubbo 的负载平衡策略,那能够看看这篇文章:

《吐血输入:2 万字长文带你细细盘点五种负载平衡策略。》

好了,那么这次的文章就到这里啦。给大家分享了一个冷门的、” 学了没多大卵用 ” 的 IdentityHashMap。

你要是不喜爱上面的荒腔走板环节的话,也请记得拉到文章的最初。留言、点赞、在看、转发、赞叹,轻易来一个就行。你要是都安顿上,我也不介意。

荒腔走板

最近项目组接到了一个工期特地缓和的我的项目。

所以刚刚过来的周末我加了两天的班。周六早晨把流程走通之后,曾经快是 22 点了。

之前预约了装置家电的徒弟,刚好也是周六。

所以只有女朋友一个人去家那边,边打扫卫生,边等着装置徒弟。

装置徒弟全副弄好之后也是 19 点之后了。

因为我从公司到家特地的近。女朋友感觉我也差不多该上班了,于是决定就在家里等我,而后一起从家里回到租住的小区。

后果一等就是 2 个多小时。

我上班之后,马上打车到小区。

下午没有吃饭,工作也比拟操劳,坐在车上,一阵困倦的感觉袭来。

然而在小区门口刷门禁卡的时候,我一低头,门口写着:欢送回家。

那一刻,我忽然感觉好暖啊,甚至还有一丝丝的打动。

走在小区的路上,感觉一切都是这么的可恶。

因为这个家,真的是属于本人的家,用本人一手一脚挣进去的钱堆进去的。

此时此刻,家里还有一个人,开着灯,在等着我回家。

之前我素来没有这样的感觉过,这是一种十分神奇的感觉。

到家之后,因为家具还没有筹备好,我看到女朋友在地上铺着一个泡沫垫子,坐在下面,靠在墙上,通过手机看着综艺。

她起来抱了抱我,说:你终于回来啦。明天的事可真是多。

咱们一起站在空荡荡的客厅两头。

那一刻,家的含意,家的感觉,素来没有这么具体过。

最初说一句 (求关注)

满腹经纶,难免会有纰漏,如果你发现了谬误的中央,能够在留言区提出来,我对其加以批改。感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

我是 why,一个被代码耽搁的文学创作者,不是大佬,然而喜爱分享,是一个又暖又有料的四川好男人。

欢送关注我呀。

退出移动版