这个问题是在面试时常问的几个问题,通常在问这个问题以前会问Hashmap和HashTable的区别?面试者通常会回答:hashtable是线程安全的,hashmap是线程不安全的。java
那么面试官就会紧接着问道,为何hashmap不是线程安全的,会形成什么问题么?因而面试者就回答:HashMap在并发状况下的put操做会形成死循环。面试
这时候就会被面试官问:HashMap在并发为何形成死循环?安全
不少面试者这时候就会一脸懵。没有过相关经验和深刻的理解源码是很难回答这个问题的。多线程
下面咱们就经过HahMap源码来验证下,多线程并发put操做为什么会生成环形链表,产生死循环。并发
这是HashMap扩容的源码spa
/** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; 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; } // while } }
开始以前先回顾一下HashMap的扩容机制:
HashMap默认设定的装载因子为0.75(可改),HashMap的大小为length,已经装载的元素数量为num,当( num / length )> 装载因子时,
开始扩容线程
先建立一个散列表HashMap:Map<Integer> map = new HashMap<Integer>(2);
,装载因子默认0.75,当插入第二个元素时,会发生扩容
咱们先在map中放入六、8两个元素。设计
这时有两个线程都执行put操做,那么在此刻两个线程都对HashMap进行扩容,这时候就注意在上文的源码里注释为(关键代码)这一行:Entry<K,V> next = e.next;
3d
假如两个线程分别为A、B两个线程。A线程在执行到关键代码
这一行线程就被挂起,那么此刻A线程中:e = 6; next = 8;
code
接着B线程开始进行扩容,假设新的散列表中,节点6 和 节点8 仍是会产生散列冲突,那么线程B的扩容过程为:
<img src="https://upload-images.jianshu.io/upload_images/2710833-091c030487692445.png" alt="申请两倍大小的空间" style="zoom:33%;" />
此时线程B的扩容已经完成,节点8 的后继节点为节点6 ,节点6的后继节点为null。
咱们将新旧两个散列表作个对比:
回顾一下线程A的当前状态:e = 6; next = 8;
,处于挂起状态。接着A线程取消挂起状态,接着执行(关键代码)以后的代码:将e = 6;
节点迁移至新的散列表,并将next = 8
的节点赋值给e
。扩容并迁移节点6后的状态,以下图所示:
因而第二次执行while循环时,当前待处理节点:e = 8;
在执行(关键代码)这一行时,因为线程B在扩容时将节点8的后继节点变为节点6,因此next不是为null,而是next = 6;
接着开始执行第三次while循环,因为节点6的后继节点为null,因此 next = null;
,执行完第三次while循环的结果为:
循环结束。
能够看到扩容后的散列表中链表成环,若是这时候执行get()
方法查询,就会致使死循环。
HashMap的方法不是线程安全的。HashMap在并发执行put操做时发生扩容,可能会致使节点丢失,产生环形链表等状况。
在jdk1.7中,因为扩容时使用头插法,在并发时可能会造成环状列表,致使死循环,在jdk1.8中改成尾插法,能够避免这种问题,可是依然避免不了节点丢失的问题。
HashMap的设计初衷就不是在并发状况下使用,若是有并发的场景,推荐使用ConcurrentHashMap