当写入操作多于读取操作时,获取并发哈希集的最佳方法是什么?

Whats the best way to obtain a concurrent hash set when write operations are excess than read operations?

我发现我们可以使用 newKeySet();keySet(default value)ConcurrentHashMap 获得并发哈希集。当写入操作多于读取操作时,这是创建线程安全集的最佳方法吗?

我读到 CopyOnWriteArraySet 似乎阅读比写作更好。

欢迎所有可能有助于我们对此了解更多的答案。

如果您的密钥是小整数,您可以使用位掩码。但是,如果没有更多信息,我会建议。

Set<Type> set = Collections.newSetFromMap(new ConcurrentHashMap<>());

ConcurrentHashMap.newKeySet()ConcurrentHashMap.keySet()那个return一个KeySetView依赖ConcurrentHashMapclass只在写上加锁文中关注的键位映射,并非全图。
因此,读取操作很快但写入也很快(事实上稍微慢一点)。

依赖于CopyOnWriteArrayList

CopyOnWriteArraySet不锁定读操作,而是锁定写操作。因此,为了保证并发读取的一致性,每个写入操作都会触发底层数组的副本。
因此,在不小的集合(例如 100 元素或更多元素)的情况下,CopyOnWriteArraySet 写操作应该比 [=13= 更昂贵(锁定整个集合 + 底层数组的副本) ](仅锁定相关条目)。

CopyOnWriteArraySet javadoc 强调了这一点:

  • It is best suited for applications in which set sizes generally stay small, read-only operations vastly outnumber mutative operations, and you need to prevent interference among threads during traversal.

  • Mutative operations (add, set, remove, etc.) are expensive since they usually entail copying the entire underlying array


这是我们比较两种行为的基准。
Set<String> 在每次迭代时使用 100 个元素(“0”到 99”值)进行初始化。
前6个方法是写入操作(删除,添加新元素,覆盖现有元素)。
而接下来的 4 个方法是读取操作(迭代、包含)。

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(8)
public class SetBenchmark {

    private Set<String> keySetView;
    private Set<String> copyOnWriteArraySet;
    private Random random;

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder().include(SetBenchmark.class.getSimpleName())
                                          .warmupIterations(5)
                                          .measurementIterations(5)
                                          .forks(1)
                                          .build();

        new Runner(opt).run();
    }

    @Setup(Level.Iteration)
    public void doSetup() {
        random = new Random(1);
        keySetView = ConcurrentHashMap.newKeySet();
        copyOnWriteArraySet = new CopyOnWriteArraySet<>();
        init(keySetView);
        init(copyOnWriteArraySet);

    }

    private void init(Set<String> set) {
        IntStream.range(0, 100)
                 .forEach(i -> {
                     final String string = String.valueOf((char) i);
                     set.add(string);
                 });

    }


    // Writing
    @Benchmark
    public void _1_keySetView_remove() {
        doRemove(keySetView);
    }

    @Benchmark
    public void _2_copyOnWriteArraySet_remove() {
        doRemove(copyOnWriteArraySet);
    }

    @Benchmark
    public void _3_keySetView_add_with_new_value() {
        doAddWithNewValue(keySetView);
    }

    @Benchmark
    public void _4_copyOnWriteArraySet_add_with_new_value() {
        doAddWithNewValue(copyOnWriteArraySet);
    }

    @Benchmark
    public void _5_keySetView_add_with_existing_value() {
        doAddWithExistingValue(keySetView);
    }

    @Benchmark
    public void _6_copyOnWriteArraySet_add_with_existing_value() {
        doAddWithExistingValue(copyOnWriteArraySet);
    }

    // Reading
    @Benchmark
    public void _7_keySetView_iterate() {
        String res = doIterate(keySetView);
    }

    @Benchmark
    public void _8_copyOnWriteArraySet_iterate() {
        String res = doIterate(copyOnWriteArraySet);
    }

    @Benchmark
    public void _9_keySetView_contains() {
        boolean res = doContains(keySetView);
    }

    @Benchmark
    public void _010_copyOnWriteArraySet_contains() {
        boolean res = doContains(copyOnWriteArraySet);
    }


    // Writing
    private void doRemove(Set<String> set) {
        set.remove(getRandomString());
    }

    private void doAddWithNewValue(Set<String> set) {
        set.add(getRandomString() + set.size());
    }

    private void doAddWithExistingValue(Set<String> set) {
        set.add(getRandomString());
    }

    // Reading
    private String doIterate(Set<String> set) {
        String result = "";
        for (String string : set) {
            result += string;
        }
        return result;
    }

    private boolean doContains(Set<String> set) {
        return set.contains(getRandomString());
    }

    // Random value with seed
    private String getRandomString() {
        return String.valueOf(random.nextInt(100));
    }

}

结果出来了(分数越低越好)。

写入操作:

Benchmark                                                   Mode  Cnt      Score   Error  Units
SetBenchmark._1_keySetView_remove                            avgt          61,659          ns/op
SetBenchmark._2_copyOnWriteArraySet_remove                   avgt         249,976          ns/op

SetBenchmark._3_keySetView_add_with_new_value                avgt         240,589          ns/op
SetBenchmark._4_copyOnWriteArraySet_add_with_new_value       avgt       30691,318          ns/op

SetBenchmark._5_keySetView_add_with_existing_value           avgt          84,472          ns/op
SetBenchmark._6_copyOnWriteArraySet_add_with_existing_value  avgt         473,592          ns/op

读取操作:

Benchmark                                                   Mode  Cnt      Score   Error  Units
SetBenchmark._7_keySetView_iterate                           avgt       13603,012          ns/op
SetBenchmark._8_copyOnWriteArraySet_iterate                  avgt       13626,146          ns/op

SetBenchmark._9_keySetView_contains                          avgt          53,081          ns/op
SetBenchmark._10_copyOnWriteArraySet_contains                avgt         250,401          ns/op

只有迭代器的两个 Set 实现的读取速度一样快。
在任何情况下 CopyOnWriteArraySet 的写入都慢得多,但是当我们 add() 一个不存在的值时,情况更糟。