26

这个Map你肯定不知道,毕竟存在感确实太低了。

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIxNTQ4MzE1NA%3D%3D&%3Bmid=2247501221&%3Bidx=1&%3Bsn=ef7fa3c9618f852d5e4bb45742eb816b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

RfAZbin.png!mobile

从Dubbo的优雅停机说起

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

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

eYNria.png!mobile

很明显,这个地方最关键的地方是红框框起来的部分。

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

java.lang.Runtime#addShutdownHook

beUr2qf.png!mobile

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

你说 hooks 是这个什么玩意?

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

盲猜也知道:这个 hooks 肯定是一个 Map。那么这么多 Map 具体是哪个呢?

来看看答案:

beUr2qf.png!mobile

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

一时间居然想不起来这是个什么玩意了,只是觉得有点眼熟。

至于它是干啥的,有啥特性,那就更是摸不清楚了。

于是我去了解了一下,发现这玩意,有点意思。属于学了基本没啥卵用,但如果你知道,偶尔会出奇制胜的东西。

AfuEzq.jpg!mobile

有啥不一样

z6RnimB.png!mobile

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

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

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

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

先上个代码给大家看看:

QnmuYrr.png!mobile

先不说后半部分输出什么了。

前面的 hashMap 最终的输出结果你肯定知道吧。

由于多次 new String("why") 出来的字符串对象的 hashCode 是一样的。

所以,最终 hashMap 里面只会留下最后一个值。

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

疑问点是 identityHashMap 最终会输出什么呢?

来,看看结果:

MjqQbme.png!mobile

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

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

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

我们可以存多个相同的 key 到 map 里面了。

比如这样的:

Ifu2ui6.png!mobile

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

来,你先告诉我,hashMap 里面放了几个对象?一个还是三个?

什么,一个?

你出去,你个假粉丝!你自己看看是几个:

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

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

HashMap 才会是表现的和我们预期一样。

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

eyUfMfR.png!mobile

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

hashMap 只能看到 18 岁的 why。

identityHashMap 可以看到 16 到 18 岁的 why。

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

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

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

畅游源码-PUT

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

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

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

3aEzyea.png!mobile

key 放好了,然后 value 直接放到 i+1 的位置:

aeAJJr7.png!mobile

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

记住这个套路,我们先从 put 方法的源码入手:

java.util.IdentityHashMap#put

MBfemaJ.png!mobile

在标号为 ① 的地方,就是 hash 方法,入参是我们传入的对象和 table 的长度。

table 是个什么玩意呢?

eaeUBfb.png!mobile

是一个 Object 的数组。所以,我们知道了 identityHashMap 的数据结构它还是一个数组,而且看注释:这个 table 的长度必须是 2 的整数倍,也就是偶数。

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

JBn6JvJ.png!mobile

是的,看起来是 32。

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

mQ7JRfE.png!mobile

可以看到这个 table 数组里面什么东西都没有,也就根本不存在触发扩容什么的。

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

后来我在构造方法中找到了答案:

YnQniu2.png!mobile

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

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

73u2eqN.png!mobile

但是你转念一想。默认容量 32 是指的 key 的容量。而一个 key 对应一个 value。

key + value 总共不就是 64 的长度吗?

好了,我们接着看 hash 方法的具体实 现:

FnuIJz2.png!mobile

hash 方法只有两行。但是这两行都非常的关键。

先看第一个 System.identityHashCode,这个是什么东西?

看看 API 上的解释:

z6FzquZ.png!mobile

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

看两个示例代码:

AnyMBfi.png!mobile

注意 Person 对象是没有重写 hashCode 方法的。

程序的最终输出结果是这样的:

uQbE3iJ.png!mobile

我们分成三个部分去看,我们可以发现。

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

即使对象(String)重写了 hashCode 方法,对于不同的对象,hashCode 值是一样的,但是 identityHashCode 可能是不一样的。

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

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

比如在 identityHashMap 里面的使用就是一个正确的使用。至于错误的使用,我们稍后会讲。

经过前面的分析我们知道了:hash 方法中的第一行代码,对于 new 出来的相同对象的不同实例,不管是否重写 hashCode 方法,会产生不同的 identityHashCode。

可以说 System.identityHashCode 方法,是整个 identityHashMap 的基石。

然后再看这一行代码:

