面试官:说一下HashMap原理,循环链表是如何产生的 - 知乎

掘金

1.在JDK1.7中,HashMap采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。
2.虽然JDK1.8采用了尾插法解决了这个问题,但是并发下的put操作也会使前一个key被后一个key覆盖。
3.由于HashMap有扩容机制存在,也存在A线程进行扩容后,B线程执行get方法出现失误的情况。

1、并发场景下出现死循环

多线程同时put时,如果同时调用了resize操作,可能会导致循环链表产生,进而使得后面get的时候,会死循环。下面详细阐述循环链表如何形成的。

resize函数

数组扩容函数,主要的功能就是创建扩容后的新数组,并且将调用transfer函数将旧数组中的元素迁移到新的数组。

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}//创建一个新的Hash TableEntry[] newTable = new Entry[newCapacity];//将Old Hash Table上的数据迁移到New Hash Table上transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}

transfer函数

transfer逻辑其实也简单,遍历旧数组,将旧数组元素通过头插法的方式,迁移到新数组的对应位置问题出就出在头插法。

void transfer(Entry[] newTable) {//src旧数组Entry[] src = table;int newCapacity = newTable.length;for (int j = 0; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;} while (e != null);//由于是链表,所以是个循环过程}}
}static int indexFor(int h, int length) {return h & (length-1);
}

下面举个实际例子:

① 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。

② 最上面的是old hash 表,其中的Hash表的size=2, 加载阈值为2∗0.75=1,所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。

③ 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

正常的Rehash的过程:

并发下的Rehash过程:

1)假设我们有两个线程,用红色和浅蓝色标注了一下。

我们再回头看一下transfer代码中的这个细节:

do {Entry<K,V> next = e.next; //假设线程一执行到这里就被调度挂起了int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;
} while (e != null);

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为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。

有人把这个问题报给了Sun,不过Sun不认为这是一个问题。因为HashMap本来就不支持并发,要并发就用ConcurrentHashmap。

这个循环链表问题只存在于JDK1.7中,在JDK1.8中使用了不同的扩容实现方式,所以不会出现这种情况。JDK1.8中HashMap是如何实现的我们后续讲解。

注:以下基于JDK1.8

2 多线程的put可能导致元素的丢失

2.1 试验代码如下

注:仅作为可能会产生这个问题的样例代码,直接运行不一定会产生问题

public class ConcurrentIssueDemo1 {private static Map<String, String> map = new HashMap<>();public static void main(String[] args) {// 线程1 => t1new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 99999999; i++) {map.put("thread1_key" + i, "thread1_value" + i);}}}).start();// 线程2 => t2new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 99999999; i++) {map.put("thread2_key" + i, "thread2_value" + i);}}}).start();}
}
复制代码

2.2 触发此问题的场景

先来看一下put方法的源码

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, I;// 初始化hash表if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 通过hash值计算在hash表中的位置,并将这个位置上的元素赋值给p,如果是空的则new一个新的node放在这个位置上if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// hash表的当前index已经存在元素,向这个元素后追加链表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) { // #1p.next = newNode(hash, key, value, null); // #2if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}
复制代码

假设当前HashMap中的table状态如下:

此时t1和t2同时执行put,假设t1执行put(“key2”, “value2”),t2执行put(“key3”, “value3”),并且key2和key3的hash值与图中的key1相同。

那么正常情况下,put完成后,table的状态应该是下图二者其一

下面来看看异常情况

假设线程1、线程2现在都执行到put源代码中#1的位置,且当前table状态如下

然后两个线程都执行了if ((e = p.next) == null)这句代码,来到了#2这行代码。

此时假设t1先执行p.next = newNode(hash, key, value, null);

那么table会变成如下状态

紧接着t2执行p.next = newNode(hash, key, value, null);

此时table会变成如下状态

这样一来,key2元素就丢了。

3. put和get并发时,可能导致get为null

场景:线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。

分析如下:

先看下resize方法源码

大致意思是,先计算新的容量和threshold,在创建一个新hash表,最后将旧hash表中元素rehash到新的hash表中

重点代码在于#1和#2两句

// hash表
transient Node<K,V>[] table;final Node<K,V>[] resize() {// 计算新hash表容量大小,beginNode<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 计算新hash表容量大小,end@SuppressWarnings({"rawtypes","unchecked”})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // #1table = newTab; // #2// rehash beginif (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}// rehash endreturn newTab;
}
复制代码

在代码#1位置,用新计算的容量new了一个新的hash表,#2将新创建的空hash表赋值给实例变量table。

注意此时实例变量table是空的。

那么,如果此时另一个线程执行get时,就会get出null。

HashMap线程安全性问题相关推荐

