JDK1.8多线程使用HashMap丢数据原因分析
2019-02-27 本文已影响0人
Stalary
今天突然发现线上对 Kafka 消费者的监控出了问题,经过排查发现是在多线程的情况下使用了 HashMap 进行读写,下面来详细分析一下丢数据的原因。
首先写一个 demo ,模拟线上问题
public class Main {
private static Map<Integer, String> map = new HashMap<>(32);
static ExecutorService exec = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 10; i++) {
int finalI = i;
exec.submit(() -> {
map.put(finalI, "test:" + finalI);
});
}
TimeUnit.SECONDS.sleep(1);
System.out.println(map);
}
}
运行几次后会发现打印出了不正常的结果
{2=test:2, 3=test:3, 4=test:4, 5=test:5, 6=test:6, 7=test:7, 8=test:8, 9=test:9, 10=test:10}
我在初始化时设置初始容量为32,所以不会有扩容的问题,下面我们从源码找一找问题,首先我看一下 table 这个字段的解释
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
大体意思就是在第一次使用时进行初始化,然后会进行 resize ,下面看一下 put 调用的 putVal 中的几行代码
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
从这里我们可以发现在第一次调用 put 时会调用 resize ,下面看一下resize中的部分代码
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 一些复制操作
}
return newTab;
可以看出第一次调用 put 时会走到else中,对容量和阈值进行初始化,初始化完毕后我们可以看到,返回了一个新的table,这里在多线程环境下就会导致数据丢失的问题,所以如果我们要在多线程环境下使用map的话,还是推荐使用 ConcurrentHashMap 的。