12

Java有线程安全版本HashMap,命名为ConcurrentHashMap,线程安全版本TreeMap命名为ConcurrentSkipListMap,但HashSet没有ConcurrentHashSet什么时候CopyOnWriteArraySet对实现线程安全的HashSet有用?

相反,通常有4种方式使用线程安全Set

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

1使用ConcurrentHashMapkeySet()实现均为Set和线程安全。

2使用​​的方式,似乎不推荐这种方式。

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,并在List数据结构contains操作是O(n),而Set数据结构是高性能contains操作,可能有人解释一下吗?

+0

在JDK中确实没有这样的本地类;你可以使用'Collections.newSetFromMap(new ConcurrentHashMap <>())'。 – fge 2015-03-25 07:25:24

+1

另一个有用的材料:http://stackoverflow.com/questions/6720396/different-types-of-thread-safe-sets-in-java – coderz 2015-03-27 16:53:02

回答

12

当您有一个线程安全集合的一小组元素时,它非常有用。

一个例子是一组听众。您需要确保唯一性并有效地对它们进行迭代。

BTW CopyOnWriteArraySet具有每个参考基础上的最低开销。它可能只有其他馆藏的1/6。如果你有很多它们,这是特别有用的。

while set data structure for high performance contains operation,有谁能解释这个吗?

COWAS在内存方面效率更高,contains比其他方案更快。什么是“高性能”取决于用例。

+2

谢谢您的回复!你能解释为什么''CopyOnWriteArraySet'是如此节省空间? – coderz 2015-03-25 07:47:06

+1

它可能比'HashSet'小,但我无法想象为什么它会比说'ArrayList'更小,特别是因为他们说他们做线程本地快照。我敢打赌,它甚至无法击败'TreeMap' /'TreeSet'。 – VoidStar 2015-03-25 08:07:23

+1

@coderz的CopyOnWriteArraySet包装引用数组,这意味着它可以每基准(即使在64位JVM)但是其它组都建立在地图这反过来每个元件具有Map.Entry的使用尽可能少为4字节。 Map.Entry约为24个字节加上对该条目的引用,使得每个元素最多为32个字节,具体取决于集合。 – 2015-03-25 08:19:21

4

写入时复制结构在功能不变。

的Java在一个点有一个非常可怜的故事,以提供诸如集上写的结构不变的看法。例如,如果你有一个成员,并且你公开地返回它,调用者可以转过来编辑它,因此编辑你的对象的内部状态!但你还能做什么,在从任何公共职能返回之前复制整个事情?这将是毫无意义的缓慢。

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

写入时复制是一种时尚,几年前,但它是多线程编程出了名的低效想法,比其他车型低效率。从你发布的文档中,他们通过创建线程本地快照来加快迭代,这意味着他们花费内存来弥补。所以只要你的数据很小就可以使用它,因为内存快照不会浪费太多内存。

+0

对于正确的用例CopyOnWriteArraySet是最有效的。使用不正确,任何集合都可能被认为效率低下。 – 2015-03-25 08:23:14

+0

'CopyOnWriteArraySet'对于大多数开发者来说是一个简单和高效的完美合理的平衡,但COW并不是“最高效”的方式。请记住,即使在同一个线程中,每次创建新的迭代器时,它都会构造一个新的快照。通常多线程环境中的COW在特殊情况下只是最优的,并且不能像效率一样让开发人员决定何时需要副本。具有手动锁定组合的'ArrayList'避免了不必要的拷贝,但是当它们帮助时仍然支持拷贝。 – VoidStar 2015-03-25 09:32:46

+0

新的Iterator不创建快照,它使用不可变引用数组。这就是为什么每当你改变数组的内容时它都必须进行写入操作。 – 2015-03-25 09:36:59

0

测试代码:

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似乎并不更好。

+1

'CopyOnWriteArraySet'的用例与用于'Collections.newSetFromMap'的用例不同。您的评估完全有缺陷。 – 2015-03-27 16:56:49

+0

@JohnVint你能讲更多的细节吗? – 2015-03-28 02:17:56

+0

当然。 CopyOnWriteArrayList非常好,当你做90 +%的读取。如果你写很多,这是非常昂贵和低效率。当你想用多线程安全地迭代列表而不必同步整个集合时,COWAL也是非常好的。 Collections.synchronizedList将会有更快的添加,所以对于简单的放入和移除可能会有好处,但是其他的不会。 – 2015-03-29 23:11:25