Collection
List
Set
Map
HashMap
hashMap实现原理:数组+链表
-
如何寻找bucket
1.使用扰动函数hash()根据key的hashCode计算出一个值hash
=> 扰动是为了减少碰撞;
2.hash&(n-1)
=> n为2的幂时,hash%n等价于hash&(n-1); -
为什么重写了equals方法必须重写hashCode方法?
详见Java基础部分Java基础 -
1.8中Node节点保存的数据
key、val、hash、next -
1.8中改为红黑树的阈值为什么是8?
本质上是为了进行时间与空间的平衡 =>TreeNode占用的空间比Node大;
红黑树的查找、插入、删除时间效率更高;当冲突的元素过少时,引入红黑树并未节约多少时间,反而造成了空间浪费
为什么恰好是8?
之所以是8,是因为Java的源码贡献者在进行大量实验发现,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此次操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8。 最后,红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。
详见:详解:HashMap红黑树的阈值为什么是8? -
HashMap线程安全问题
1.7使用头插法存在线程安全问题
若多个线程并发resize(),可能造成死循环void transfer(Entry[] newTable) { Entry[] src = table; // 旧的数组 int newCapacity = newTable.length; // newTable是扩容后的数组 // 从OldTable里摘一个元素出来,然后放到NewTable中 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; // 旧数组中每个bucket的第一个结点 if (e != null) { src[j] = null; // 释放旧数组中对该结点的引用 do { Entry<K,V> next = e.next; // 下一个元素 int i = indexFor(e.hash, newCapacity); // 计算e在新数组中的bucket位置 e.next = newTable[i]; // 头插法,因此e放在newTable[i]的前面 newTable[i] = e; // 更新newTable[i] e = next; } while (e != null); } } }
最终造成循环链表,从而
导致出现cpu100%
详情参考:JAVA HashMap的死循环
这里不太好理解,不过关键之处在于:头插法会导致原bucket中的元素在新bucket中形成逆序,这为死循环埋下了隐患
1.8使用尾插法
JDK1.8使用尾插法,即使并发情况下也不存在死循环