3AnUf2B.png!mobile

很多朋友第一眼看到位运算,心里就稍微有点抵触。

别这样,我带你分析一下,很简单的。

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

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

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

fUjIFjN.jpg!mobile

而 hash 方法就是计算 key 的位置。

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

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

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

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

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

h<<1 的最终结果肯定是一个偶数吧?

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

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

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

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

buMZjm.gif!mobile

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

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

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

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

经过前面的分析,我们知道了标号为 ① 的地方返回的 i 肯定是一个 0 到 len-1 之间的偶数:

MBfemaJ.png!mobile

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

标号为 ② 的地方是检查传进来的这个 key 是否在数组中已经存在了,也就是我们说的是否 hash 冲突。

如果没冲突,继续往下执行。

如果冲突了,且 value 值存在,就替换 value 值,然后返回。

如果冲突了,且 value 值不存在, i 值经过 nextKeyIndex 方法后也发生了变化。

下标 i 是怎么变化的呢?

假设我们来了一个 key=key2 的元素,经过 hash 计算后,对应数组下标为 2,但是该位置上已经有了一个 key1 ,那么就是发生了 hash 冲突:

3aayUrZ.png!mobile

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

代码中是这样的体现的:

NnIn6vQ.png!mobile

当 key2 的 identityHashCode 和 key1 一样,发生 hash 冲突之后,是这样存储的:

NFBvMbb.png!mobile

那势必会出现 i+2 的结果比 len 还长的情况:

7ZB7Zne.png!mobile

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

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

jMnAZ3b.png!mobile

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

qEjMV3n.png!mobile

怎么样,是不是很骚。

执行到编号为 ③ 的地方,就很清晰了:

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

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

36JBFfB.png!mobile

和我们画图的逻辑一致。

畅游源码-GET

接下来我们看看 get 方法:

YbYFb2J.png!mobile

标号为 ① 的地方,直接取到了对应的 key。

你注意这个地方, 用的是 == 来判断对象是否相等 ,hashMap 用的是 equals 。

标号为 ② 的地方,是没有对应的 key,直接返回 null。

走到标号为 ③ 的地方,代表这个 key 发生过 hash 冲突。那么接着找下一个偶数位下标的 key。

比如我们这里的 key2:

RNFJFvY.png!mobile

整个过程还是非常清晰的。学习的时候可以和 hashMap 的 get 方法进行对比学习。

你会发现,思想是一个思想,但是解决方案是完全不同的解决方案。

畅游源码-REMOVE

接着再看最后一个 remove 方法:

fqMFR3N.png!mobile

首先,标号为 ① 的地方,你想到了什么东西?

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

nMVV7bZ.png!mobile

是为了抛出 ConcurrentModificationException 服务的。

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

关于这个异常最经典的一个面试题就是: ArrayList 如果一边遍历,一边删除,会出现什么情况?

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

假粉丝,请你回去等通知吧。

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

执行完标号为 ② 的地方, remove 的操作也就完成了。

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

6NvYbqJ.jpg!mobile

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

NFBvMbb.png!mobile

如果这个时候我要移除 key=key1 的键值对,当标号为 ② 的地方执行完成后,是这个样子的:

fEFZvmY.png!mobile

发现问题了吗?

如果这个时候我来查询 key2,而 key2 经过 hash 方法后计算出来的 i 还是 2,而对应位置上的值是 null:

mqIFf2V.png!mobile

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

key2,啪,没了!

UJ3uey6.png!mobile

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

java.util.IdentityHashMap#closeDeletion

y2EVfqf.png!mobile

你看这个方法标号为 ① 的地方,自己都说了:

朋友,因为我们这个结构是一个圆,这个方法比较混乱。做好心理准备。

然后就是一个异常复杂的 if 判断。

这个我是看懂了,但是属于只可意会不可言传的那种,所以就不给大家分析了。大家有兴趣的自己去看看。

只要你抓准了它的存储机制和方法功能,理解起来应该不算很费劲。

qiqEJnv.jpg!mobile

再看标号为 ② 的地方,理解起来就很容易了,把之前由于 hash 冲突导致的位置偏移的数据,一个个的往前挪:

FzU3Ufa.png!mobile

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

先把 key1 从 i=2 的位置移走。然后把 i=4 的 key2 往前移动 2 位。

