Java源码解析|HashMap的前世今生 (2)

1.hashmap1.7线程不安全

Alt text


并发下并发下,扩容时,需要使用transfer函数拷贝链表数据,有坑,容易出现死循环链表,死锁

查看参考链接

2.hash碰撞的安全问题
Java1.7中的hash算法会出现碰撞,可以通过恶意请求引发DOS
如下,hash值相同

Alt text

解决方法:换一种hash计算方法

Alt text


Alt text

今生——Java 1.8 底层数据结构

HashMap底层的数据结构是:数组+链表+红黑树

当链表的长度大于等于8时,链表会转化成红黑树;

当红黑树的大小小于等于6时,红黑树会转化成链表;
整体的数据结构如下:

Alt text

扩容与初始化

常见属性:

//初始容量为 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //负载因子默认值 static final float DEFAULT_LOAD_FACTOR = 0.75f; //桶上的链表长度大于等于8时,链表转化成红黑树 static final int TREEIFY_THRESHOLD = 8; //桶上的红黑树大小小于等于6时,红黑树转化成链表 static final int UNTREEIFY_THRESHOLD = 6; //当数组容量大于 64 时,链表才会转化成红黑树 static final int MIN_TREEIFY_CAPACITY = 64; //记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast transient int modCount; //HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化) transient int size; //存放数据的数组 transient Node<K,V>[] table; // 扩容的门槛,有两种情况 // 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。 // 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75 int threshold; //链表的节点 static class Node<K,V> implements Map.Entry<K,V> { //红黑树的节点 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>

可以看到初始容量为16,最大容量为2的30次方

当数组容量大于 64 时,并且链表结点数>=8时,链表才会转化成红黑树
而转化成红黑树的概率是非常小的(千万分之一),原因是一个合适的hash计算不会很少出现多次碰撞
在考虑设计链表结点数>=8这个值的时候,我们参考了泊松分布概率函数,由泊松分布中得出结论,链表各个长度的命中概率为:

* 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

意思是,当链表的长度是8的时候,出现的概率是0.00000006,不到千万分之一,所以说正常情况下,链表的长度不可能到达8,而一旦到达8时,肯定是hash 算法出了问题,所以在这种情况下,为了让HashMap仍然有较高的查询性能,所以让链表转化成红黑树,我们正常写代码,使用HashMap时,几乎不会碰到链表转化成红黑树的情况。

扩容
扩容有两种情况:

如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组的容量大小会近似一下,数组大小永远是 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,也就是 2 的 5 次方。

如果是通过 resize 方法进行扩容,当大小 > 数组容量 * 0.75进行resize

Alt text


Alt text

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wspxsp.html