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

来源:互联网 发布:淘宝怎么卖东西 编辑:程序博客网 时间:2024/05/22 05:00

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;}