这样,下次来查询 key2 的时候,就能得到正确的返回了。

这里留下一个疑问,假设下面这个场景:

QnYFR3.png!mobile

key1 和 key2 是有 hash 冲突的,但是 key3 是正常的。

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

Uvqmam2.png!mobile

代码是怎么控制或者说怎么知道 key2 和 key1 是有冲突的,所以移走 key1 之后,需要把 key2 往前移动。 而 key 3 和 key2 是没有关系的,所以 key3 放着不动。

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

好了,到这里关于 identityHashMap 增删改查我们就分享完毕了。

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

就像传统功夫,都是点到为止。不像有的年轻人,不讲武德,希望能耗子尾汁...

aammemE.png!mobile

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

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

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

identityHashCode的错误使用

前面说了,IdentityHashMap 的核心点在于 System.identityHashCode 方法。

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

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

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

eEvmU3n.png!mobile

上面的源码是 2.7.8 版本。

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

然后调用了 invokers ,也就是 list 的 hashCode 方法。

因为一致性哈希的负载均衡的思想就是当服务发生了上下线之后,我们需要对哈希环进行调整。

如果服务没有发生上下线,那么是不需要进行哈希环调整的。

具体到这个 list 来说就是:

当 list 里面的元素发生了变化,那么说明有服务上下线的情况发生。

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

所以作者在这里还写了一个备注: 我们应该只注意 list 里面的元素就可以了。

言外之意就是我刚刚说的:装元素的 list 是否发生了变化,我是不关心的。

按照开源框架的尿性,这地方专门写了一行注释,说明这个地方曾经是有问题的。

那我们看看这个地方的提交记录:

zQVFZrr.png!mobile

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

提交的代码如下:

nyErUv6.png!mobile

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

后来修改为调用 list 的 hashCode 方法。

单单看着一行代码,我们就知道,之前的代码是关注 list 这个容器了,导致了某些 bug 的出现。

具体什么原因,我们可以看看这次提交对应的 pr:

也就是编号为 5429 的 issue:

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

JVvyy2R.png!mobile

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

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

INZBfa3.jpg!mobile

而且这个 bug 其实是非常好发现的,只要你把环境一搭,代码一跑,场景一模拟。是个必现的问题。

而产生这个 bug 的原因,可谓是蝴蝶效应。在离这段源码很远的,毫不相干的一次需求中,不知不觉的就影响到了这段代码。

而且连开发者自己都不知道,自己的修改会影响到一致性哈希负载均衡算法。所以,根本也就谈不上什么测试用例了。

如果你想更进一步了解这个 bug 的来龙去脉。可以看看这篇文章:

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

如果你想更进一步的了解 Dubbo 的负载均衡策略,那可以看看这篇文章:

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

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

你要是不喜欢下面的荒腔走板环节的话,也请记得拉到文章的最后。留言、点赞、在看、转发、赞赏,随便来一个就行。你要是都安排上,我也不介意。

荒腔走板

最近项目组接到了一个工期特别紧张的项目。

所以刚刚过去的周末我加了两天的班。周六晚上把流程走通之后,已经快是 22 点了。

之前预约了安装家电的师傅,刚好也是周六。

所以只有女朋友一个人去家那边,边打扫卫生,边等着安装师傅。

安装师傅全部弄好之后也是 19 点之后了。

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

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

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

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

但是在小区门口刷门禁卡的时候,我一抬头,门口写着:欢迎回家。

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

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

因为这个家,真的是属于自己的家,用自己一手一脚挣出来的钱堆出来的。

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

之前我从来没有这样的感觉过,这是一种非常神奇的感觉。

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

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

我们一起站在空荡荡的客厅中间。

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

哦,对了,文章的第一张照片就是我坐在家里的小阳台上摆拍的。哎,现在大家都慢慢的使用自己拍的照片做封面图片了,我也得跟着"卷起来"呀。

最后说一句(求关注)

好了,看到了这里安排个 “一键三连” (转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。

BBbEbe.gif!mobile

才疏学浅,难免会有纰漏,如 果你发现了错误的地方, 可以在留言区提出来 ,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

NBrayeE.png!mobile

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

还有, 欢迎关注我呀。

vUzQVnZ.png!mobile

往期回顾

留言区

转发、点赞、在看、一 键三连。

别白嫖我,好吗?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK