我们知道,默认情况下迭代并发集合不是线程安全的,所以不能使用:
Set<E> set = Collections.synchronizedSet(new HashSet<>());
//fill with data
for (E e : set) {
process(e);
}
这是因为在迭代期间可能会添加数据,因为set
上没有排他锁。
这在Collections.synchronizedSet
的{{3}}中描述:
public static Set synchronizedSet(Set s)
返回由指定集支持的同步(线程安全)集。为了保证串行访问,必须通过返回的集合完成对后备集的所有访问。
当迭代它时,用户必须手动同步返回的集合:
Set s = Collections.synchronizedSet(new HashSet());
...
synchronized (s) { Iterator i = s.iterator(); // Must be in the synchronized block while (i.hasNext()) foo(i.next()); }
不遵循此建议可能会导致非确定性行为。
但,这不适用于从javadoc继承默认方法Set.forEach
的{{1}}。
现在我查看了源代码,在这里我们可以看到我们有以下结构:
forEach
。我们得到一个:
Collections.synchronizedSet()
它扩展了public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
...
static class SynchronizedSet<E>
extends SynchronizedCollection<E>
implements Set<E> {
private static final long serialVersionUID = 487447009682186044L;
SynchronizedSet(Set<E> s) {
super(s);
}
SynchronizedSet(Set<E> s, Object mutex) {
super(s, mutex);
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return c.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return c.hashCode();}
}
}
,在明显的方法旁边有以下有趣的方法:
SynchronizedCollection
此处使用的// Override default methods in Collection
@Override
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
@Override
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
@Override
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
@Override
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
与mutex
锁定的所有操作的对象相同。
现在我们可以通过实现来判断使用Collections.synchronizedSet
是否可以线程安全,但它是否也是规范的线程安全?
(令人困惑的是,Collections.synchronizedSet(...).forEach(...)
不线程安全的实现,并且规范的判定似乎也是未知的。)
答案 0 :(得分:11)
正如您所写的那样,根据实施情况来看,forEach()
对于JDK提供的集合是线程安全的(请参阅下面的免责声明),因为它需要监视要获取的互斥锁才能继续。
规范是否也是线程安全的?
我的意见 - 不,这是一个解释。用短语重写的Collections.synchronizedXXX()
javadoc说 - “除了那些用于迭代它的方法之外,所有方法都是线程安全的。”
我的另一个,虽然非常主观的论点是 yshavit 所写的 - 除非告知/读取,考虑API /类/任何不是线程安全的。
现在,让我们仔细看看javadoc。我想我可能会声明方法forEach()
用于迭代它,因此,遵循javadoc的建议,我们应该认为它不是线程安全的,尽管它与现实(实现)相反。
无论如何,我同意 yshavit 的声明,即文档应该更新,因为这很可能是文档,而不是实现缺陷。但是,除了JDK开发人员之外,没有人可以肯定地说,请看下面的问题。
我想在本次讨论中提到的最后一点 - 我们可以假设自定义集合可以用Collections.synchronizedXXX()
包装,并且此集合的forEach()
的实现可以是... can做任何事。该集合可能会对forEach()
方法中的元素执行异步处理,为每个元素生成一个线程......它仅受作者想象力的限制,并且 synchronized(互斥)包裹不能保证此类线程的安全性。例即可。该特定问题可能是不将forEach()
方法声明为线程安全的原因..
答案 1 :(得分:6)
值得看看documentation of Collections.synchronizedCollection
而不是Collections.synchronizedSet()
作为已经清理文档:
用户在遍历它时必须手动同步返回的集合 通过
Iterator
,Spliterator
或Stream
:...
我认为,这很明显,通过除了同步Collection
本身之外的对象和使用其forEach
方法的迭代之间存在区别。但即使使用旧的措辞,您也可以得出结论:存在这样的区别:
当迭代它时,用户必须手动同步返回的集合 :...
(我强调)
与documentation for Iterable.forEach
比较:
对
Iterable
的每个元素执行给定操作,直到处理完所有元素或操作引发异常。
虽然开发人员很清楚必须进行(内部)迭代才能实现这一点,但这次迭代是一个实现细节。根据给定规范的措辞,它只是一个(元)动作,用于对每个元素执行操作。
使用该方法时,用户 不迭代元素,因此不对Collections.synchronized…
文档中提到的同步负责。
然而,这有点微妙,documentation of synchronizedCollection
明确列出手动同步的情况是好的,我认为其他方法的文档也应该适应。
答案 2 :(得分:0)
如@Holger所说,doc clearly says用户在通过Collections.synchronizedXyz()
遍历Iterator
时必须手动同步Collection c = Collections.synchronizedCollection(myCollection);
...
synchronized (c) {
Iterator i = c.iterator(); // Must be in the synchronized block
while (i.hasNext())
foo(i.next());
}
返回的集合:
当通过Iterator,Spliterator或Stream遍历返回的集合时,用户必须手动对其进行同步:
public Iterator<E> iterator() { return c.iterator(); // Must be manually synched by user! }
我想进一步解释一下代码。
考虑Collections.synchronizedList()
method。它返回Collections.SynchronizedList类实例,该实例扩展了SynchronizedCollection
,它定义了iterator()
,如下所示:
SynchronizedCollections
将此方法与public String toString() {
synchronized (mutex) {return c.toString();}
}
的其他方法进行比较,例如:
SynchronizedList
因此iterator()
从SynchronizedCollection
继承了render
,需要用户手动对其进行同步。