JDK1.8多线程使用HashMap丢数据原因分析

0.261字数 322阅读 99

今天突然发现线上对 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 的。

推荐阅读更多精彩内容