Hash函数与xor-shift scheme,HashCollections,BloomFilter

Hash函数与xor-shift scheme,HashCollections,BloomFilter

根据定义,Hashcode用于帮助Equals更快速的鉴定两个对象是否相同,于此同时,HashCode也广泛运用于很多基于Hash的Collections。本文简要分析了HashCode的实现以及用于。

1. HashCode

1.1 来自hashcode的思考

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java<font size="-2"><sup>TM</sup></font> programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

注释说的太清楚我居然无言以对....

  1. 对一个对象调用多次hashcode,其返回值应当相同。
  2. 如果o1.equals(o2),那么o1.hashcode==o2.hashcode
  3. (不是必须但try best),if(!o1.equals(o2)) o1.hashcode可以=o2.hashcode,但应该尽可能避免该情况。

但我有几个问题之前一直没想明白:

  • 问题1. 简单类型和其他类型的实现不同么:
Integer i1=1;
Integer i2=1;
i1.hashcode=i2.hascode?

Entry o1=Entry(1,2)
Entry o2=Entry(1,2)
o2.hashcode=o2.hashcode?

后来我看到Integer.hashcode方法...

 public int hashCode() {
        return value;
    }
  • 问题2. 对象的引用更改时,Hashcode变么?

这个问题源自我对一个很长的链表上某个元素做hash的时候,让我不得不考虑会不会每次做hash都会遍历一遍整个链表。

结果很蛋疼,更改引用甚至值的话,如果你不重写hashcode方法,hashcode值是不会变的

	TmpOB o1=new TmpOB(1,"aaa");
	TmpOB o2=new TmpOB(1,"aaa");
	System.out.println(o1.hashCode());
	o2.a=3;
	System.out.println(o1.hashCode());
	***************
	//result:
	727129599
	727129599

1.2 hashcode的实现

因为Object 的hashcode方法是native的,所以参考一些别人的文章,大致意思是JVM里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;
}

原谅我C++学艺不精,完全不知道他在做什么。还好我找到了一些大神的讨论

ITEYE上的讨论

StackOverFlow上的讨论

The hashCode() method is often used for identifying an object. I think the Object implementation returns the pointer (not a real pointer but a unique id or something like that) of the object. But most classes override the method. Like the String class. Two String objects have not the same pointer but they are equal:

new String("a").hashCode() == new String("a").hashCode()
I think the most common use for hashCode() is in Hashtable, HashSet, etc..

Java API Object hashCode()

Edit: (due to a recent downvote and based on an article I read about JVM parameters)

