如何calculateIfAbsent随机使ConcurrentHashMap失败?

时间:2020-05-10 20:08:00

标签: java multithreading concurrency concurrenthashmap

我有以下代码,这是一个玩具代码,但可以重现该问题:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;

public class TestClass3 {
    public static void main(String[] args) throws InterruptedException {
        // Setup data that we will be playing with concurrently
        List<String> keys = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");

        HashMap<String, List<Integer>> keyValueMap = new HashMap<>();
        for (String key : keys) {
            int[] randomInts = new Random().ints(10000, 0, 10000).toArray();
            keyValueMap.put(key, stream(randomInts).boxed().collect(toList()));
        }

        // Entering danger zone, concurrently transforming our data to another shape
        ExecutorService es = Executors.newFixedThreadPool(10);
        Map<Integer, Set<String>> valueKeyMap = new ConcurrentHashMap<>();
        for (String key : keys) {
            es.submit(() -> {
                for (Integer value : keyValueMap.get(key)) {
                    valueKeyMap.computeIfAbsent(value, val -> new HashSet<>()).add(key);
                }
            });
        }
        // Wait for all tasks in executorservice to finish
        es.shutdown();
        es.awaitTermination(1, TimeUnit.MINUTES);
        // Danger zone ends..

        // We should be in a single-thread environment now and safe
        StringBuilder stringBuilder = new StringBuilder();
        for (Integer integer : valueKeyMap.keySet()) {
            String collect = valueKeyMap
                    .get(integer)
                    .stream()
                    .sorted()  // This will blow randomly
                    .collect(Collectors.joining());
            stringBuilder.append(collect);  // just to print something..
        }
        System.out.println(stringBuilder.length());
    }
}

当我一遍又一遍地运行此代码时,它通常将无任何异常地运行并且会打印一些数字。但是随着时间的推移(大约每10次尝试中的1次),我将得到类似于以下的异常:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 6
    at java.util.stream.SortedOps$SizedRefSortingSink.accept(SortedOps.java:369)
    at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1556)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:566)
    at biz.tugay.TestClass3.main(TestClass3.java:40)

我可以肯定这与

有关
valueKeyMap.computeIfAbsent(value, val -> new HashSet<>()).add(key);

如果按以下方式更改此部分,则永远不会出现异常:

synchronized (valueKeyMap) {
    valueKeyMap.computeIfAbsent(value, val -> new HashSet<>()).add(key);
}

我正在思考 computeIfAbsent仍在修改valueKeyMap,即使所有线程都已完成。

有人可以解释一下这段代码是如何随机失败的,原因是什么?还是有完全不同的原因使我无法看到,或者我错误地认为应该归咎于computeIfAbsent

3 个答案:

答案 0 :(得分:9)

问题不在computeIfAbsent调用中,而是在结尾的.add(key)中:您可以有多个线程尝试向同一HashSet中添加元素,而没有任何东西可以确保安全的并发访问。由于HashSet不是线程安全的,因此无法正常工作,并且HashSet有时会处于损坏状态。稍后,当您尝试遍历HashSet以获取字符串时,由于此损坏状态而使其崩溃。 (从您的异常来看,HashSet认为其后备数组比实际更长,因此它试图访问越界数组元素。)

即使在运行过程中出现异常,您有时也可能最终会丢失应该添加的元素,但是并发更新意味着某些更新丢失了。 / p>

答案 1 :(得分:4)

ConcurrentHashMap.computeIfAbsent是原子执行的,也就是说,一次只有一个线程可以访问与给定键关联的值。

但是,返回值后就没有这种保证。 HashSet可以被多个写入线程访问,因此不能安全地线程访问。

相反,您可以执行以下操作:

valueKeyMap.compute(value, (k, v) -> {
    if (v == null) {
      v = new HashSet<>();
    }
    v.add(key);
    return v;
});

之所以有效,是因为compute也是原子的。

答案 2 :(得分:1)

使用synchronized时没有出现异常这一事实应该已经阐明了问题所在。如前所述,该问题确实是HashSet,因为它不是线程安全的。该集合的文档中也对此进行了说明。

请注意,此实现未同步。。如果多个线程同时访问哈希集,并且至少有一个线程修改了哈希集,则必须在外部对其进行同步。 。通常,通过对自然封装了该集合的某个对象进行同步来实现。

解决方案是使用synchronized块,或者使用线程安全的CollectionView,例如KeySetView,您可以使用ConcurrentHashMap.newKeySet()来获得。