-
Object类中的hashcode方法
2020-07-02 13:21:59回答后,仔细一想,不对呀,这个 hash 值具体是怎么计算的,我终究还是没有答到点上,而是绕开话题,回答了含义。 脑壳一热,忽然想起去年虐我的阿里面试题,hashCode 是怎么得到的呢? 一、..转自:https://mp.weixin.qq.com/s/qjEEU7nWo3xJXLjo_apjww
引言
这两天有个学弟问过我这个问题:对象的 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 ^= (t << 11) ; Self->_hashStateX = Self->_hashStateY ; Self->_hashStateY = Self->_hashStateZ ; Self->_hashStateZ = Self->_hashStateW ; unsigned v = Self->_hashStateW ; v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ; Self->_hashStateW = v ; value = v ; } value &= markOopDesc::hash_mask; if (value == 0) value = 0xBAD ; assert (value != markOopDesc::no_hash, "invariant") ; TEVENT (hashCode: GENERATE) ; return value; }
源码中的 hashCode 其实就是 JVM 启动的一个参数,每一个分支对应一个生成策略。通过 -XX:hashCode,可以任意切换 hashCode 的生成策略。
首先解释一下入参 oop obj 就是对象的逻辑地址。所以与地址相关的生成策略有两条,在 hashCode 等于 1 或 4 的时候。-
hashCode==1:这种方式具有幂等的性质,在 STW(stop-the-world)操作中,这种策略通常用于同步方案中。利用对象地址计算,使用不经常更新的随机数参与运算。
-
hashCode==4:与创建对象的内存位置有关,原样输出。
其他情况:
-
hashCode==0:简单地返回随机数,与对象的内存地址没有联系。然而根据随机数生成并全局地读写在多处理器下并不占优势。
-
hashCode==2:始终返回完全相同的标识,即 hashCode=1。这可用于测试依赖对象标识的代码。
-
hashcode==3:从零开始计算哈希代码值。它看起来不是线程安全的,因此多个线程可以生成具有相同哈希代码的对象。
-
hashCode>=5(默认):在 jdk1.8 中,这是默认的 hashCode 生成算法,支持多线程生成。使用了 Marsaglia 的 xor-shift 算法产生伪随机数。
可以知道,hashCode 为 5 就是我们程序调用时的默认策略。其他的几个分支我的理解也只能到这里,如果有大佬了解的更细,可以在评论指出。这里先不管 Marsaglia 大佬是谁,为什么是伪随机数呢?
关于真随机数的生成,这里可能要牵扯到随机数生成的物理知识。Intel810RNG 的原理大概是:利用热噪声 (是由导体中电子的热震动引起的) 放大后,影响一个由电压控制的振荡器,通过另一个高频振荡器来收集数据... ...
我们实际应用的基本上都是通过数学公式产生的伪随机数。严格意义上讲,伪随机数不是完全随机的,但是真随机生成比较困难,所以只要能通过一定的随机数统计检测,就可以当作真随机数来使用。
有点离题了,下面来谈谈这个 xor-shift 算法~
Marsaglia 的 xor-shift 策略,支持多线程执行的状态,这可能是最好的整体实现 ,这种方式生成随机数执行起来很快。简单来说,看起来就是一个移位寄存器,每次移入的位由寄存器中若干位取异或生成。每次新生成的位看起来是随机的。如果要深究,可能会扯很多数学公式,这里就不探讨了(毕竟数学太深奥了,菜是原罪)。
从维基百科上粘的基本实现:
uint32_t xor128(void) { static uint32_t x = 123456789; static uint32_t y = 362436069; static uint32_t z = 521288629; static uint32_t w = 88675123; uint32_t t; t = x ^ (x <<11); x = y; y = z; z = w; return w = w ^ (w>> 19) ^ (t ^ (t>> 8)); }
这里面的入参还是需要好好打磨的,才能通过随机数的严苛测试~
拓展阅读:zhihu.com/question/2795
论文地址:jstatsoft.org/v08/i14/p2.3 从局部到全局
了解了 hashCode 是怎么产生的,再看看上层,获取前需要做哪些准备?具体代码比较长,就不贴出了,简单概括。
如果处于偏向锁状态,就需要先撤销偏向锁。然后确保当前线程执行路径不在 safe point 上,并且是 java 线程,未阻塞状态。读取稳定的对象头,防止对象继续锁升级,如果是,就需要等待升级完。等到对象状态稳定了,从对象头中取出 hash,如果不存在,则执行上文代码,计算 hashCode。如果对象处于轻量级锁状态,并且当前线程持有,就从 线程的栈里取对象头。当升级为重量级锁时,就执行上文代码,计算 hashCode。
因此,hashCode 只会被计算一遍,之后就存在对象头中。
拓展阅读:zhihu.com/question/2997
至此,jdk 原生 hashCode 的生成过程梳理完了。
三、String、Lombok 对 hashCode 的实现
3.1 Lombok 实现 hashCode
如果把实体类换成 Lombok 实现,又会怎么样呢?
@Data public class Student { private int no; private String name; public static void main(String[] args) { Student student1=new Student(); student1.setName("张三"); student1.setNo(12); System.out.println(student1.hashCode()); Map<Student,String> map=new HashMap<>(); map.put(student1,"student1"); student1.setName("111"); System.out.println(student1.hashCode()); System.out.println(map.get(student1)); } }
输出:
779078 52846 null
可以神奇地看到,hashCode 明显被修改了,并且 hashMap 也取不到值,这是怎么回事?
原来,Lombok 的 @Data 注解相当于 5 个注解:-
@Getter
-
@Setter
-
@RequiredArgsConstructor
-
@ToString
-
@EqualsAndHashCode
相当于重写了 hashCode,只要属性发生变化,再次输出时,hashCode 就会不同。
如果将代码反编译后,不难发现。
public class Student { private int no; private String name; public int hashCode() { int PRIME = true; int result = 1; int result = result * 59 + this.getNo(); Object $name = this.getName(); result = result * 59 + ($name == null ? 43 : $name.hashCode()); return result; } }
3.2 String 实现 hashCode
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
可以看出,相同的字符串调用 hashCode () 方法,得到的值是一样的,与内存地址、进程、机器无关。代码似乎很简单,但是一定要归纳出来他的实现过程。
注:n 为字符串长度。如果字符串相等,hashCode 必然一样;如果 hashCode 一样,字符串不一定相等,因为计算时可能发生溢出。
为什么计算时选择 31?
-
31 是个奇质数,不大不小,一般质数非常适合 hash 计算,偶数相当于移位运算,容易溢出,数据信息丢失。如果太小,则产生的哈希值区间小;太大则容易溢出,数据信息丢失。
-
31 * i == (i << 5) - i。非常易于维护,将移位代替乘除,会有性能的提升,并且 JVM 执行时能够自动优化成这个样子。
-
通过实验计算,选用 31 后出现 hash 冲的概率相比于其他数字要小。
拓展阅读:segmentfault.com/a/1190
最后
底层源码还是很深奥的,知识都是互通的。最后物理,数学都融合在一起哈哈,还是很微妙的~
参考文章:
blog.csdn.net/weixin_30
zhihu.com/question/2997
segmentfault.com/a/1190
it1352.com/958039.html
zhihu.com/question/2795 -
-
python碰到问题的时候应该如何查找帮助
2015-12-20 15:39:00本文介绍一下我在使用python时如何查找一下基本函数的用法,首先是搜索引擎,百度有时候还是有点不行,都是很类似的回答,可能都是错的,他们也不说清楚怎么写的代码,每一句话的含义,用google又要翻墙,网速不行的...编程的时候最痛苦的事情就是不懂错在哪里,不懂基本的用法。本文介绍一下我在使用python时如何查找一下基本函数的用法,首先是搜索引擎,百度有时候还是有点不行,都是很类似的回答,可能都是错的,他们也不说清楚怎么写的代码,每一句话的含义,用google又要翻墙,网速不行的话非常煎熬。python最强的学习方法就是阅读源代码,帮助手册也很有用处。不想看源代码可以用help(类名)快速看到类的变量和方法。在命令行中使用,要先ipmort对应模块
-
-
清华大学的计算机网络课件
2010-03-26 11:11:56问题3-5:除了差错检测外,面向字符的数据链路层协议还必须解决哪些特殊的问题? 问题3-6:为什么计算机进行通信时发送缓存和接收缓存总是需要的? 问题3-7:在教材中的3.3.3节提到“发送窗口用来对发送端进行流量... -
-
-
-
-
-
-
-
-
-
-
-
-
Java面向对象
2017-06-30 16:08:15今天,又去面了越秀金科,怎么说?我发现自己的 一个问题就是明明问的东西其实自己是想得通的,为什么就不能完整的表达出来呢?而且回答的很没有方向感,很没有条理。...面向对象的含义,类,对象,面向对象的特性---今天,又去面了越秀金科,怎么说?我发现自己的 一个问题就是明明问的东西其实自己是想得通的,为什么就不能完整的表达出来呢?而且回答的很没有方向感,很没有条理。真的是很简单的一个问题:如何理解Java面向对象?到底回答的一个思路是什么?我找到了下面这一篇。
区分面向对象和面向过程
面向过程就是指分析出解决问题所需要的步骤,然后用函数把这些步骤一步步实现,使用时依次调用即可。例如,一辆汽车用面向过程的思路去想,是如何启动汽车、如何起步、加速、熄火、刹车等操作,在这里汽车不是我们关心的,我们关心的是事件。
面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。还是拿汽车来说,用面向对象的思想去看,我们关注的主体是汽车,汽车的发动机、传动箱、变速箱、刹车等属性是汽车这个对象本身所具有的,做任何操作只需要告诉汽车即可。
如何理解面向对象?
面向对象的内容主要包括:类、对象和面向对象的三大特征(封装、继承和多态)
类就是具有相同属性和方法的一组对象集合,它为属于该类的所有对象提供了统一的抽象描述。
对象就是类的实例,系统会为对象分配内存。当一个对象没有引用指向它时,该对象为无用对象,java的垃圾收集器会自动扫描动态内存区,把无用对象全都收集起来并释放内存。
封装:封装是面向对象编程的核心思想,类把对象的属性和行为封装起来,隐藏其内部状态。这就是封装的思想。
继承:当一个类的属性与行为均与现有类的相似,属于现有类的一种,这个类可以定义为现有类的子类。相反,如果多个类具有相同的属性与行为,我们可以抽取出共性的内容并定义成父类,这时再创建相似的类时只要继承父类即可。
多态:多态的特征是具有多种形态,具有多种实现方式。或者同一个接口,使用不同的实例会执行不同的操作。例如,不同的运动器材搭配球是不同的运动,有羽毛球运动,篮球运动,乒乓球运动等等,但我们只定义了一种器材搭配球而已,而后台又会集体判断是什么运动器材搭配球,从而得知是什么运动。这就是多态。
抽象与封装有何区别?
抽象是指从众多事物中抽取出共同的、本质的特征,而舍弃其非本质的特征。
封装则是指将抽象出来的数据和行为相结合,形成一个有机的整体,也就是类,其中数据和行为都是类的成员。private就是封装的一种体现。
封装是抽象策略的一部分,在我理解认为,抽象包括抽取出共性的、本质的内容和将其封装起来两部分,而封装则仅仅是体现出对象封装其内部状态,并对外隐藏的行为。
因此抽象才是我们更常用的术语,例如List接口是一个集合抽象,因为它把ArrayList,LinkedList等集合实现类中共同本质的东西抽取并封装起来。
接口和抽象类有何区别?
从语法层面来说:(1)一个类只能继承一个抽象类,但可以实现多个接口(2)抽象类的成员变量可以是多种类型,接口的成员变量必须要有public static final 修饰(3)抽象类中可以含有静态代码块和静态方法,接口不可以(4)抽象类可以提供成员方法的实现细节,接口的成员方法只能是public abstract。从设计层面来说:抽象类是对类的抽象,接口则是对行为的抽象。所以抽象类既能抽象成员变量,也能抽象成员方法,但接口只能抽象成员方法而不能抽象成员变量。继承代表“是不是”的关系,实现代表“能不能”的关系。而抽象类只能被继承,接口只能被实现,因此抽象类的子类必然是抽象类的一种,而接口的实现类必然具有接口中所声明的能力。例如现在有飞机和鸟两个抽象类,飞机和鸟显然不是同一种类,但都会飞,因此把飞这个行为抽取出来封装在接口里,飞机和鸟如果实现了这个接口,则表示能飞,没有实现则表示不能飞。由于飞机和鸟具体有很多种类,比如飞机有战斗机,民用飞机,鸟有麻雀和老鹰等等,它们之间是继承关系,即表达成战斗机是飞机的一种,麻雀是鸟的一种。权限修饰符之间的区别
11个java修饰词作用对象和所起到的作用
JVM的类装载机制
虚拟机把描述类的数据从class文件加载到内存(即从硬盘到内存),并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。这就是Java虚拟机的类装载机制。其中解释一下JVM把HelloWorld.class变成Class对象的全过程(1)类加载器的初始化通过上面这个图我们可以知道AppClassLoader的父ClassLoader是ExtClassLoader,而ExtClassLoader的父ClassLoader是Bootstrap Loader。Public class Test{
Public static void main(String[] arg){
ClassLoader c = Test.class.getClassLoader(); //获取Test类的类加载器
System.out.println(c);
ClassLoader c1 = c.getParent(); //获取c这个类加载器的父类加载器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
System.out.println(c2);
}
}以上这个代码执行之后输出的结果是:AppClassLoaderExtClassLoaderNull为什么最后不是Bootstrap Loader而是Null呢?因为Bootstrap Loader是用C++语言写的,但这是在Java环境下,因此逻辑上并不存在Bootstrap Loader这个类实例,因此输出为Null。注意:同一个ClassLoader(如果是不同的ClassLoader,但其顶层类加载器相同,也视为同一个ClassLoader)加载同一个Class文件,只产生一个Class实例。两个不同的ClassLoader(其顶层类加载器不同)加载同一个Class文件,会产生两个Class实例。这种情况体现了双亲委派模型的好处。比如java.lang.Object类,由始至终都是启动类加载器(Bootstrap Loader)去加载,只产生一个Object实例。但如果用户自定义了一个Object同名类,并放在程序的classpath中,则这个Class文件是由AppClassloader加载产生了另一个Object实例,会使程序变得混乱。(2)类的加载类加载的动态性体现:一个应用程序总是由n个类组成,当启动Java程序时,JVM并不是一次性把所有的类加载进内存,而是先将能保证程序正常运行的基本API加载进JVM,其它的类在需要用到的时候再由JVM加载。这样做的好处是节省了内存的开销。通过上图可以知道是通过AppClass装载器加载类文件的,但其实如果AppClass装载器的父装载器存在的话,是由其父装载器加载类文件的。如果不存在,则是由AppClass来执行装载操作- 装载:查找和导入Class文件
(1) 通过一个类的全限定名来获取定义此类的二进制字节流
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
虚拟机规范中并没有准确说明二进制字节流应该从哪里获取以及怎样获取,这里可以通过定义自己的类加载器去控制字节流的获取方式。- 链接:把类的二进制数据合并到JRE
校验:检查载入class文件数据的正确性准备:给类的静态变量分配存储空间准备阶段是正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,需要说明的是:
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里所说的初始 值“通常情况”是数据类型的零值,假如:
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行解析:将符号引用转换成直接引用- 初始化:对类的静态变量,静态代码块执行初始化操作
(1)ClassLoader loader = HelloWorld.class.getClassLoader();Class class = loader.loadClass("HelloWorld");(2)Class class = Class.forName("HelloWorld",false,loader);(3)Class class = Class.forName("HelloWorld");(4)遇到new字眼的隐式装载前三种情况都是显式装载,而且显式装载时凡是有指定ClassLoader对象的,不会执行该类的静态方法和静态代码块。但第三种情况就有执行类的静态方法和静态代码块。下面是JVM的内存模型通过一个例子形象说明一下什么样的数据存放在哪个区域内java内存存储模型(句柄访问)java内存存储模型(直接指针访问)Java内存模型的原子性、可见性和有序性
其实这里就是从内存角度出发解释并发编程时出现的问题,我们要针对这些问题提出解决方案。工作内存要操作主内存中的变量的过程中包括以下原子操作:read、load、use、assign、store、write。因为线程之间要通过共享变量进行数据通信,但线程不是直接操作主内存中的引用,而是拷贝一个副本到工作内存中,对该副本引用进行操作,所以副本的值有变,而主内存的值并没变,所以值的改变在其它线程中并不可见,必须要同步到主内存中才可见,因此我们要保证可见性必须实现同步,而volatile、synchronized和final可以保证可见性。volatile只能保证可见性,不能保证有序性,而synchronized既能保证可见性又能保证有序性。 -
ChatBotCourse.rar
2020-05-17 19:34:52整体上分为三个重要模块:提问处理模块、检索模块、答案抽取模块。...最后再说这个句法和语义分析,这是对你问题的深层含义做一个剖析,比如你的问题是:聊天机器人怎么做?那么我要知道你要问的是聊天机器人的研发方法 -
Oracle Database 9i10g11g编程艺术:深入数据库体系结构(第2版)--详细书签版
2013-02-03 11:42:53在本书第1版出版时隔4年后,Thomas Kyte及时了解了大家的这一迫切需求,根据他的实战经验以及人们最关心的问题对这本书做了全面补充和调整,以涵盖11g最受关注的多项特性。例如11g引入dbms_parallel_execute包来帮助... -
-
-
-
-
-
-
-
-
-
-
jquery库是什么意思
-
辅助驾驶的哈密顿量对绝热演化是否总是有用?
-
access应用的3个开发实例
-
朱老师c++课程第3部分-3.5STL的其他容器讲解
-
数据仓库多维数据模型设计
-
pl是什么软件
-
2021-02-25
-
基于python的dango框架购物商城毕业设计毕设源代码使用教程
-
利用windows防火墙可以干嘛
-
Python启蒙到架构师的核心技术精讲课程
-
Mysql数据库面试直通车
-
JMETER 性能测试基础课程
-
PPTP_NNN 服务生产环境实战教程
-
用于文档聚类的半监督概念分解
-
AIC和RIE法制备的黑硅纳米林
-
Mavean导包失败、导入工程失败解决方法
-
自动化测试Python3+Selenium3+Unittest
-
【Java并发编程】Reentrantlock(一):基本使用及特性方法
-
2021-02-25
-
APPKIT打造稳定、灵活、高效的运营配置平台