CopyOnWriteArraySet 何时对实现线程安全的 HashSet 有用?

Posted

技术标签:

【中文标题】CopyOnWriteArraySet 何时对实现线程安全的 HashSet 有用?【英文标题】:When is CopyOnWriteArraySet useful to achieve thread-safe HashSet? 【发布时间】:2015-05-28 18:32:29 【问题描述】:

Java 中,有名为ConcurrentHashMap 的线程安全版本HashMap 和名为ConcurrentSkipListMap 的线程安全版本ConcurrentSkipListMap,但HashSet 没有ConcurrentHashSet

相反,通常有 4 种方法可以使用线程安全的Set

    Set<String> mySet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); Set<String> s = Collections.synchronizedSet(new HashSet<String>()); ConcurrentSkipListSet<E> CopyOnWriteArraySet<E>

1 使用keySet()ConcurrentHashMap 来实现Set 和线程安全。

2使用synchronized的方式,好像不推荐这种方式。

3 基于ConcurrentSkipListMap,被广泛使用。

4 基于CopyOnWriteArrayList,因此它具有与CopyOnWriteArrayList 相同的基本属性。以下是从CopyOnWriteArraySet doc中选择的:http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CopyOnWriteArraySet.html

它最适合通常保持集合大小的应用程序 小型只读操作的数量远远超过可变操作,并且 您需要在遍历期间防止线程之间的干扰。 它是线程安全的。 可变操作(添加、设置、删除等)代价高昂,因为它们通常需要复制整个底层数组。 迭代器不支持可变删除操作。 通过迭代器的遍历速度很快,不会遇到其他线程的干扰。 迭代器依赖于构造迭代器时数组的不变快照。

既然常用1和3,那么CopyOnWriteArraySet为什么会存在呢? CopyOnWriteArraySet 什么时候有用?

补充:CopyOnWriteArraySet是基于CopyOnWriteArrayListcontainsList数据结构中的运算是O(n),而@987654349 @ 数据结构用于高性能 contains 操作,有人可以解释一下吗?

【问题讨论】:

JDK中确实没有这个native class;但是,您可以使用Collections.newSetFromMap(new ConcurrentHashMap<>()) 另一个有用的材料:***.com/questions/6720396/… 【参考方案1】:

当您有一小部分元素用于线程安全集合时,它很有用。

一个例子是一组监听器。您需要确保唯一性并有效地迭代它们。

BTW CopyOnWriteArraySet 在每个引用的基础上具有最低的开销。它可以是其他集合大小的 1/6。如果您有很多,这将特别有用。

虽然 Set 数据结构是为了高性能包含操作,但有人可以解释一下吗?

COWAS 在内存方面的效率更高,它的 contains 对于小型集合比其他方法更快。什么是“高性能”取决于用例。

【讨论】:

感谢您的回复!你能解释一下为什么CopyOnWriteArraySet 如此节省空间吗? 它可能比HashSet 小,但我无法想象为什么它会比ArrayList 小,特别是因为他们说他们制作线程本地快照。我敢打赌它甚至无法击败TreeMap / TreeSet @coderz CopyOnWriteArraySet 包装了一个引用数组,这意味着它每个引用最多可以使用 4 个字节(即使在 64 位 JVM 上)但是其他集合是建立在 Maps 上的,而 Maps 又具有每个元素的 Map.Entry。 Map.Entry 大约有 24 个字节加上对该条目的引用,这使得每个元素最多 32 个字节,具体取决于集合。 @VoidStar ArrayList 的大小相同,但它不是 a) 线程安全的,b) 一个 Set。 TreeMap 不是 a) 线程安全的,b) 非常节省空间并且每个条目使用大约 24 个字节。 TreeSet 是建立在 TreeMap 之上的,效率并不高。 @VoidStar ArrayList 不支持迭代,虽然它可能会被更新,也不支持将其包装在 Collections.synchronisedList() 中,即它速度较慢并且仍然不是线程安全的。【参考方案2】:

写时复制结构在功能上是不可变的。

Java 在为可写结构(例如集合)提供不可变视图方面曾有过一个非常糟糕的故事。例如,如果您有一个 set 成员,并且您公开返回它,调用者可以转身编辑它,因此正在编辑您的对象的内部状态!但是你还能做什么,在从任何公共函数返回之前复制整个内容?那将是毫无意义的缓慢。

