有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

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

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

我读过CopyOnWriteArraySet读得过多似乎比写得好

欢迎所有可能有助于我们进一步了解这一点的答案


共 (2) 个答案

  1. # 1 楼答案

    返回KeySetViewConcurrentHashMap.newKeySet()ConcurrentHashMap.keySet()依赖于ConcurrentHashMap类,该类仅在写入时锁定,并且仅锁定与写入相关的键映射,而不是整个映射
    因此,读操作很快,但写操作也很快(事实上,读操作稍微慢一点)

    依赖于CopyOnWriteArraySetCopyOnWriteArrayList不锁定读取操作,而是锁定写入操作。因此,为了保证并发读取一致性,每个写入操作都会触发底层数组的副本
    因此,对于不小的集合(100元素或更多,例如,CopyOnWriteArraySet写入操作应该比KeySetView(仅锁定且唯一地锁定相关条目)更昂贵(锁定整个集合+基础数组的副本)

    ^{} 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()没有一个存在的值时,情况就更糟了

  2. # 2 楼答案

    如果键是小整数,可以使用位掩码。然而,如果没有更多的信息,我会建议

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