Java HashSet 是只读的线程安全的吗?

Posted

技术标签:

【中文标题】Java HashSet 是只读的线程安全的吗?【英文标题】:is Java HashSet thread-safe for read only? 【发布时间】:2011-07-19 18:49:17 【问题描述】:

如果我通过 Collections.unmodifiableSet() 运行了一个 HashSet 实例,它是线程安全的吗?

我问这个是因为 Set 文档指出它不是,但我只是执行读取操作。

【问题讨论】:

另见***.com/questions/20039470/… 【参考方案1】:

如果你不改变它,每个数据结构都是线程安全的。

因为你必须改变一个 HashSet 才能初始化它,所以必须在初始化集合的线程和所有读取线程之间同步一次。您只需一次 次。例如,当您将对不可修改集的引用传递给一个以前从未接触过它的新线程时。

【讨论】:

这并不完全正确。看看我的答案,看看它不正确的例子。仍然存在突变,但用户无法知道;它是封装状态的突变。 事实并非如此。至少在Java中。你不改变的对象和 immutable 对象之间是有区别的。这两个只有不可变对象自动线程安全。对象在以下情况下是不可变的:1)它在构造后无法修改 2)它的所有字段都是最终的 3)此引用在构造期间没有转义 @jmg 字符串绝对是不可变的。您奇怪的结论是基于这样一个事实,即特定方法不是引用透明的,因为它取决于另一个隐式参数。这是方法的属性,而不是对象本身。 @jmg 也读取全局变量不是副作用,它根本不是引用透明的。但是,您可以读取此变量并将其作为参数提供,因此读取当前 Locale 并将其提供给 String.toUpperCase 使 String 函数具有引用透明性。 @jmg @Mark_Peters 字符串不是不可变的,但它是线程安全的。它有一个缓存的哈希码,其设置具有竞争条件。尽管哈希函数具有引用透明性,但这场竞赛是良性的。【参考方案2】:

来自 Javadoc:

请注意,此实现不同步。如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须对外同步

阅读不会修改集合,因此你很好。

【讨论】:

除了创建 HashSet 并向其添加值,因此它不为空是有用的,写入它。所以这个答案具有误导性。 执行类似 Collections.unmodifiableMap( /* call or code that created a new HashMap here */ ) 将创建一个填充的、不可修改的映射(确保没有人更改它并且线程只从它读取)。 @Raedwald 我不认为这个答案具有误导性,因为它清楚地说明了修改集合,通过修改它并不意味着我们应该添加、删除或更新集合中的任何元素。【参考方案3】:

如果共享内存永远不会改变,你可以随时读取而不用同步。使集合不可修改只会强制执行无法写入的事实。

【讨论】:

这是非常非常错误的。 必须在原始写入和读取之间存在某种发生前的关系,否则无法保证除了默认值之外的任何内容都会被看到。在 x86 架构上,您通常会看到一些东西,这与任何类型的保证都不相同。【参考方案4】:

HashSet 如果以只读方式使用,将是线程安全的。这并不意味着您传递给Collections.unmodifiableSet()任何 Set 都是线程安全的。

想象一下contains 的这种幼稚实现,它缓存了最后检查的值:

Object lastKey;
boolean lastContains;

public boolean contains(Object key) 
   if ( key == lastKey ) 
      return lastContains;
    else 
      lastKey = key;
      lastContains = doContains(key);
      return lastContains;
   

显然这不是线程安全的。

【讨论】:

从地图示例更改为总集示例以更适用于问题。【参考方案5】:

是的,并发读取访问是安全的。这是文档中的相关句子

如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须在外部进行同步。

它表明只有at least one线程修改它时才需要同步。

【讨论】:

不,它没有说明。【参考方案6】:

这将是线程安全的,但这只是因为Collections.unmodifiableSet() 在内部以安全的方式(通过final 字段)发布目标Set

请注意,诸如“只读对象始终是线程安全的”之类的一般陈述是不正确的,因为它们没有考虑操作重新排序的可能性。

(理论上)有可能,由于操作重新排序,在对象完全初始化并填充数据之前,对该只读对象的引用将对其他线程可见。为了消除这种可能性,您需要以安全的方式发布对对象的引用,例如,将它们存储在 final 字段中,就像 Collections.unmodifiableSet() 所做的那样。

【讨论】:

+1,一般来说“只读对象总是线程安全的”这样的说法是不正确的 为什么 final 关键字在操作重新排序方面使这个“只读”线程安全? JVM 会因此而采取不同的行动吗? @Asaf:是的,final 字段是特殊情况,请参阅java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5 包装器安全发布是否记录?如果不是,那么假设它这样做是不安全的。虽然我无法想象内部没有使用 final 字段的实现。【参考方案7】:

我不认为它是线程安全的,因为你运行 Collections.unmodifiableSet()。即使 HashSet 完全初始化并且您将其标记为不可修改,但这并不意味着这些更改将对其他线程可见。更糟糕的是,在没有同步的情况下,允许编译器重新排序指令,这可能意味着读取线程不仅可以看到丢失的数据,而且还可以看到处于奇怪状态的哈希集。因此,您将需要一些同步。我相信解决这个问题的一种方法是将哈希集创建为 final 并在构造函数中完全初始化它。这是一篇关于 JMM http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 的好文章。阅读有关最终字段如何在新 JMM 下工作的部分?

查看正确构造的字段值的能力很好,但如果字段本身是一个引用,那么您还希望您的代码查看它指向的对象(或数组)的最新值。如果您的字段是 final 字段,这也是有保证的。因此,您可以拥有一个指向数组的最终指针,而不必担心其他线程会看到数组引用的正确值,但会看到数组内容的错误值。同样,这里的“正确”是指“在对象的构造函数结束时是最新的”,而不是“可用的最新值”。

【讨论】:

正如在别处指出的那样,Collections.unmodifiable 视图安全地发布集合的状态,就像创建不可修改的包装器时一样(发生在之前)。但随后的更改不是,应该放弃原来的参考。

以上是关于Java HashSet 是只读的线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章

Java中HashMap,HashSet是线程安全的吗,ArrayList是线程不安全的那如何避免其出异常?

Java并发多线程编程——集合类线程不安全之HashSet的示例及解决方案

const是多线程安全的吗

验证HashSet和HashMap不是线程安全

java是线程安全的吗

并发遍历实现线程安全遍历