web-dev-qa-db-ja.com

どのようにしてcomputeIfAbsentが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);
}

私はthinkingcomputeIfAbsentは、すべてのスレッドが終了した後でもvalueKeyMapを変更しています。

誰かがこのコードがランダムに失敗する理由を説明できますか?その理由は何ですか?または、私がおそらく見ることができず、computeIfAbsentが原因であるという私の推測が間違っている、まったく異なる理由がありますか?

8
Koray Tugay

synchronizedを使用するときに例外が発生しないという事実は、問題がどこにあるのかについてすでにいくつかの光を放っているはずです。すでに述べたように、スレッドセーフではないため、問題は確かにHashSetです。これは、コレクションのドキュメントにも記載されています。

この実装は同期されないことに注意してください。複数のスレッドが同時にハッシュセットにアクセスし、少なくとも1つのスレッドがそのセットを変更する場合、 しなければならない 外部と同期する。これは通常、セットを自然にカプセル化するいくつかのオブジェクトで同期することによって行われます。

解決策は、synchronizedブロックを使用するか、ConcurrentHashMap.newKeySet()を使用して取得できるCollectionViewなどのスレッドセーフKeySetViewを利用することです。

1
Matthew Formosa