热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

hashhashcode变化_你所不知道的HashCode

引言这两天有个学弟问过我这个问题:对象的hashCode到底是怎么实现的?在深挖之前,我可能只能说:如果没有被重载ÿ

引言

这两天有个学弟问过我这个问题:对象的 hashCode 到底是怎么实现的?
在深挖之前,我可能只能说:如果没有被重载,代表的是对象的地址通过某种 hash 算法计算后在 hash 表中的位置。
回答后,仔细一想,不对呀,这个 hash 值具体是怎么计算的,我终究还是没有答到点上,而是绕开话题,回答了含义。
脑壳一热,忽然想起去年虐我的阿里面试题,hashCode 是怎么得到的呢?

一、问题定义

hashCode 真的只是通过地址计算的吗?如果对象地址变化了,比如经历的 GC,hashCode 是不是也跟着变了呢?如果此时刚好在进行锁升级,对于 hashCode 的计算会有影响吗?多线程的情况下会不会生成一样的 hashCode 呢?具体通过什么样的 hash 算法得到的呢?相比之下,我真的是太皮毛了~

首先看下一个简单的实现类,这里先别使用 lombok 注解,原因后文会解释:

public class Student {

private int no;
private String name;

public void setNo(int no) {
this.no = no;
}

public void setName(String name) {
this.name = name;
}

public static void main(String[] args) {
Student student1=new Student();
student1.setName("张三");
student1.setNo(12);
System.out.println(student1.hashCode());
}
}

多次运行后,可以大胆假设 hashCode 的计算是稳定的。只要对象的引用不变,每次运行都是相同的结果,所以网上说使用随机数计算的回答,这个先打一个问号。..

大家可能印象比较深刻,当你打开源码时,会发现 native 修饰的方法会挡住你的去路。C++ 实现的方法难道就该让我们止步了吗?这次打算死磕到底。

二、源码揭秘

2.1 Object.hashCode () 注释解读

简单归纳一下 JDK 团队的注释:

  • hashCode 表示对象在 hash 表中的位置,对于同一个对象来说,多次调用,返回相同的 hashCode。

  • 如果 Object.equal () 相等,Object.hashCode () 也必然相等。重写时也建议保证此特性。

  • 如果 Object.equal () 相等,这并不要求 Object.hashCode () 也返回不同值。如果真出现这种情况,最好优化代码,充分利用 hash 表的性能。

2.2 hashCode 生成源码

下面是 C++ 对应的实现,这里拷贝一下网上其他大佬发的 hashCode 实现核心源码:

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG, // so it's possible for two threads to race and generate the same RNG. // On MP system we'll have lots of RW access to a global, so the // mechanism induces lots of coherency traffic. value = os::random() ;
} else
if (hashCode == 1) {
// This variation has the property of being stable (idempotent) // between STW operations. This can be useful in some of the 1-0 // synchronization schemes. intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing } else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = intptr_t(obj) ;
} else {
// Marsaglia's xor-shift scheme with thread-specific state // This is probably the best overall implementation -- we'll // likely make this the default in future releases. unsigned t = Self->_hashStateX ;
t ^&#61; (t <<11) ;
Self->_hashStateX &#61; Self->_hashStateY ;
Self->_hashStateY &#61; Self->_hashStateZ ;
Self->_hashStateZ &#61; Self->_hashStateW ;
unsigned v &#61; Self->_hashStateW ;
v &#61; (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW &#61; v ;
value &#61; v ;
}

