hashCode()的实现细节

  "hashCode()的实现细节"

Posted by     "华恒" on Sunday, September 13, 2015

TOC

图片来源:维基百科

#从类加载的问题说起

同事在开发中遇到一个关于hashCode的诡异问题,为了简化问题突出本质,现将具体的业务代码进行了抽象,请看以下的代码:

C.jar:
    SomeClass.class

A.jar:
    Class.forName("SomeClass").hashCode();

B.jar:
    someClassObject.getClass().hashCode();

有一个类姑且称之为SomeClass位于C.jar中,它有一个对象someClassObject, A.jar里使用了Class.forName(“SomeClass”).hashCode()获取SomeClass的hashCode,B.jar里使用了someClassObject.getClass().hashCode()来获取hashCode。A.jar,B.jar均依赖C.jar, 三个Jar包都运行于同一个JVM中,以上是问题的上下文。问题出现在代码运行中发现两个语句得到的hashCode尽然不相等!

遇到这种问题第一直觉就是两个jar包的ClassLoader不同,后来找到问题的根源的确是ClassLoader引起的。下面是该环境的ClassLoader层次结构:

 Bootstrap ClassLoader
          |
 Extension ClassLoader
          |
Application ClassLoader
          |
 Customize ClassLoader

问题出现在: B.jar被Customize ClassLoader先加载,A.jar被Application ClassLoader后加载,SomeClass类同时存在于Application ClassLoader和Customize ClassLoader中,从而造成了hashCode不等。

因为其中的原因比较复杂, 本文先不去深究为什么Customize ClassLoader没有委托Application ClassLoader去加载SomeClass。由上面这个问题引申出来的另一个问题:hashCode()的实现细节才是本文的讨论重点。

#深入理解hashCode() 在Java语言里,每个对象都有hashCode()这个方法,该方法是从Object继承而来的。hashCode()方法的存在意义在于对象之间相等的比较,譬如将一个对象put到HashMap当中,HashMap就会调用对象的hashCode()来进一步计算该对象的哈希值。

对于hashCode()这个方法有以下的约定:

在同一个Java进程中,相等的对象hashCode()返回值必须相等

但以下观点是错误的:

1. 不相等的对象hashCode()一定不等

2. hashCode()返回值相等的对象就一定相等

这里先提一个问题,下面两条语句得到的结果一定不等吗?

Class.forName("AClass").hashCode();

Class.forName("BClass").hashCode();

由前面的观点可知不一定,究其原因,就得对hashCode的实现细节进行深入点的研究。不同JDK的实现可能不同,我们来看看OpenJDK7的实现代码synchronizer.cpp

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==0的注释中清清楚楚的写着:

so it's possible for two threads to race and generate the same RNG.

对于多线程的环境下,如果采用hashCode=0的哈希生成算法,两个不同的类是完全有可能生成相同的哈希值。

我们可以通过配置Java启动参数-XX:hashCode=n (from 0 to 5)来控制hashCode()生成算法,0到5所对应的算法如下:

0 – Park-Miller RNG (default)  Park-Miller随机数生成器
1 – function of address and some global state
2 – const 1
3 – sequenatial counter
4 – address of an object
5 – thread specific xor-shift

每个选项采用不同的算法来生成对象的哈希值, 其中Park-Miller随机数生成算法可以参考Lehmer random number generator。而第四种算法则直接使用object的内存地址作为哈希值。对于其他几种算法,本文就不再作进一步的探究。