【发布时间】:2011-07-19 18:49:17
【问题描述】:
如果我通过 Collections.unmodifiableSet() 运行了一个 HashSet 实例,它是线程安全的吗?
我问这个是因为 Set 文档指出它不是,但我只是执行读取操作。
【问题讨论】:
标签: java concurrency hashset
如果我通过 Collections.unmodifiableSet() 运行了一个 HashSet 实例,它是线程安全的吗?
我问这个是因为 Set 文档指出它不是,但我只是执行读取操作。
【问题讨论】:
标签: java concurrency hashset
如果你不改变它,每个数据结构都是线程安全的。
因为你必须改变一个 HashSet 才能初始化它,所以必须在初始化集合的线程和所有读取线程之间同步一次。您只需一次 次。例如,当您将对不可修改集的引用传递给一个以前从未接触过它的新线程时。
【讨论】:
来自 Javadoc:
请注意,此实现不同步。如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须对外同步
阅读不会修改集合,因此你很好。
【讨论】:
如果共享内存永远不会改变,你可以随时读取而不用同步。使集合不可修改只会强制执行无法写入的事实。
【讨论】:
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;
}
}
显然这不是线程安全的。
【讨论】:
是的,并发读取访问是安全的。这是文档中的相关句子:
如果多个线程同时访问一个哈希集,并且至少有一个线程修改了该集,则必须在外部进行同步。
它表明只有at least one线程修改它时才需要同步。
【讨论】:
这将是线程安全的,但这只是因为Collections.unmodifiableSet() 在内部以安全的方式(通过final 字段)发布目标Set。
请注意,诸如“只读对象始终是线程安全的”之类的一般陈述是不正确的,因为它们没有考虑操作重新排序的可能性。
(理论上)有可能,由于操作重新排序,在对象完全初始化并填充数据之前,对该只读对象的引用将对其他线程可见。为了消除这种可能性,您需要以安全的方式发布对对象的引用,例如,将它们存储在 final 字段中,就像 Collections.unmodifiableSet() 所做的那样。
【讨论】:
final 字段是特殊情况,请参阅java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
我不认为它是线程安全的,因为你运行 Collections.unmodifiableSet()。即使 HashSet 完全初始化并且您将其标记为不可修改,但这并不意味着这些更改将对其他线程可见。更糟糕的是,在没有同步的情况下,允许编译器重新排序指令,这可能意味着读取线程不仅可以看到丢失的数据,而且还可以看到处于奇怪状态的哈希集。因此,您将需要一些同步。我相信解决这个问题的一种方法是将哈希集创建为 final 并在构造函数中完全初始化它。这是一篇关于 JMM http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html 的好文章。阅读有关最终字段如何在新 JMM 下工作的部分?
查看正确构造的字段值的能力很好,但如果字段本身是一个引用,那么您还希望您的代码查看它指向的对象(或数组)的最新值。如果您的字段是 final 字段,这也是有保证的。因此,您可以拥有一个指向数组的最终指针,而不必担心其他线程会看到数组引用的正确值,但会看到数组内容的错误值。同样,这里的“正确”是指“在对象的构造函数结束时是最新的”,而不是“可用的最新值”。
【讨论】: