欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

HashMap学习笔记

程序员文章站 2022-03-10 10:48:31
HashMap线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。JDK1.7的Entry(JDK1.8改成了 Node):static class Entry implements Map.Entry {final K key;V value;Entry next;int hash;/...

HashMap线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

JDK1.7的Entry<K,V>(JDK1.8改成了 Node):

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;//key的hashcode值
......
}

JDK1.7 扩容代码:

void transfer(Entry[] newTable, boolean rehash) {
//新hashMap的大小
int newCapacity = newTable.length;
//重新hash原数组(table)的数据找到新数组(newTable)中的新位置
for (Entry<K,V> e : table) {
 //链表中最后一个元素的next为null则退出,进入上一层继续对数组中的下一个链表中的元素进行操作
 while(null != e) {
   //初始化next指向操作元素的下一个,防止丢失
   Entry<K,V> next = e.next;
   if (rehash) {
     e.hash = null == e.key ? 0 : hash(e.key);
   }
   //计算新位置,i可能是新的(无值)可能是旧的(有值)
   int i = indexFor(e.hash, newCapacity);
   //操作元素指向新位置的元素(头插法),此时e是操作元素和原i位置元素的链表
   e.next = newTable[i];
   //新数组newTable第i个赋值e(操作元素和原i位置元素的链表),至此完成对一个元素的重新hash
   newTable[i] = e;
   //对e重新赋值下一个要操作的元素(即链表的下一个元素)
   e = next;
 }
}
}

扩容会造成死循环的简单描述:
线程A,线程B同时对HashMap进行扩容操作,线程A操作到 newTable[i] = e; 时CPU分配的时间片用完*挂起,而线程B完成了扩容过程。当线程A再次被唤醒时会继续从中断位置开始执行,table和newTable是线程B扩容后的了,由于头插法会导致以后循环的时候产生闭环。 扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。

多线程put时可能会导致数据丢失:
主要问题出在addEntry方法的new Entry (hash, key, value, e),如果两个线程都同时取得了e,则他们下一个元素都是e,然后赋值给table元素的时候有一个成功有一个丢失。

JDK1.8 put值代码:

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;
}

JDK1.8的扩容做了优化,每次扩容都是原长度的2倍, 所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash值了。

JDK1.8的hash方法做了优化,低16位和高16位进行异或计算,这样就保证在数组比较小时候hashcode的高低位都能参与到运算中,防止一些散列不均匀的情况。

参考博客(有图):https://blog.csdn.net/swpu_ocean/article/details/88917958

Q&A:

Q:Hashtable扩容的数组长度为什么是旧数组长度乘以2加1?

A:Hashtable中数组的长度尽量为素数或者奇数,同时Hashtable采用取模的方式来计算数组下标,这样减少Hash碰撞,计算出来的数组下标更加均匀。但是这样效率会比HashMap利用位运算计算数组下标低。

Q:Hashtable为什么采用头插法的方式迁移数组?

A:采用头插法的方式效率更高。如果采用尾插法需要遍历数组将元素放置到链表的末尾,而采用头插法将结点放置到链表的头部,减少了遍历数组的时间,效率更高。

Q:JDK1.8前HashMap也是采用头插法迁移数据,多线程情况下会造成死循环,JDK1.8对HashMap做出了优化,为什么JDK1.8Hashtable还是采用头插法的方式迁移数据?

A:Hashtable是线程安全的,所以Hashtable不需要考虑并发冲突问题,可以采用效率更高的头插法。

Q:为什么Hashtable渐渐被弃用?

A:Hashtable使用synchronized来实现线程安全,在多并发的情况下效率低下。

本文地址:https://blog.csdn.net/qq_36042338/article/details/107689175

相关标签: java