通过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

现在我查看了源代码,在这里我们可以看到我们有以下结构:

  1. 我们要求一个Collections.synchronizedSet()
  2. 我们得到一个:

     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();} } } 
  3. 它扩展了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! } 

此处使用的mutexCollections.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()文档,因为已经清理了文档:

当通过IteratorSpliteratorStream遍历它时,用户必须手动同步返回的集合:…

我认为,这很明显,通过除了同步Collection本身之外的对象和使用forEach方法的迭代之间存在区别。 但即使使用旧的措辞,您也可以得出结论:存在这样的区别:

当迭代时 ,用户必须手动同步返回的集合:…

(我强调)

Iterable.forEach的文档进行比较:

Iterable每个元素执行给定的操作,直到处理完所有元素或操作抛出exception为止。

虽然开发人员很清楚必须进行(内部)迭代才能实现此目的,但此迭代是一个实现细节。 从给定规范的措辞来看,它只是一个(元)动作,用于对每个元素执行操作。

使用该方法时, 用户 不会迭代元素,因此不负责Collections.synchronized…文档中提到的同步。

但是,这有点微妙,并且很好, synchronizedCollection的文档明确列出了手动同步的情况,我认为其他方法的文档也应该进行调整。