With the JVM parameter -XX:hashCode you can change the way how the hashCode is calculated (see the Issue 222 of the Java Specialists' Newsletter).

HashCode==0: Simply returns random numbers with no relation to where in memory the object is found. As far as I can make out, the global read-write of the seed is not optimal for systems with lots of processors.

HashCode==1: Counts up the hash code values, not sure at what value they start, but it seems quite high.

HashCode==2: Always returns the exact same identity hash code of 1. This can be used to test code that relies on object identity. The reason why JavaChampionTest returned Kirk's URL in the example above is that all objects were returning the same hash code.

HashCode==3: Counts up the hash code values, starting from zero. It does not look to be thread safe, so multiple threads could generate objects with the same hash code.

HashCode==4: This seems to have some relation to the memory location at which the object was created.

HashCode>=5: This is the default algorithm for Java 8 and has a per-thread seed. It uses Marsaglia's xor-shift scheme to produce pseudo-random numbers.

可以看到HashCode>=5是默认实现,可是这个XorShift是个虾米?

感谢wikipedia上的资料

上面讲的很清楚,弗罗里达州立大学一位叫做George Marsaglia的老师发表了一篇使用位移以及亦或运算生成随机数的方法,并发表了论文在一篇统计学杂志上。

最简单的实现是这样

#include <stdint.h>

/* These state variables must be initialized so that they are not all zero. */
uint32_t x, y, z, w;

uint32_t xorshift128(void) {
    uint32_t t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}

哈哈 这次和C++代码总算对上了。先不管为什么,先看看wiki上继续的讨论。

1.3 随机数

因为随机数是密码学的基础,因此关于随机数生成,测试,比较,坑很深,不再过多解释。

随机数大体可分为物理产生的真正的随机数,以及一些函数生成的伪随机数,部分Intel的芯片里就带有物理产生随机数的方法

来自hcwang的关于随机数的笔记

  • 真随机数

真随机数只能用某些随机物理过程来产生。例如:放射性衰变、电子设备的热噪音、宇宙射线的触发时间等等。如果采用随机物理过程来产生蒙特卡洛计算用的随机数,理论上不存在问题,但是实际应用中,要做出速度很快而又准确的随机物理过程产生器是很困难的。Intel810RNG的原理大概是:利用热噪声(是由导体中电子的热震动引起的)放大后,影响一个由电压控制的振荡器,通过另一个高频振荡器来收集数据。

  • 伪随机数

实际应用的随机数通常都是通过某些数学公式计算而产生的伪随机数。这样的伪随机数从数学意义上讲已经一点不是随机的了。但是,只要伪随机数能够通过随机数的一系列的统计检验,我们就可以把它当作真随机数而放心地使用。这样我们就可以很经济地、重复地产生出随机数。理论上要求伪随机数产生器要具备以下特征:良好的统计分布特性、高效率的伪随机数产生、伪随机数产生的循环周期长,产生程序可移植性好和伪随机数可以重复产生。其中满足良好的统计特性是最重要的。

摘自知乎的部分测试随机数生成算法的一些测试思想:

来自知乎-如何评价一个伪随机数生成算法的优劣?

  • 频数测试:测试二进制序列中,“0”和“1” 数目是否近似相等。如果是,则序列是随机的。
  • 块内频数测试:目的是确定在待测序列中,所有非重叠的 长度为M位的块内的“0”和“1”的数目是否表现为随机分布。如果是,则序列是随机的。
  • 游程测试:目的是确定待测序列中,各种特定长度的 “0”和“1”的游程数目是否如真随机序列期望的那样。如果是,则序列是随机的。
  • 块内最长连续“1”测试:目的是确定待测序列中, 最长连“1”串的长度是否与真随机序列中最长连“1”串的 长度近似一致。如果是,则序列是随机的。
  • 矩阵秩的测试:目的是检测待测序列中,固定长度子序列的线性相关性。如果线性相关性较小,则序列是随机的。
  • 离散傅里叶变换测试:目的是通过检测待测序列的周期性质,并与真随机序列周期性质相比较,通过它们之间的偏离程度来确定待测序列随机性。如果偏离程度较小,序列是随机的。
  • 非重叠模板匹配测试:目的是检测待测序列中,子序列是否与太多的非周期模板相匹配。太多就意味着待测序列是非随机的。
  • 重叠模板匹配测试:目的是统计待测序列中,特定长度的连续“1”的数目,是否与真随机序列的情况偏离太大。太大是非随机的。
  • 通用统计测试:目的是检测待测序列是否能在信息不丢失的情况下被明显压缩。一个不可被明显压缩的序列是随机的。
  • 压缩测试:目的是确定待测序列能被压缩的程度,如果能被显著压缩,说明不是随机序列。
  • 线性复杂度测试:目的是确定待测序列是否足够复杂,如果是,则序列是随机的。
  • 连续性测试:目的是确定待测序列所有可能的m位比特的组合子串出现的次数是否与真随机序列中的情况近似相同,如果是,则序列是随机的。
  • 近似熵测试:目的是通过比较m位比特串与m-1位比特串在待测序列中出现的频度,再与正态分布的序列中的情况相对比,从而确定随机性。
  • 部分和测试:目的确定待测序列中的部分和是否太大或太小。太大或太小都是非随机的。
  • 随机游走测试:目的是确定在一个随机游程中,某个特定状态出现的次数是否远远超过真随机序列中的情况。如果是,则序列是非随机的。
  • 随机游走变量测试:目的是检测待测序列中,某一特定状态在一个游机游程中出现次数与真随机序列的偏离程度。如果偏离程度较大,则序列是非随机的。

1.4 xor-shift scheme

好了,我们还是来看看xor-shift scheme吧。根据Wiki上的描述,该方法在计算机上奇快,比较好,只是有一些统计学测试点没过......但如果通过该方法加上一些非线性函数就可以轻松过所有测试点,轻松虐 Mersenne Twister以及WELL

当然该方法不是没有问题,x,y,z几个变量的初始值还得好好选选。估计人家搞加密的比较关系这些问题,咱们是在算hash冲突啊!

O(∩_∩)O我们是在写Hash是吧,怎么越写越远啊O(∩_∩)O

1.5 小结

总之可以看到,目前对于一个Object的hashCode方法的第一次计算,默认情况下是通过xor-shift scheme的一个伪随机函数生成的。

那么第二次调用呢?事实上,hashcode值在第一次生成后会写入到内存里,和Object存储在一起(存在Object的头部),之后的调用直接从object里当一个property读出来就可以了。

当然,还有部分陈旧的实现(老版本的JDK里)是通过使用Object存储的地址实现的。但这也存在一个问题,就算你第一次是按照当前Object的地址生成,以后都直接读取,但你GC要是用并行GC或者 SerialGC的时候Object是会被移动的,移动之后原来的地址上再生成Object会不会很容易Hash冲突呢?

所以我觉得如果使用地址实现,至少也要再和GC Times进行一下位运算减少冲突次数。

至此为止,一个Object的Hashcode生成方法就是这样了。

2. HashMap && HashSet

HashSet通过HashMap实现,这就不用说啥了。

2.1 HashMap里的hash函数

唯一值得说一说的就是HashMap.hash(int)方法。该方法输入key.hashCode又进行了很多位运算才返回HashMap里给Key使用的Hash值。

为什么?

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

遗憾,还是没想明白,看别人的blog才明白的。

基本思路很简单,与HashMap自身结合也很紧密。

首先,hashMap是用一些桶存key,然后用拉链解决冲突的。而且每次扩容的时候是成倍扩容,所以通数量一定是1<<k的形式。

那么我们假设某时刻,一个hashmap.tablesize=1<<11,这时 两个objectkey要入库,一个是hashvalue=M,另一个是M+(1<<13)。我们算index函数:

 	static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

index函数直接取前10位,显然M&(1<<12-1)==(M+(1<<13))&(1<<12-1)
因为他们的低11位本来就相同啊!

看到问题了吧,虽然hash函数将每个obj与1<<32或者1<<64对应起来了,而且对应的很离散。但只要HashMap的size不是1<<32,那么就要把原hashcode 向一个更小的集合映射。而这个映射过程,就是这个HashMap里的hash过程。

该过程保证了任何一个高位(比如前面例子里M 与 M+1<<13)的移动,都会在低位产生对应的影响。而不是直接截取Hashcode里的低位,从而使冲突变得显而易见。

具体过程看图吧,太清楚了,感谢marystone。

图

其中h(h>>>7)(h>>>4) 结果中的位运行标识是把h>>>7 换成 h>>>8来看。

即最后h(h>>>8)(h>>>4) 运算后hashCode值每位数值如下:

8=8

7=7^8

6=678

5=587^6

4=47658

3=3865847

2=2754738^6

1=164386275

结果中的1、2、3三位出现重复位^运算

3=3865847 -> 36547

2=2754738^6 -> 25438^6

1=164386275 -> 1438275

算法中是采用(h>>>7)而不是(h>>>8)的算法,应该是考虑1、2、3三位出现重复位运算的情况。使得最低位上原hashCode的8位都参与了运算,所以在table.length为默认值16的情况下面,hashCode任意位的变化基本都能反应到最终hash table 定位算法中,这种情况下只有原hashCode第3位高1位变化不会反应到结果中。

关于Hashmap以及ConcurrentHashMap,和BloomFilter我们单开一章吧。

posted @ 2015-08-27 14:35  RobinMeng  阅读(734)  评论(0编辑  收藏  举报