  1. HashMap和Hashtable 线程安全性

    HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题.HashMap的工作原理.ArrayList与Vect ...

  2. Java集合(实现类线程安全性)

    转载自  Java集合(实现类线程安全性) 1.集合和Map 下图是Java集合的Collection集合体系的继承树: 下图是Java的Map体系的继承树: 对于Set.List.Queue和Map ...

  3. java 线程安全性_我如何测试Java类的线程安全性

    java 线程安全性 我在最近的一次网络研讨会中谈到了这个问题,现在是时候以书面形式进行解释了. 线程安全是Java等语言/平台中类的重要品质,在Java中我们经常在线程之间共享对象. 缺乏线程安全性 ...

  4. 如何测试Java类的线程安全性

    我在最近的一次网络研讨会中谈到了这个问题,现在是时候以书面形式进行解释了. 线程安全是Java等语言/平台中类的重要品质,我们经常在线程之间共享对象. 缺乏线程安全性导致的问题很难调试,因为它们是零星 ...

  5. java 线程安全list_JAVA并发编程实战-线程安全性

    线程安全性: 对象的状态是指存储在状态变量(例如实例和静态域)中的数据. 对象的状态可能包括其他依赖对象的域. 例如:某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.En ...

  6. Java多线程编程(3)--线程安全性

    一.线程安全性   一般而言,如果一个类在单线程环境下能够运作正常,并且在多线程环境下,在其使用方不必为其做任何改变的情况下也能运作正常,那么我们就称其是线程安全的.反之,如果一个类在单线程环境下运作 ...

  7. java中什么是线程安全_Java 多线程:什么是线程安全性

    线程安全性 什么是线程安全性 <Java Concurrency In Practice>一书的作者 Brian Goetz 是这样描述"线程安全"的:"当多 ...

  8. 为什么HashMap线程不安全?以及实现HashMap线程安全的解决方案

    一.为什么HashMap线程不安全? 原著参考 1.JDK1.7 扩容引发的死循环和数据丢失 (1).当前jdk1.7版本的HashMap线程不安全主要是发生在扩容函数中,其中调用了HshMap的tr ...

  9. HashMap线程安全问题详细解析

    1.简介 HashMap是一种非线程安全的数据结构,即在多线程环境下,无法保证其操作的原子性和一致性.在多个线程同时访问HashMap并进行修改操作时,可能会导致数据的不一致性和线程竞争条件的出现. ...

最新文章

  1. 代理详解 静态代理+JDK/CGLIB 动态代理实战
  2. python【力扣LeetCode算法题库】面试题59 - II- 队列的最大值
  3. 不生成新数组的迭代器方法:forEach()every()some()reduce()reduceRight()
  4. Tomcat源码解析七:Tomcat Session管理机制
  5. 反射机制 Class.getDeclaredMethod
  6. 推荐系统之---如何理解低秩矩阵?
  7. 在SQL Server中批量复制,导入和导出的技术
  8. Win10蓝牙鼠标老是断连卡顿的解决方法
  9. Docker镜像保存save、加载load
  10. 机器学习笔记(十三):主成分分析法(PCA)
  11. Excel如何快速制作文件目录
  12. AE zoom to selected 地图刷新
  13. 富士通服务器irmc账号密码,PRIMERGY TX1330 M2 E3-1200 V5单路 Fujitsu富士通立式服务器...
  14. graphviz linux教程,linux下做图工具——graphviz安装配置
  15. hbase 报:Java::JavautilConcurrent::TimeoutException:The procedure 1 is still running
  16. 【转】systemd环境变量的小坑
  17. homework-08
  18. 爬虫-6-selenium和phantomJSheadless
  19. AV终结者技术大曝光(另附AV终结者10大死法)
  20. Linux安装Git(图文解说详细版)

热门文章

  1. linux中给普通用户添加root用户权限
  2. 优品商城-建表(user、member-goods、goods_cart、category、order、spec-address、province、city、county)
  3. 如何使用E邮宝的热敏标签纸打印亚马逊外箱单?
  4. PPC气箱脉冲除尘器
  5. Mac解决JAVA_HOME问题
  6. 事件监听 ActionListener
  7. php多线程原子操作,C语言线程互斥和原子操作
  8. C/C++程序内存布局(data段,bss段,text段)以及static关键字详解
  9. 穿越存在吗?诺奖得主基普·S·索恩:人类穿梭时间可能摧毁自己
  10. 遍历Java中的列表