这是 Java 历史上较早的故事。他们几乎完全依赖于不可变对象(字符串就是一个例子)。集合是这种模式的一个例外,因此从封装的角度来看是有问题的。当添加CopyOnWriteArraySet 时,unmodifiableCollectionunmodifiableSet 还不存在(虽然unmodifiableCollection 已经在很大程度上解决了这个问题,但我仍然觉得它比其他语言提供的解决方案更麻烦,尤其是在使用自定义数据结构时) .因此,这可能首先解释了创建CopyOnWriteArraySet 的最大动机。您可以返回 CopyOnWriteArraySet,而不必担心其他人会修改您对象的内部状态,也不会浪费时间制作不必要的副本。

Copy-On-Write 在几年前是一种时尚,但它对于多线程编程来说是一个出了名的低效想法,并且效率低于其他模型。从您发布的文档中,他们通过创建线程本地快照加快了迭代速度,这意味着他们正在消耗内存来补偿。因此,只要您的数据很小,它就是一个完全可以使用的类……因为内存快照不会增加太多浪费的内存。

【讨论】:

对于正确的用例,CopyOnWriteArraySet 是最有效的。使用不当,任何收集都可以被认为是低效的。 CopyOnWriteArraySet 对于大多数开发人员来说是简单和高效之间完美合理的平衡,但 COW 并不是“最高效”的方式。请记住,每次创建新迭代器时它都会构造一个新快照,即使在同一个线程中也是如此。通常,多线程环境中的 COW 仅在非常特殊的情况下才是最佳的,并且无法与简单地让开发人员决定何时需要复制那样的效率相匹配。带有手动锁定组合的ArrayList 可避免不必要的副本,但在有帮助时仍支持副本。 一个新的迭代器不会创建快照,它使用不可变的引用数组。这就是为什么当你改变数组的内容时它必须在写时复制。 这里不是这么说的。 docs.oracle.com/javase/7/docs/api/java/util/concurrent/… 。如果他们一直在使用读写锁来阻止旧不可变视图上的垃圾收集,那么您的想法可能会奏效。但是如果你考虑一下,它必须是递归读取的,而递归读写锁定实际上是相当昂贵的(在内部你需要每个线程的状态,例如列表/映射),所以对于一个小数组,它实际上使正如他们所记录的那样,在迭代器构造中复制整个内容更有意义。 再想一想,原子引用计数会很好地工作。然后,唯一的浪费是开发人员无论如何都要使用迭代器进行可变操作,然后你只浪费了一个额外的原子增量/减量。我可能已经这样做了,不知道他们为什么选择 per-iterator 的东西。【参考方案3】:

测试代码:

Set<String> a = new CopyOnWriteArraySet<String>();
    for(int i=0;i<10;i++) 
        a.add("str" + i);
    
    boolean flag = true;
    long t1 = System.currentTimeMillis();
    for(int i=0;i<200000;i++) 
        flag = a.contains("str" + i);
    
    System.out.println(System.currentTimeMillis() - t1);

    Set<String> b = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
    for(int i=0;i<10;i++) 
        b.add("str" + i);
    
    t1 = System.currentTimeMillis();
    for(int i=0;i<200000;i++) 
        flag = b.contains("str" + i);
    
    System.out.println(System.currentTimeMillis() - t1);

表明CopyOnWriteArraySetCollections.newSetFromMap 慢。由于测试用例是一个很小的Set 只读操作,CopyOnWriteArraySet 似乎并不好。

【讨论】:

CopyOnWriteArraySet 的用例与 Collections.newSetFromMap 的用例不同。您的评估完全有缺陷。 @JohnVint 你能说详细点吗? 当然。当您进行 90+% 的读取时,CopyOnWriteArrayList 非常好。如果你写了很多,那是非常昂贵和低效的。当您想要使用多个线程安全地迭代列表而不必同步整个集合时,COWAL 也非常好。 Collections.synchronizedList 将具有更快的添加速度,因此可能对简单的放置和删除有好处,但其他方面就没什么了。

以上是关于CopyOnWriteArraySet 何时对实现线程安全的 HashSet 有用?的主要内容,如果未能解决你的问题,请参考以下文章

CopyOnWriteArrayList,CopyOnWriteArraySet源码分析

JUC 一 CopyOnWriteArrayList 和 CopyOnWriteArraySet

死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

JUC之CopyOnWriteArrayList和CopyOnWriteArraySet

java并发之CopyOnWriteArraySet

Java多线程系列--“JUC集合”03之 CopyOnWriteArraySet