HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。 只是在JDK1.8中,链表长度大于8的时候,链表会转成红黑树!
简单来说:
Put过程:
简单来说:按照数组–>链表–>红黑树–>扩容的顺序进行数据插入。
Get过程:
当发生哈希冲突并且size大于阈值(loadfactor*current capacity)的时候,需要进行数组扩容,扩容时,需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去(按照Put过程进行),扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。
为什么扩容是2的次幂?
HashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。
简单而言
HashMap不是线程安全的,若是并发的进行ReHash,可能因为种种巧合,使得在链表中出现环形链接,一旦进行Get操作,将陷入死循环。以下是来自coolshell的一个例子,详情请阅读原文。
正常的ReHash的过程
并发下的Rehash
1)假设我们有两个线程。而我们的线程二执行完成了。于是我们有下面的这个样子。
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
2)线程一被调度回来执行。
先是执行 newTalbe[i] = e;
然后是e = next,导致了e指向了key(7),
而下一次循环的next = e.next导致了next指向了key(3)
3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
4)环形链接出现。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。