前言
HashMap在JDK7和JDK8是有了一些不同的,具体体现如下:
- JDK7HashMap底层是数组+链表,而JDK8是数组+链表+红黑树
- JDK7扩容采纳头插法,而JDK8采纳尾插法
- JDK7的rehash是全副rehash,而JDK8是局部rehash。
- JDK8对于key的hash值计算相比于JDK7来说有所优化。
如果还有趣味的小伙伴能够学习学习我的以下文章,写的非常具体!!
高频考题:手写HashMap
JDK7、8扩容源码级详解
JDK7、8HashMap的get()、put()流程详解
JDK7 HashMap
JDK7HashMap在多线程环境下会呈现死循环问题。
如果此时A、B线程同时对一个HashMap进行put操作,且HashMap刚号达到扩容条件须要进行扩容
那么这两个线程都会取对HahsMap进行扩容(JDK7HashMap扩容调用 resize()办法,而resize()办法中须要调用transfer()办法将旧数组元素全副rehash到新数组中去==重点:这里在多线程环境下就会呈现问题==)
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //对数组的每一条链表遍历rehash for (Entry<K,V> e : table) { while(null != e) { //保留下一个节点 Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //失去对应在新数组中的索引地位 int i = indexFor(e.hash, newCapacity); //尾插法 e.next = newTable[i]; newTable[i] = e; e = next; } }}
咱们假如当初有一个链表 C——>D,且C、D扩容后计算的索引地位仍然不变,那他么还在同一链表中
当初A线程进入到transfer办法拿到C和它的下一个节点D(Entry<K,V> next = e.next;)后,A线程被挂起,此时B线程失常走流程将C、D rehash到新的数组中,那么依据头插法在新的数组中是D——>C
B执行完之后,A线程持续去执行
因为A获取到了 e = C,next = D,所以C能够进行rehash,C进行完后拿到D,发现D.next = C,所以D也能够进行rehash,那么此时因为D——>C,此时会再拿到C,发现C.next = null,但C不是null,所以C再进行rehash,此时链表尾 C——> D ——>C,因为此时e = NULL,所以退出循环,此时呈现死循环。C——>D——>C。
==各位能够好好想想这些话或者本人在草稿纸上画一画再来看上面的图!==
图示演示:
==B失常执行实现==
==A继续执行==
因为A获取到了 e = C,next = D,所以C能够进行rehash
C进行完后拿到e = D,发现D.next = C,所以D也能够进行rehash
那么此时因为D——>C,此时会再拿到C,发现C.next = null,但C不是null,所以C再进行rehash
此时e = NULL,所以退出循环,此时呈现死循环。C——>D——>C。
JDK8 HashMap
JDK1.8会呈现数据笼罩的状况
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}
- ==第6行代码==:假如两个线程A、B都在进行put操作,并且依据key计算出的hash值雷同,那么失去得索引下标也雷同,当线程A执行完第六行代码后因为工夫片耗尽导致被挂起,而线程B失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程A取得工夫片,因为之前曾经进行了hash碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程B插入的数据被线程A笼罩了,从而线程不平安。
- ==第38行代码==++size不平安,还是线程A、B,这两个线程同时进行put操作时,假如以后HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中取得size的值为10后筹备进行+1操作,然而因为工夫片耗尽只好让出CPU,线程B高兴的拿到CPU还是从主内存中拿到size的值10进行+1操作,实现了put操作并将size=11写回主内存,而后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,然而size的值只减少了1,所有说还是因为数据笼罩又导致了线程不平安。
<br/>
最初
我是 Code皮皮虾,一个酷爱分享常识的 皮皮虾爱好者,将来的日子里会不断更新出对大家无益的博文,期待大家的关注!!!
创作不易,如果这篇博文对各位有帮忙,心愿各位小伙伴能够==一键三连哦!==,感激反对,咱们下次再见~~~
==分享纲要==
大厂面试题专栏
Java从入门到入坟学习路线目录索引
开源爬虫实例教程目录索引
<font size="5">更多精彩内容分享,请点击 Hello World (●'◡'●)
欢送关注我的公众号,外延更多优质博文分享,期待您的退出!。