这个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 的负载均衡策略,那可以看看这篇文章:
好了,那么这次的文章就到这里啦。给大家分享了一个冷门的、"学了没多大卵用" 的 IdentityHashMap。
你要是不喜欢下面的荒腔走板环节的话,也请记得拉到文章的最后。留言、点赞、在看、转发、赞赏,随便来一个就行。你要是都安排上,我也不介意。
荒腔走板
最近项目组接到了一个工期特别紧张的项目。
所以刚刚过去的周末我加了两天的班。周六晚上把流程走通之后,已经快是 22 点了。
之前预约了安装家电的师傅,刚好也是周六。
所以只有女朋友一个人去家那边,边打扫卫生,边等着安装师傅。
安装师傅全部弄好之后也是 19 点之后了。
因为我从公司到家特别的近。女朋友觉得我也差不多该下班了,于是决定就在家里等我,然后一起从家里回到租住的小区。
结果一等就是 2 个多小时。
我下班之后,马上打车到小区。
下午没有吃饭,工作也比较劳累,坐在车上,一阵疲倦的感觉袭来。
但是在小区门口刷门禁卡的时候,我一抬头,门口写着:欢迎回家。
那一刻,我突然觉得好暖啊,甚至还有一丝丝的感动。
走在小区的路上,感觉一切都是这么的可爱。
因为这个家,真的是属于自己的家,用自己一手一脚挣出来的钱堆出来的。
此时此刻,家里还有一个人,开着灯,在等着我回家。
之前我从来没有这样的感觉过,这是一种非常神奇的感觉。
到家之后,由于家具还没有准备好,我看到女朋友在地上铺着一个泡沫垫子,坐在上面,靠在墙上,通过手机看着综艺。
她起来抱了抱我,说:你终于回来啦。今天的事可真是多。
我们一起站在空荡荡的客厅中间。
那一刻,家的含义,家的感觉,从来没有这么具体过。
最后说一句(求关注)
才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。
欢迎关注我呀。

这个Map你肯定不知道,毕竟存在感确实太低了。的更多相关文章
- Linux宕机最安全的重启方法(你肯定不知道)
Linux 内核虽然号称“不死族”,几乎不会崩溃或者死机,但是特殊情况下,还是有一定几率会宕机的.因为 Linux 广泛用于生产环境,所以每一次宕机都会引起相当大的损失.本文介绍在它死机至后,一种温柔 ...
- [肯定不知道]PeopleSoft中PSADMIN你不知道的秘密
PeopleSoft psadmin工具是用于管理PS App server,process scheduler 和 web server节点的.可以使用一些设置菜单选项来管理或配置上面提到的任何组件 ...
- PyCharm设置Python版本,你肯定不知道!
前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:智小星 PyCharm默认会使用虚拟的Python解释器,即使 ...
- CSS的三种样式,有一种你肯定不知道
前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者LFuser 正文 新手注意:如果你学习遇到问题找不到人解答,可以点我进裙 ...
- 不是我吹,Lambda这个坑你肯定不知道!
上周有小伙伴反馈zk连接很慢.整理出zk连接的关键逻辑如下: public class ClientZkAgent { //单例模式 private static final ClientZk ...
- 给IDEA道个歉,这不是它的BUG,而是反编译插件的BUG。
你好呀,我是歪歪. 上周我不是发了<我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!>这篇文章吗. 主要描述了在 IDEA 里面反编译后的 class 文件中有这样的代码片段: 很明 ...
- 【转】IT行业岗位以及发展方向
以下转自https://blog.csdn.net/qq_23994787/article/details/79847270 职业生涯规划的意义 1.以既有的成就为基础,确立人生的方向,提供奋斗的策略 ...
- 给扔物线 HenCoder Plus 学员的一次分享文字版
半个月前,和我的终极技术目标扔物线朱凯一拍即合,到了他所开展的 HenCoder Plus 课程给大家分享了 1 个多小时的「模拟面试」心得,也顺便听了几次凯哥的课程,感觉真的挺用心的.自己也希望能一 ...
- 我经历的IT公司面试及离职感受(转)
毕业后几年一直待在广州,觉得这是一个比较生活化及务实的城市,其互联网公司和相应的投融资环境都不如北深上活跃,大大小小的面试也有几十个,有点规模的公司应该都面试过了,面试一般会见到主力技术人员,技术主管 ...
随机推荐
- 【组合计数】visit
题目大意 从 \((0,0)\) 开始,每次只可走上下左右一个单位长度,可走重复路,求第 \(T\) 步正好走到 \((n,m)\) 的方案数. 答案要求对 \(MOD\) 取模,\(MOD\) 保证 ...
- centos8平台mysql日志的按天切分
一,mysqladmin使用flush-logs的文档: mysql8官网上面针对mysqladmin的文档地址 https://dev.mysql.com/doc/refman/8.0/en/mys ...
- js改变,设置table单双行颜色,jquery改变,设置table单双行颜色
1.js实现单双行以不同颜色显示 $(document).ready(function () { var color = "#ffeab3"; $("#GvList tr ...
- QT/C++插件式框架、利用智能指针管理内存空间的实现、动态加载动态库文件
QT.C++插件式框架.主要原理还是 动态库的动态加载. dlopen()函数.下面为动态加载拿到Plugininstance对应指针.void**pp=(void**)dlsym(handle,&q ...
- abp(net core)+easyui+efcore实现仓储管理系统——出库管理之三(五十二)
abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...
- window 属性:自定义元素(custom elements)
概述 Web Components 标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为 custom elements(自定义标签),而往常,开发者不得不写一大堆冗长.深层嵌套的标 ...
- 从Linux源码看TIME_WAIT状态的持续时间
从Linux源码看TIME_WAIT状态的持续时间 前言 笔者一直以为在Linux下TIME_WAIT状态的Socket持续状态是60s左右.线上实际却存在TIME_WAIT超过100s的Socket ...
- oracle索引失效情况(转)
1.隐式转换导致索引失效.这一点应当引起重视.也是开发中经常会犯的错误. 由于表的字段tu_mdn定义为varchar2(20),但在查询时把该字段作为number类型以where条件传给Orac ...
- C语言,产生一组数字,并将其写入txt文档中
#include<stdio.h> /*产生一组连续的数字,并将其写到txt文档中*/ /*说明:本程序在在win10 系统64位下用Dev-C++ 5.11版本编译器编译的*/int m ...
- 项目实战:流水线图像显示控件(列刷新、1ms一次、缩放、拽拖、拽拖预览、性能优化、支持OpenGL GPU加速)
需求 流水线图像扫描采集控件(带模拟数据测试)性能需求 1.需至少满足可1ms接收一次列数据,而不丢包(接收后可不必立马显示) 2.图片刷新率可达30HZ:限制需求 1.图片高度最小只能 ...