参考文章:
https://blog.csdn.net/jiary5201314/article/details/51439982
https://blog.csdn.net/justloveyou_/article/details/62893086
https://www.zhihu.com/question/20733617/answer/111577937
程序员乔戈里面经
JDK7
hashmap的底层是数组+链表
从上图可以看出数组中每个节点是所在链表的头结点,每个节点存储键值对。
put方法
首先通过key.hashcode获取hashcode,然后rehash,具体的做法是与length-1做一个按位与,之所以不做取模运算,是因为在计算机中按位与比取模更快并且在该情况下两者效果是相同的。rehash后得到数组的index,然后看此位置有没有元素,如果没有,那么把键值对直接插入即可,如果此位置有元素,那么就需要对该位置的链表进行遍历,看链表中是否有与我们要插入的元素的key相同的元素,如果有,那么直接替换掉value,如果没有就进行头插。如下图所示。
rehash目的
上述put操作,我们得到的数组下标是通过rehash来完成的,rehash的目的就是让插入的元素尽可能在数组中分布均匀,因为如果分布不均匀,极端点来考虑,可能插入的所有节点都放到了同一个数组下标,最后退化为链表,大大降低查询效率。
resize
hashmap初始容量为16(2的幂,性能最高,可以看文章开头的三篇文章来理解),默认扩容的负载因子为0.75。因此当数组达到16*0.75=12时,会触发扩容,扩容过程十分消耗性能,因此我们最好预估我们需要多大的空间,在初始化时就尽量避免扩容。至于为什么扩容消耗性能,这是因为在jdk7版本下,扩容操作是这样的:
- 创建一个扩大一倍容量的数组
- 对原数组中每个元素重新计算下标
- 复制原数组的元素到新数组
显然此操作很消耗性能。在jdk8中采用了一种更巧妙的方法,此方法在下文会有介绍。
get
get方法的put方法类似,同样先拿到hashcode,然后rehash,最后遍历链表看有没有自己要查找的key,有就把value拿出来。没有返回null。
hashmap允许key/value为null
底层提供了一个putForNullKey,当put方法添加元素时,首先会判断该元素的key是否为null,如果key是null,则调用putForNullKey找到数字第一个元素,看该元素是不是空,如果是空,就直接插入。如果非空,则遍历该链表看是否有key为null的,如果有则替换value,没有的话就头插。
hashmap线程不安全的原因
由于哈希碰撞和扩容导致,扩容时容易形成环形链表,查询时候死循环
原理
现在假设你就是线程1,你的女朋友是线程2,你拿到这个数组后,把5这个节点的信息保存在你的栈里,你现在拿着一个key=5,next=8。
现在你只是保存了一个节点的信息,但是还没有对它进行操作,然后突然来了个电话让你办事去,结果你走了(挂起),你走了之后你的女朋友拿到这个hashmap把它扩容了,变成右边的hashmap了,然后就去睡觉了。这个时候你回来了,但你不知道你女朋友把hashmap的结构改了,结果你拿着你女朋友改过的hashmap的结构进行扩容。
结果成了这个样子。。。
结论:hashmap线程不安全。
jdk8的变化
数据结构变化
- jdk7:数组 + 链表
- jdk8:数组 + 链表 + 红黑树
增加红黑树是因为,即使hash函数再好,也不可避免的会有链表过长的情况出现,为了提高性能,使用了红黑树查询效率高的特点。
树化:链表长度 >= 8
链化:链表长度 <= 6
树化为什么是8?这一点我们可以看源码中的一段小注释:
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
大致意思就是:理想情况下,在随机哈希代码下,桶中的节点频率遵循泊松分布,桶长度k的频率表如上。由频率表可以看出,桶的长度超过8的概率非常非常小。作者以此为依据定义了树化长度。
链化为什么是6,博主在源码中并未找到解释(ps:有可能是我瞎了,如果有哪位找到了还请不吝赐教),我觉得是因为一个基于软件复杂度的设计理念。
rehash变化
- jdk7:key.hashcode & (length-1)
- jdk8: (key == null)? 0 : h=key.hashcode() ^ (h >>> 16)
可以看出jdk8采用了高16位异或低16位的机制进行rehash。使得得到的数组下标分布的更加不均匀。
扩容变化
- jdk7:新建数组,拷贝
- jdk8:使用2次幂的扩展,只需看扩容后数组的长度的二进制的第一位是0还是1,0则在原位置,1则变为“原位置 + 原数组容量”。