通过Collections.synchronizedSet(…)。forEach()的迭代是否保证是线程安全的?
我们知道,默认情况下迭代并发集合不是线程安全的,所以不能使用:
Set set = Collections.synchronizedSet(new HashSet()); //fill with data for (E e : set) { process(e); }
这是因为在迭代期间可能会添加数据,因为set
上没有排它锁。
这在Collections.synchronizedSet
的javadoc中描述:
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()); }
不遵循此建议可能会导致非确定性行为。
但是 ,这不适用于Set.forEach
,它inheritance了Iterable.forEach的默认方法forEach
。
现在我查看了源代码,在这里我们可以看到我们有以下结构:
- 我们要求一个
Collections.synchronizedSet()
。 -
我们得到一个:
public static Set synchronizedSet(Set s) { return new SynchronizedSet(s); } ... static class SynchronizedSet extends SynchronizedCollection implements Set { private static final long serialVersionUID = 487447009682186044L; SynchronizedSet(Set s) { super(s); } SynchronizedSet(Set 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 consumer) { synchronized (mutex) {c.forEach(consumer);} } @Override public boolean removeIf(Predicate filter) { synchronized (mutex) {return c.removeIf(filter);} } @Override public Spliterator spliterator() { return c.spliterator(); // Must be manually synched by user! } @Override public Stream stream() { return c.stream(); // Must be manually synched by user! } @Override public Stream parallelStream() { return c.parallelStream(); // Must be manually synched by user! }
此处使用的mutex
与Collections.synchronizedSet
所有操作锁定的对象相同。
现在我们可以根据实现来判断使用Collections.synchronizedSet(...).forEach(...)
是否是线程安全的 ,但它是否也符合规范的线程安全?
(令人困惑的是, Collections.synchronizedSet(...).stream().forEach(...)
通过实现不是线程安全的,并且规范的判定似乎也是未知的。)
正如您所写,通过实现来判断, forEach()
对于JDK提供的集合是线程安全的 (请参阅下面的免责声明),因为它需要监视要获取的互斥锁才能继续。
是否通过规范线程安全?
我的意见 – 不,这是一个解释。 Collections.synchronizedXXX()
javadoc,用简短的文字重写,说 – “所有方法都是线程安全的,除了那些用于迭代它的方法”。
我的另一个,虽然非常主观的论点是yshavit写的 – 除非告知/读取,考虑API /类/任何不是线程安全的。
现在,让我们仔细看看javadocs。 我想我可以声明forEach()
方法用于迭代它,因此,遵循javadoc的建议,我们应该认为它不是线程安全的,尽管它与现实(实现)相反。
无论如何,我同意yshavit的声明,即文档应该更新,因为这很可能是文档,而不是实现缺陷。 但是,除了JDK开发人员之外,没有人可以肯定地说,请看下面的问题。
我想在这个讨论中提到的最后一点 – 我们可以假设自定义集合可以用Collections.synchronizedXXX()
包装,并且这个集合的forEach()
的实现可以是……可以是任何东西。 该集合可以在forEach()
方法中执行元素的异步处理,为每个元素生成一个线程……它仅受作者想象力的限制,并且synchronized(互斥)包装不能保证这种情况的线程安全 。 该特定问题可能是不将forEach()
方法声明为线程安全的原因。
有必要查看Collections.synchronizedCollection
而不是Collections.synchronizedSet()
文档,因为已经清理了文档:
当通过
Iterator
,Spliterator
或Stream
遍历它时,用户必须手动同步返回的集合:…
我认为,这很明显,通过除了同步Collection
本身之外的对象和使用forEach
方法的迭代之间存在区别。 但即使使用旧的措辞,您也可以得出结论:存在这样的区别:
当迭代时 ,用户必须手动同步返回的集合:…
(我强调)
与Iterable.forEach
的文档进行比较:
对
Iterable
每个元素执行给定的操作,直到处理完所有元素或操作抛出exception为止。
虽然开发人员很清楚必须进行(内部)迭代才能实现此目的,但此迭代是一个实现细节。 从给定规范的措辞来看,它只是一个(元)动作,用于对每个元素执行操作。
使用该方法时, 用户 不会迭代元素,因此不负责Collections.synchronized…
文档中提到的同步。
但是,这有点微妙,并且很好, synchronizedCollection
的文档明确列出了手动同步的情况,我认为其他方法的文档也应该进行调整。