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
是基于CopyOnWriteArrayList
,contains
在List
数据结构中的运算是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
时,unmodifiableCollection
和unmodifiableSet
还不存在(虽然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);
表明CopyOnWriteArraySet
比Collections.newSetFromMap
慢。由于测试用例是一个很小的Set
只读操作,CopyOnWriteArraySet
似乎并不好。
【讨论】:
CopyOnWriteArraySet
的用例与 Collections.newSetFromMap
的用例不同。您的评估完全有缺陷。
@JohnVint 你能说详细点吗?
当然。当您进行 90+% 的读取时,CopyOnWriteArrayList 非常好。如果你写了很多,那是非常昂贵和低效的。当您想要使用多个线程安全地迭代列表而不必同步整个集合时,COWAL 也非常好。 Collections.synchronizedList 将具有更快的添加速度,因此可能对简单的放置和删除有好处,但其他方面就没什么了。以上是关于CopyOnWriteArraySet 何时对实现线程安全的 HashSet 有用?的主要内容,如果未能解决你的问题,请参考以下文章
CopyOnWriteArrayList,CopyOnWriteArraySet源码分析
JUC 一 CopyOnWriteArrayList 和 CopyOnWriteArraySet
死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计