value &&#61; markOopDesc::hash_mask;
if (value &#61;&#61; 0) value &#61; 0xBAD ;
assert (value !&#61; markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}

源码中的 hashCode 其实就是 JVM 启动的一个参数&#xff0c;每一个分支对应一个生成策略。通过 -XX:hashCode&#xff0c;可以任意切换 hashCode 的生成策略。
首先解释一下入参 oop obj 就是对象的逻辑地址。所以与地址相关的生成策略有两条&#xff0c;在 hashCode 等于 1 或 4 的时候。

  • hashCode&#61;&#61;1&#xff1a;这种方式具有幂等的性质&#xff0c;在 STW(stop-the-world)操作中&#xff0c;这种策略通常用于同步方案中。利用对象地址计算&#xff0c;使用不经常更新的随机数参与运算。

  • hashCode&#61;&#61;4&#xff1a;与创建对象的内存位置有关&#xff0c;原样输出。

其他情况&#xff1a;

  • hashCode&#61;&#61;0&#xff1a;简单地返回随机数&#xff0c;与对象的内存地址没有联系。然而根据随机数生成并全局地读写在多处理器下并不占优势。

  • hashCode&#61;&#61;2&#xff1a;始终返回完全相同的标识&#xff0c;即 hashCode&#61;1。这可用于测试依赖对象标识的代码。

  • hashcode&#61;&#61;3&#xff1a;从零开始计算哈希代码值。它看起来不是线程安全的&#xff0c;因此多个线程可以生成具有相同哈希代码的对象。

  • hashCode>&#61;5(默认)&#xff1a;在 jdk1.8 中&#xff0c;这是默认的 hashCode 生成算法&#xff0c;支持多线程生成。使用了 Marsaglia 的 xor-shift 算法产生伪随机数。

可以知道&#xff0c;hashCode 为 5 就是我们程序调用时的默认策略。其他的几个分支我的理解也只能到这里&#xff0c;如果有大佬了解的更细&#xff0c;可以在评论指出。这里先不管 Marsaglia 大佬是谁&#xff0c;为什么是伪随机数呢&#xff1f;

关于真随机数的生成&#xff0c;这里可能要牵扯到随机数生成的物理知识。Intel810RNG 的原理大概是&#xff1a;利用热噪声 (是由导体中电子的热震动引起的) 放大后&#xff0c;影响一个由电压控制的振荡器&#xff0c;通过另一个高频振荡器来收集数据... ...

我们实际应用的基本上都是通过数学公式产生的伪随机数。严格意义上讲&#xff0c;伪随机数不是完全随机的&#xff0c;但是真随机生成比较困难&#xff0c;所以只要能通过一定的随机数统计检测&#xff0c;就可以当作真随机数来使用。

有点离题了&#xff0c;下面来谈谈这个 xor-shift 算法&#xff5e;

Marsaglia 的 xor-shift 策略&#xff0c;支持多线程执行的状态&#xff0c;这可能是最好的整体实现 &#xff0c;这种方式生成随机数执行起来很快。简单来说&#xff0c;看起来就是一个移位寄存器&#xff0c;每次移入的位由寄存器中若干位取异或生成。每次新生成的位看起来是随机的。如果要深究&#xff0c;可能会扯很多数学公式&#xff0c;这里就不探讨了(毕竟数学太深奥了&#xff0c;菜是原罪)。

从维基百科上粘的基本实现&#xff1a;

uint32_t xor128(void) {
static uint32_t x &#61; 123456789;
static uint32_t y &#61; 362436069;
static uint32_t z &#61; 521288629;
static uint32_t w &#61; 88675123;
uint32_t t;

t &#61; x ^ (x <<11);
x &#61; y; y &#61; z; z &#61; w;
return w &#61; w ^ (w>> 19) ^ (t ^ (t>> 8));
}

这里面的入参还是需要好好打磨的&#xff0c;才能通过随机数的严苛测试&#xff5e;

拓展阅读&#xff1a;zhihu.com/question/2795
论文地址&#xff1a;jstatsoft.org/v08/i14/p

2.3 从局部到全局

了解了 hashCode 是怎么产生的&#xff0c;再看看上层&#xff0c;获取前需要做哪些准备&#xff1f;具体代码比较长&#xff0c;就不贴出了&#xff0c;简单概括。

如果处于偏向锁状态&#xff0c;就需要先撤销偏向锁。然后确保当前线程执行路径不在 safe point 上&#xff0c;并且是 java 线程&#xff0c;未阻塞状态。读取稳定的对象头&#xff0c;防止对象继续锁升级&#xff0c;如果是&#xff0c;就需要等待升级完。等到对象状态稳定了&#xff0c;从对象头中取出 hash&#xff0c;如果不存在&#xff0c;则执行上文代码&#xff0c;计算 hashCode。如果对象处于轻量级锁状态&#xff0c;并且当前线程持有&#xff0c;就从 线程的栈里取对象头。当升级为重量级锁时&#xff0c;就执行上文代码&#xff0c;计算 hashCode。

因此&#xff0c;hashCode 只会被计算一遍&#xff0c;之后就存在对象头中。

拓展阅读&#xff1a;zhihu.com/question/2997

至此&#xff0c;jdk 原生 hashCode 的生成过程梳理完了。

三、String、Lombok 对 hashCode 的实现

3.1 Lombok 实现 hashCode

如果把实体类换成 Lombok 实现&#xff0c;又会怎么样呢&#xff1f;

&#64;Data
public class Student {

private int no;

private String name;

public static void main(String[] args) {
Student student1&#61;new Student();
student1.setName("张三");
student1.setNo(12);
System.out.println(student1.hashCode());
Map map&#61;new HashMap<>();
map.put(student1,"student1");
student1.setName("111");
System.out.println(student1.hashCode());
System.out.println(map.get(student1));
}
}

输出&#xff1a;

779078
52846
null

可以神奇地看到&#xff0c;hashCode 明显被修改了&#xff0c;并且 hashMap 也取不到值&#xff0c;这是怎么回事&#xff1f;
原来&#xff0c;Lombok 的 &#64;Data 注解相当于 5 个注解&#xff1a;

  • &#64;Getter

  • &#64;Setter

  • &#64;RequiredArgsConstructor

  • &#64;ToString

  • &#64;EqualsAndHashCode

相当于重写了 hashCode&#xff0c;只要属性发生变化&#xff0c;再次输出时&#xff0c;hashCode 就会不同。

如果将代码反编译后&#xff0c;不难发现。

public class Student {
private int no;
private String name;

public int hashCode() {
int PRIME &#61; true;
int result &#61; 1;
int result &#61; result * 59 &#43; this.getNo();
Object $name &#61; this.getName();
result &#61; result * 59 &#43; ($name &#61;&#61; null ? 43 : $name.hashCode());
return result;
}
}

3.2 String 实现 hashCode

public int hashCode() {
int h &#61; hash;
if (h &#61;&#61; 0 && value.length > 0) {
char val[] &#61; value;

for (int i &#61; 0; i h &#61; 31 * h &#43; val[i];
}
hash &#61; h;
}
return h;
}

可以看出&#xff0c;相同的字符串调用 hashCode () 方法&#xff0c;得到的值是一样的&#xff0c;与内存地址、进程、机器无关。代码似乎很简单&#xff0c;但是一定要归纳出来他的实现过程。ef9b1818-4443-eb11-8da9-e4434bdf6706.svg
注&#xff1a;n 为字符串长度。

如果字符串相等&#xff0c;hashCode 必然一样&#xff1b;如果 hashCode 一样&#xff0c;字符串不一定相等&#xff0c;因为计算时可能发生溢出。

为什么计算时选择 31&#xff1f;

  1. 31 是个奇质数&#xff0c;不大不小&#xff0c;一般质数非常适合 hash 计算&#xff0c;偶数相当于移位运算&#xff0c;容易溢出&#xff0c;数据信息丢失。如果太小&#xff0c;则产生的哈希值区间小&#xff1b;太大则容易溢出&#xff0c;数据信息丢失。

  2. 31 * i &#61;&#61; (i <<5) - i。非常易于维护&#xff0c;将移位代替乘除&#xff0c;会有性能的提升&#xff0c;并且 JVM 执行时能够自动优化成这个样子。

  3. 通过实验计算&#xff0c;选用 31 后出现 hash 冲的概率相比于其他数字要小。

拓展阅读&#xff1a;segmentfault.com/a/1190

最后

底层源码还是很深奥的&#xff0c;知识都是互通的。最后物理&#xff0c;数学都融合在一起哈哈&#xff0c;还是很微妙的&#xff5e;
文章如有错误&#xff0c;欢迎指出&#xff5e;
卑微求在看&#xff0c;祝大佬们头发越来越茂密哈 ~

参考文章&#xff1a;

blog.csdn.net/weixin_30zhihu.com/question/2997segmentfault.com/a/1190it1352.com/958039.htmlzhihu.com/question/2795




推荐阅读
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 本文介绍如何使用OpenCV和线性支持向量机(SVM)模型来开发一个简单的人脸识别系统,特别关注在只有一个用户数据集时的处理方法。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 本文对比了杜甫《喜晴》的两种英文翻译版本:a. Pleased with Sunny Weather 和 b. Rejoicing in Clearing Weather。a 版由 alexcwlin 翻译并经 Adam Lam 编辑,b 版则由哈佛大学的宇文所安教授 (Prof. Stephen Owen) 翻译。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • 本文介绍了几种常用的图像相似度对比方法,包括直方图方法、图像模板匹配、PSNR峰值信噪比、SSIM结构相似性和感知哈希算法。每种方法都有其优缺点,适用于不同的应用场景。 ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • 字节流(InputStream和OutputStream),字节流读写文件,字节流的缓冲区,字节缓冲流
    字节流抽象类InputStream和OutputStream是字节流的顶级父类所有的字节输入流都继承自InputStream,所有的输出流都继承子OutputStreamInput ... [详细]
  • 在机器学习领域,深入探讨了概率论与数理统计的基础知识,特别是这些理论在数据挖掘中的应用。文章重点分析了偏差(Bias)与方差(Variance)之间的平衡问题,强调了方差反映了不同训练模型之间的差异,例如在K折交叉验证中,不同模型之间的性能差异显著。此外,还讨论了如何通过优化模型选择和参数调整来有效控制这一平衡,以提高模型的泛化能力。 ... [详细]
  • 线程能否先以安全方式获取对象,再进行非安全发布? ... [详细]
author-avatar
拐久了_618
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有