HashMap多线程并发问题
HashMap多线程并发问题
HashMap并非线程安全的,在多个线程put时,会造成key之间的死循环。当另一个线程调用这个key时,get()方法会一直执行,导致线程积压,最终造成CPU满。
问题原因分析
HashMap结构
HashMap通过一个数组table[]来存储key,当放入一个key时,通过hash算法计算key的下标,并存储在数组的table[i]处。如果table[]的尺寸很小,当放入多个key时,可能会出现下标相同,这样就会在table[i]处形成链表。这时,原本查找一个key需要耗时O(1),现在耗时变成了O(n),n为链表的长度。
因此,为了提高查找效率,当有新的key值存入时,会检查Hash表的大小是否超过设定的thredhold,超过的话就会增加Hash表的大小,这样子Hash表里的元素会重新计算一遍,这个过程叫做rehash。
正常的Rehash过程
假设原先hash的size是2,存放了三个元素a、b、c,都在table[1]这里,rehash后size为4。
取出第一个key值a,计算hasn值为3,存放在table[3];
取出第二个key值b,计算hash值为1,存放在table[1];
取出第三个key值c,计算hash值为3,存放在table[3],与key值a形成链表,且c的next指向了a。
并发的Rehash过程
如果存在线程1和线程2,在rehash之前中,a、b、c在table[1]形成了链表,a的next指向了b,这时发生了put操作,两个线程同时进行了rehash。
线程1在遍历Hash表元素中,取a.next时被挂起。
线程2继续完成了rehash操作,重组了链表,重组结束后,b.next指向了a。
线程1继续执行,a.next又指向了b,环形链表因此产生了。
这时,当我们调用到同一链表的其他值时,就会出现死循环,线程一直会执行下去。
解决方案
HashTable
HashTable是同步的,对此对HashTable进行操作时,都会锁住整个结构。如果并发地修改,会抛出异常。
HashTable不支持在遍历时修改自身的元素,否则会抛出ConcurrentModificationException。
ConcurrentHashMap
ConcurrentHashMap的锁是细粒度的,将hash表分为16个桶(默认),每次需要时只会锁当前用到的桶。而且在读是不会锁表,完全支持并发操作。只有在size()时会锁住整个表,因此ConcurrentHashMap并发时效率更高。
此外,ConcurrentHashMap则是在遍历迭代时发生改变不会抛出异常。