从并发修改的ConcurrentSkipListSet

时间:2016-02-28 14:29:32

标签: java collections concurrency

通常,并发集合可以安全迭代;根据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)重现这一点。

2 个答案:

答案 0 :(得分:6)

据浏览来源我可以理解,TreeSet的问题在于它在迭代之前调用size()然后使用它而不是调用hasNext()。这可能是一个错误,但我认为这只是红黑树是复杂结构需要仔细平衡的结果,因此需要提前知道尺寸,以便在创建过程中在线性时间内正确平衡它。

您可以通过手动迭代并向TreeSet添加元素来避免这种情况,但这会导致n log n复杂性,这可能是TreeSet构造函数的原因不这样做(它的API规范保证线性时间)。当然它在构建树时仍然可以调用hasNext(),但是在构造完成之后可能需要一些额外的动作来重新平衡树,这可能导致分摊的线性复杂性。但是,红黑树本来就是一团糟,而那种黑客攻击会使实施更加混乱。

尽管如此,我认为它非常令人困惑,应该在API文档的某处记录,但我不确定究竟在哪里。可能在他们解释什么是弱一致迭代器的部分。具体来说,应该提到的是,某些库类依赖于返回的大小,因此可能抛出NoSuchElementException。提及特定课程也会有所帮助。

答案 1 :(得分:3)

我实际上开始倾向于TreeSet / TreeMap(更新,it is)中的错误。正如谢尔盖所​​说,问题是TreeMap在阅读其元素之前缓存了ConcurrentSkipListSet.size()的结果。

换句话说,它假定传递的Collection在构造过程中不会被修改,这是一个错误的假设。

请注意,即使buildFromSorted()确实调用Iterator.hasNext(),此时它的唯一选项也会失败,因为在构建过程中修改了支持数据结构。

查看可能在复制并发结构时遇到问题的其他集合,包括ArrayListLinkedListCopyOnWriteArrayList(我只看for-each over the elements的大多数其他集合) ,在进行任何实际工作之前,将提供的集合显式复制到数组中,以避免出现这个问题。我认为TreeSetTreeMap应该做同样的事情。

由于这个错误,我们实际上不必接受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));

现在成功。

很好找到:)。

相关问题