您的位置:首页 > 其它

HashMap多线程操作下的问题总结

2017-09-04 00:13 267 查看

HashMap多线程操作下的问题总结

前段时间海外库存系统隔一段时间就会出现CPU使用率告警。最终排查出来,是由于海外库存在接收多线程数据查询结果时,使用了一个普通的HashMap来接收,也就是多个线程对同一个HashMap进行非线程安全的put操作导致的。经证实,海外库存的数据查询偶尔出现非预期结果,也与此有关:比如有库存的商品,查出来却是0等等。

HashMap多线程操作会造成一系列问题,这很多人都知道。但反过来根据现象查问题,可能就不那么明显了。因此这里对多线程下HashMap使用会造成的问题做个小总结,以供大家“根据现象反查问题”作参考。

问题1. 导致死循环,CPU使用率飙升

特征1:生产环境某个实例CPU使用率飙升,并且多次thread dump显示同一个线程在很长一段时间内一直在对同一个HashMap在做put、get、或者遍历操作。

特征2:每当一个线程进入死循环,就会占用100%/CPU核数 的CPU利用率,我们的生产环境是4核CPU,因此可以看 到,生产环境上有25%左右、50%左右、75%左右以及99%的CPU利用率(99%利用率的时候会告警,我们就去重启了,所以这里看不到)。

下图左边是 修复HashMap多线程使用前,日常的CPU利用率,右图是修复后一周的日常CPU利用率。



大致原理

HashMap的内部存储结构如下,由一个数组,以及数组上的链表组成:



其中,key的Hash值与数组长度取模得出的值相等的元素将会放在同一个链表上。而HashMap查找的过程,实际就是根据key计算hash值,与当前长度取模,从而定位到数组中的某个链表,然后再从这个链表上进行遍历查找数据。

在put的过程中,随着hashmap元素个数的增长,链表越来越长,Map查找的效率会越来越低。因此当数量增长到一定时候,一般是为 元素个数 > 数组length * loadFactor。loadFactor默认0.75,可以自己定义。就会对数组进行扩容,并且遍历原来的HashMap中的所有元素,将原有元素全部重新put到新的数组及链表中。

在多线程情况下,put的过程在操作同一个链表时,会形成如下循环链表(这里要讲篇幅就长了,网上关于HahsMap扩容过程的资料很多)。当进行get查询,或者遍历操作的时候,就会进行链表的死循环遍历,从而导致CPU占用彪高。



问题2. Map.size()与实际不合

多线程环境下put的HashMap会被“损坏”,其中会造成size与实际不符合,以下代码中,如果有幸没有进入死循环,assert断言有很大概率不会通过。

Map<String, Object> testMap = new HashMap<>();
// TODO 多线程环境下对testMap进行put操作 ...
// 。。。 。。。
// 多线程对testMap进行put操作完成

int realSize = 0;
for (Entry entry : testMap.entrySet()){
realSize += 1;
}
// assert 很大概率失败
assert realSize == testMap.size();


Map.size与实际不合将会导致一些依赖Map.size的业务逻辑出现不可预知的异常。

问题3. 数据丢失

多线程环境下put的HashMap会造成数据丢失,明明put进去的数据,却get不到了。

下边的一段代码中,如果运气好,没有进入死循环,那么assert断言也有很大可能性过不了。

Map<Integer, Object> testMap = new HashMap<>();
Object val = new Object();
CountDownLatch cdl = new CountDownLatch(100);
for (int i = 0; i < 100 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = i * 100; j < (i + 1) * 100 ; j++ ) {
testMap.put(j, val);
}
cdl.countDown();
}
}).start();
}
cdl.await();
for (int i = 0; i < 10000; i++ ) {
// assert 很大概率会失败
assert testMap.get(i) != null;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息