HashMap多线程中引发的死循环
串叔发现的问题,觉得很有意思,已有文章作详细描述,把原文翻译地讲一下。原文:http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html。
Java的HashMap本身就不支持多线程,是线程不安全的,不过代码中有一些机制来阻止多线程中的不安全因素,比如在多线程修改操作中抛出ConcurrentModificationException,但是HashMap忽视了数组扩容时候这一修改操作,从而导致可能在多线程中进行了不安全的操作。
java.util.HashMap 的 transfer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry e = src[j]; if (e != null) { src[j] = null; do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } } |
假设现在HashMap中的数组的size为2(桶0和桶1),现在有2个Entry A{key=3}和 B{key=7},于是都位于桶1中形成链表,假设A在B之前,就是array[1]=>A=>B=>null。
假设有2个线程T1和T2同时进行扩容操作,即会同时调用transfer方法。假设T1先于T2抢到CPU执行transfer方法执行到第9行后切换到T2执行,然后T2顺利执行完毕,然后T1继续执行完毕,看一下这个过程的具体。
T1先执行到第9行完毕,变量e=A,next=B。
T2顺利执行完毕,新的数组size为4(桶0-3),A和B还是hash到了同一个桶中,变为T2_newArray[3]=>B=>A=>null,此时A.next=null,B.next=A。
T1继续执行,第一次循环完毕,T1_newArray[3]=>A=>null,e=B,next=B,e!=null进入下一个循环,此时A.next=null,B.next=A。
第二次循环完毕,T1_newArray[3]=>B=>A=>null,e=A,next=A,e!=null进入下一个循环,此时A.next=null,B.next=A。
第三次循环完毕,T1_newArray[3]=>A<=>B,e=null,next=null,e==null跳出循环,此时A.next=B,B.next=A。此时桶3的EntryA和EntryB的next指针互相指向对象,没有一个指向null。
因为T1后执行完毕,HashMap采用T1_newArray作为新的array(见HashMap的resize方法)。
T1和T2的扩容操作完毕,并没有引发任何异常和错误,只是埋下了隐患。
当HashMap继续执行其他get和put操作时,并且得到的hash值为3,即在桶3上进行操作问题就出现了,会进入无限循环,而没有抛出任何异常。
总结
不要信任HashMap防止多线程错误的代码,不能对HashMap进行多线程修改的操作,并发读是可以的。
对HashMap进行线程安全的办法有很多,比如使用java.util.concurrent.ConcurrentHashMap或者使用java.util.Collections.synchronizedMap(Map<K,V> m)方法包装HashMap,或者在自己调用HashMap的方法中使用同步机制。
HashMap在你使用的过程中,进行扩容和减容,都会引起异常。 以前经常遇到。bug还挺隐蔽的。
这就是死循环吗??? 之前还是不知道的呢!!!