如何用JAVA源码解析hashcode方法
这期内容当中小编将会给大家带来有关如何用JAVA源码解析hashcode方法,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。
创新互联公司专注于陆河网站建设服务及定制,我们拥有丰富的企业做网站经验。 热诚为您提供陆河营销型网站建设,陆河网站制作、陆河网页设计、陆河网站官网定制、微信小程序定制开发服务,打造陆河网络公司原创品牌,更为您提供陆河网站排名全网营销落地服务。
在开发过程中我们可能会经常接触到hashcode
这个方法来生成哈希码,那么底层是如何实现的?使用时有何注意点呢?
hashcode() 方法底层实现
hashcode()
是Object
的方法:
@HotSpotIntrinsicCandidate public native int hashCode();
它是一个native
的方法,并且被@HotSpotIntrinsicCandidate
注解修饰,证明它是一个在HotSpot中有一套高效的实现,该高效实现基于CPU指令。
具体的实现参考源码synchronizer.cpp
:
static inline intptr_t get_next_hash(Thread* self, oop obj) { intptr_t value = 0; if (hashCode == 0) { value = os::random(); } else if (hashCode == 1) { intptr_t addr_bits = cast_from_oop(obj) >> 3; value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; } else if (hashCode == 2) { value = 1; } else if (hashCode == 3) { value = ++GVars.hc_sequence; } else if (hashCode == 4) { value = cast_from_oop (obj); } else { 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 &= markWord::hash_mask; if (value == 0) value = 0xBAD; assert(value != markWord::no_hash, "invariant"); return value; }
可以看出,根据hashcode
这个全局变量的取值,决定用何种策略生成哈希值,查看globals.hpp
来看是哪一种变量:
experimental(intx, hashCode, 5, "(Unstable) select hashCode generation algorithm")
发现是一个experimental
的 JVM 变量,这样的话,想要修改,必须添加额外的参数,如下所示:
-XX:+UnlockExperimentalVMOptions -XX:hashCode=2
并且,这个hashCode
默认为5。
哈希值是每次hashcode()方法调用重计算么?
对于没有覆盖hashcode()
方法的类,实例每次调用hashcode()
方法,只有第一次计算哈希值,之后哈希值会存储在对象头的 标记字(MarkWord)中。
如果进入各种锁状态,那么会缓存在其他地方,一般是获取锁的线程里面存储,恢复无锁(即释放锁)会改回原有的哈希值。
关于对象头结构,以及对象存储结构,感兴趣的话,可以参考:Java GC详解 - 1. 理解Java对象结构
-XX:hashCode=0 利用 Park-Miller 伪随机数生成器生成哈希值
if (hashCode == 0) { value = os::random(); }
调用 os 的 random 方法生成随机数。这个方法的实现方式是: os.cpp:
//初始seed,默认是1 volatile unsigned int os::_rand_seed = 1; static int random_helper(unsigned int rand_seed) { /* standard, well-known linear congruential random generator with * next_rand = (16807*seed) mod (2**31-1) * see * (1) "Random Number Generators: Good Ones Are Hard to Find", * S.K. Park and K.W. Miller, Communications of the ACM 31:10 (Oct 1988), * (2) "Two Fast Implementations of the 'Minimal Standard' Random * Number Generator", David G. Carta, Comm. ACM 33, 1 (Jan 1990), pp. 87-88. */ const unsigned int a = 16807; const unsigned int m = 2147483647; const int q = m / a; assert(q == 127773, "weird math"); const int r = m % a; assert(r == 2836, "weird math"); // compute az=2^31p+q unsigned int lo = a * (rand_seed & 0xFFFF); unsigned int hi = a * (rand_seed >> 16); lo += (hi & 0x7FFF) << 16; // if q overflowed, ignore the overflow and increment q if (lo > m) { lo &= m; ++lo; } lo += hi >> 15; // if (p+q) overflowed, ignore the overflow and increment (p+q) if (lo > m) { lo &= m; ++lo; } return lo; } int os::random() { // Make updating the random seed thread safe. while (true) { unsigned int seed = _rand_seed; unsigned int rand = random_helper(seed); //CAS更新 if (Atomic::cmpxchg(&_rand_seed, seed, rand) == seed) { return static_cast(rand); } } }
其中,random_helper 就是随机数的生成公式的实现,公式是: 这里,a=16807, c=0, m=2^31-1
由于这些随机数都是采用的同一个生成器,会 CAS 更新同一个 seed,如果有大量的生成的新对象并且都调用hashcode()
方法的话,可能会有性能问题。重复调用同一个对象的hashcode()
方法不会有问题,因为之前提到了是有缓存的。
-XX:hashCode=1或者4 基于对象指针 OOPs
OOPs(Ordinary Object Pointers)对象指针是对象头的一部分。关于对象头结构,以及对象存储结构,感兴趣的话,可以参考:Java GC详解 - 1. 理解Java对象结构。可以简单理解为对象在内存中的地址的描述。
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 addr_bits = cast_from_oop(obj) >> 3; value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; } else if (hashCode == 4) { value = cast_from_oop (obj); }
cast_from_oop
很简单,就是获取oop
的实现基类oopDesc
的指向地址(oopDesc
描述了OOP的基本组成,感兴趣可以参考:Java GC详解 - 1. 理解Java对象结构):
templateinline T cast_from_oop(oop o) { return (T)(CHECK_UNHANDLED_OOPS_ONLY((oopDesc*))o); }
当-XX:hashCode=4
,直接用oop
的地址作为哈希值。-XX:hashCode=1
则是经过变换的,每次发生 Stop The World (STW)stw_random会发生改变,通过这个addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random
变换减少哈希碰撞,让哈希值更散列化。
想更深入了解 Stop the world,可以参考:JVM相关 - SafePoint 与 Stop The World 全解(基于OpenJDK 11版本)
-XX:hashCode=2 敏感测试,恒定为1
else if (hashCode == 2) { value = 1; // for sensitivity testing }
主要用于测试某些集合是否对于哈希值敏感。
-XX:hashCode=3 自增序列
else if (hashCode == 3) { value = ++GVars.hc_sequence; } struct SharedGlobals { // omitted DEFINE_PAD_MINUS_SIZE(1, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int) * 2); // Hot RW variable -- Sequester to avoid false-sharing volatile int hc_sequence; DEFINE_PAD_MINUS_SIZE(2, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int)); }; static SharedGlobals GVars;
每创建一个新对象,调用哈希值,这个自增数+1,可以看出,散列性极差,很容易哈希碰撞。
-XX:hashCode=5 默认实现
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; }
采用的算法是 Marsaglia's xor-shift 随机数生成法。主要是这篇论文提出的一种快速并且散列性好的哈希算法。
特殊的哈希值导致某些场景的问题
我们经常使用某个对象或者某个字段的哈希值,通过对于某个数组长度取模,获取到下标,取出数组对应下标的对象,进行进一步处理。这在负载均衡,任务调度,线程分配很常见。那下面这段代码是否有问题呢?
//获取userId这个字符串的哈希值的绝对值 int index = Math.abs(userId.hashCode()); //返回哈希值取模之后的下标的对象 return userAvatarList.get(index % userAvatarList.size()).getUrl();
通常大多数情况下,是没有问题的,但是假设userId
是这几个哈希值为Integer.MIN_VALUE
的字符串:
System.out.println("polygenelubricants".hashCode()); System.out.println("GydZG_".hashCode()); System.out.println("DESIGNING WORKHOUSES".hashCode());
输出:
-2147483648 -2147483648 -2147483648
对于这些值,如果你用Math.abs()
取绝对值的话,我们知道Math.abs(Integer.MIN_VALUE)
还是等于Integer.MIN_VALUE
,这是因为底层实现:
public static int abs(int a) { return (a < 0) ? -a : a; }
-Integer.MIN_VALUE
和Integer.MIN_VALUE
是相等的。Integer.MIN_VALUE
取模还是负数,这样取下标对应的对象的时候,就会报异常。
所以,需要修改为:
int index = Math.abs(userId.hashCode() % userAvatarList.size()); return userAvatarList.get(index).getUrl();
上述就是小编为大家分享的如何用JAVA源码解析hashcode方法了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注创新互联行业资讯频道。
文章标题:如何用JAVA源码解析hashcode方法
转载源于:http://scyanting.com/article/ggooid.html