簡體   English   中英

computeIfAbsent 如何隨機使 ConcurrentHashMap 失敗?

[英]How does computeIfAbsent fail ConcurrentHashMap randomly?

我有以下代碼,它是一個玩具代碼,但可以重現該問題:

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應該受到責備的假設是錯誤的?

問題不在於computeIfAbsent調用,而在於最后的.add(key) :您可以讓多個線程嘗試將元素添加到同一個 HashSet,而沒有任何東西可以確保安全的並發訪問。 由於 HashSet 不是線程安全的,因此無法正常工作,並且 HashSet 有時會以損壞的 state 結束。 稍后,當您嘗試遍歷 HashSet 以獲取字符串時,它會因為這個損壞的 state 而崩潰。 (從您的例外情況來看, HashSet 認為其后備數組比實際更長,因此它試圖訪問越界數組元素。)

即使在沒有出現異常的運行中,您有時也可能最終“丟棄”本應添加的元素,但同時更新意味着丟失了一些更新。

ConcurrentHashMap.computeIfAbsent以原子方式執行,即一次只有一個線程可以訪問與給定鍵關聯的值。

但是,一旦返回值,就沒有這樣的保證。 HashSet可以被多個寫入線程訪問,因此不是線程安全的。

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

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

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

使用synchronized時不會出現異常這一事實應該已經說明了問題出在哪里。 如前所述,問題確實是HashSet ,因為它不是線程安全的。 這在收藏的文檔中也有說明。

請注意,此實現不同步。 be synchronized externally.如果多個線程同時訪問一個 hash 集,並且至少有一個線程修改了該集,則在外部進行同步。 這通常是通過在自然封裝集合的一些 object 上同步來實現的。

解決方案是使用synchronized塊或使用線程安全的CollectionView ,例如KeySetView ,您可以使用ConcurrentHashMap.newKeySet()獲得。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM