文章部分代码图片和总结来自参考资料
哈希和常用的方法
散列,从中文字面意思就很好理解了,分散排列,我们知道数组地址空间连续,查找快,增删慢,而链表,查找慢,增删快,两者结合起来形成散列表。如下图。
常见的hash 散列方法有 :
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。(ThreadLocalMap中取的斐波那契数列数 0x61c88647 )
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
散列后难免有碰撞 ,下面是解决碰撞的方法 :
开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。(ThreadLoalMap)
链地址法
将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。(hashMap)
再哈希法
当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
建立公共溢出区
将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
源码解析
我们知道Object类型有个hashCode()方法,那么假如让你设计散列表,我们的直接想法肯定是直接对象的hashCode()对面讴歌数取模就够了,为什么呢,首先理解一下取模,以我们的时钟为例,12 个指针,13对12 取模就是1 (相当于下午1时),同理,要是24小时,对12取模,得到地数值很均匀。我们知道取模操作是“%”,处于效率考虑,一般使用位运算来代替(&运算)。
X % 2^n = X & (2^n - 1)
2^n表示2的n次方,也就是说,一个数对2^n取模 == 一个数和(2^n - 1)做按位与运算 。
假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。
此时X & (2^3 - 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
下面看源码分析。
HashMap
hash方法计算出hash值,而indexFor 计算出该元素在散列表中的位置。indexFor方法很好理解啊,就是上面的(2的N次方-1),当时hash方法的一波操作是什么意思呢?我们来看不同的三个数的hash值。
可以看到后两个数的高位不同,低位相同,产生hash冲突,下图是后两个数经过一波操作后,得到的hash值,可以看到经过这样的操作就解决了冲突。 我们思考一下为什么会冲突,是因为 &运算,0和谁&都会得到0
于是我们得出了如下结论
这段代码是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
同时还有一点,Object的hashCode方法会有负数,hashmap使用位运算,得到的hash值都是正整数(可以想一下为什么)
HashTable in Java7
我们在JDK1.8 中 HashTable 的put方法中看到 :
1 int hash = key.hashCode(); 2 int index = (hash & 0x7FFFFFFF) % tab.length;
前面的 ‘0 & 0x7FFFFFFF’ 是去绝对值的意思,后面直接取模了。需要注意的是 HashTable 默认的初始大小为11,之后每次扩充为原来的2n+1,也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。可参考:http://zhaox.github.io/algorithm/2015/06/29/hash)
ConcurrentHashMap In Java 7
有了上面 hashmap的扰动运算的介绍,应该很好理解了。
HashMap In Java 8
直接的链地址法,就是冲突了就在后面增加一个节点的方法有什么坏处呢?在最坏的情况下,这种方式会将HashMap的get方法的性能从 O(1)
降低到 O(n)----有可能所有的数据生成在一条链表,即每个都冲突
。Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从 O(n)
提高到 O(logn)
。
我们再看一下hash函数。
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的。以上方法得到的int的hash值,然后再通过
h&(table.length-1)
来得到该对象在数据中保存的位置。
ConcurrentHashMap In Java 8
Java 8的ConcurrentHashMap作者认为引入红黑树后,即使哈希冲突比较严重,寻址效率也足够高,所以作者并未在哈希值的计算上做过多设计,只是将Key的hashCode值与其高16位作异或并保证最高位为0(从而保证最终结果为正整数)。
参数资料 :
- Hollis 公众号里的“全网把 Map 中的 hash() 分析的最透彻的文章,别无二家”一文
- 从 ThreadLocal 的实现看散列算法