通常,并发集合可以安全迭代;根据Javadoc的说法:'迭代器是弱一致的,在迭代器创建时或之后的某个时刻返回反映集合状态的元素。它们不会抛出ConcurrentModificationException,并且可能与其他操作同时进行。 但是,请考虑一下:
import java.util.Random;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListSet;
public class ConcurrencyProblem {
private static volatile boolean modifierIsAlive = true;
public static void main(String[] args) {
final ConcurrentSkipListSet<Integer> concurrentSet = new ConcurrentSkipListSet<>();
Thread modifier = new Thread() {
private final Random randomGenerator = new Random();
public void run() {
while (modifierIsAlive) {
concurrentSet.add(randomGenerator.nextInt(1000));
concurrentSet.remove(randomGenerator.nextInt(1000));
}
};
};
modifier.start();
int sum = 0;
while (modifierIsAlive) {
try {
TreeSet<Integer> sortedCopy = new TreeSet<>(concurrentSet);
// make sure the copy operation is not eliminated by the compiler
sum += sortedCopy.size();
} catch (RuntimeException rte) {
modifierIsAlive = false;
rte.printStackTrace();
}
}
System.out.println("Dummy output: " + sum);
}
}
输出
java.util.NoSuchElementException
at java.util.concurrent.ConcurrentSkipListMap$Iter.advance(ConcurrentSkipListMap.java:2299)
at java.util.concurrent.ConcurrentSkipListMap$KeyIterator.next(ConcurrentSkipListMap.java:2334)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2559)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2547)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2579)
at java.util.TreeMap.buildFromSorted(TreeMap.java:2504)
at java.util.TreeMap.addAllForTreeSet(TreeMap.java:2462)
at java.util.TreeSet.addAll(TreeSet.java:308)
at java.util.TreeSet.<init>(TreeSet.java:172)
at mtbug.ConcurrencyProblem.main(ConcurrencyProblem.java:27)
Dummy output: 44910
我想知道这是一个错误还是一个功能;我们没有得到ConcurrentModificationException,但仍然,不得不关心迭代(回落到同步块或其他)有点失败ConcurrentSkipListSet / Map的目的。我已经能够使用Java 7和8(目前,我的Linux机器上的8u72)重现这一点。
答案 0 :(得分:6)
据浏览来源我可以理解,TreeSet
的问题在于它在迭代之前调用size()
然后使用它而不是调用hasNext()
。这可能是一个错误,但我认为这只是红黑树是复杂结构需要仔细平衡的结果,因此需要提前知道尺寸,以便在创建过程中在线性时间内正确平衡它。
您可以通过手动迭代并向TreeSet
添加元素来避免这种情况,但这会导致n log n
复杂性,这可能是TreeSet
构造函数的原因不这样做(它的API规范保证线性时间)。当然它在构建树时仍然可以调用hasNext()
,但是在构造完成之后可能需要一些额外的动作来重新平衡树,这可能导致分摊的线性复杂性。但是,红黑树本来就是一团糟,而那种黑客攻击会使实施更加混乱。
尽管如此,我认为它非常令人困惑,应该在API文档的某处记录,但我不确定究竟在哪里。可能在他们解释什么是弱一致迭代器的部分。具体来说,应该提到的是,某些库类依赖于返回的大小,因此可能抛出NoSuchElementException
。提及特定课程也会有所帮助。
答案 1 :(得分:3)
我实际上开始倾向于TreeSet
/ TreeMap
(更新,it is)中的错误。正如谢尔盖所说,问题是TreeMap
在阅读其元素之前缓存了ConcurrentSkipListSet.size()
的结果。
TreeSet.addAll()
来电TreeMap.addAllForTreeSet()
并将集合的当前大小和可能并发的Iterator
传递给TreeMap.buildFromSorted()
最终调用Iterator.next()
size
- 次。换句话说,它假定传递的Collection
在构造过程中不会被修改,这是一个错误的假设。
请注意,即使buildFromSorted()
确实调用Iterator.hasNext()
,此时它的唯一选项也会失败,因为在构建过程中修改了支持数据结构。
查看可能在复制并发结构时遇到问题的其他集合,包括ArrayList
,LinkedList
和CopyOnWriteArrayList
(我只看for-each over the elements的大多数其他集合) ,在进行任何实际工作之前,将提供的集合显式复制到数组中,以避免出现这个问题。我认为TreeSet
和TreeMap
应该做同样的事情。
由于这个错误,我们实际上不必接受O(n log n)性能,但它会成为一个黑客。我们不能简单地将值复制到数组或其他数据结构中,因为插入TreeSet
不是线性时间。但我们可以通过声称副本是TreeSet
来欺骗SortedSet
。
public static class IterateOnlySortedSet<E>
extends AbstractSet<E> implements SortedSet<E> {
private final ArrayList<E> elements;
private final Comparator<? super E> comparator;
public IterateOnlySortedSet(SortedSet<E> source) {
elements = new ArrayList<>(source);
comparator = source.comparator();
}
@Override
public Iterator<E> iterator() {
return elements.iterator();
}
@Override
public int size() {
return elements.size();
}
@Override
public Comparator<? super E> comparator() {
return comparator;
}
// remaining methods simply throw UnsupportedOperationException
}
将TreeSet
施工线更改为:
TreeSet<Integer> sortedCopy = new TreeSet<>(new IterateOnlySortedSet<>(concurrentSet));
现在成功。
很好找到:)。