【问题标题】:Encounter Order wrong when sorting a parallel stream对并行流进行排序时遇到顺序错误
【发布时间】:2015-05-22 21:41:23
【问题描述】:

我有一个Record 类:

public class Record implements Comparable<Record>
{
   private String myCategory1;
   private int    myCategory2;
   private String myCategory3;
   private String myCategory4;
   private int    myValue1;
   private double myValue2;

   public Record(String category1, int category2, String category3, String category4,
      int value1, double value2)
   {
      myCategory1 = category1;
      myCategory2 = category2;
      myCategory3 = category3;
      myCategory4 = category4;
      myValue1 = value1;
      myValue2 = value2;
   }

   // Getters here
}

我创建了一个包含大量记录的大列表。只有第二个和第五个值 i / 10000i 稍后分别由 getter getCategory2()getValue1() 使用。

List<Record> list = new ArrayList<>();
for (int i = 0; i < 115000; i++)
{
    list.add(new Record("A", i / 10000, "B", "C", i, (double) i / 100 + 1));
}

请注意,前 10,000 条记录的 category20,然后接下来的 10,000 条记录有 1,依此类推,而 value1 的值依次为 0-114999。

我创建了一个Stream,它同时是parallelsorted

Stream<Record> stream = list.stream()
   .parallel()
   .sorted(
       //(r1, r2) -> Integer.compare(r1.getCategory2(), r2.getCategory2())
   )
   //.parallel()
;

我有一个 ForkJoinPool 维护 8 线程,这是我的 PC 上的内核数。

ForkJoinPool pool = new ForkJoinPool(8);

我使用技巧described here to submit a stream processing task to my own ForkJoinPool instead of the common ForkJoinPool

List<Record> output = pool.submit(() ->
    stream.collect(Collectors.toList()
)).get();

我希望并行的sorted 操作会尊重流的遇到顺序,并且它会是一个稳定 排序,因为ArrayList 返回的Spliterator 是@987654347 @。

但是,打印出结果 List output 的元素的简单代码表明情况并非如此。

for (Record record : output)
{
     System.out.println(record.getValue1());
}

输出,精简:

0
1
2
3
...
69996
69997
69998
69999
71875  // discontinuity!
71876
71877
71878
...
79058
79059
79060
79061
70000  // discontinuity!
70001
70002
70003
...
71871
71872
71873
71874
79062  // discontinuity!
79063
79064
79065
79066
...
114996
114997
114998
114999

outputsize()115000,所有元素似乎都在那里,只是顺序略有不同。

所以我写了一些检查代码来查看sort 是否稳定。如果它是稳定的,那么所有value1 值应该保持有序。此代码验证订单,打印任何差异。

int prev = -1;
boolean verified = true;
for (Record record : output)
{
    int curr = record.getValue1();
    if (prev != -1)
    {
        if (prev + 1 != curr)
        {
            System.out.println("Warning: " + prev + " followed by " + curr + "!");
            verified = false;
        }
    }
    prev = curr;
}
System.out.println("Verified: " + verified);

输出:

Warning: 69999 followed by 71875!
Warning: 79061 followed by 70000!
Warning: 71874 followed by 79062!
Warning: 99999 followed by 100625!
Warning: 107811 followed by 100000!
Warning: 100624 followed by 107812!
Verified: false

如果我执行以下任何操作,这种情况仍然存在:

  • ForkJoinPool 替换为ThreadPoolExecutor

    ThreadPoolExecutor pool = new ThreadPoolExecutor(8, 8, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
    
  • 通过直接处理Stream来使用常见的ForkJoinPool

    List<Record> output = stream.collect(Collectors.toList());
    
  • 打电话给parallel()之后我打电话给sorted

    Stream<Record> stream = list.stream().sorted().parallel();
    
  • 请致电parallelStream() 而不是stream().parallel()

    Stream<Record> stream = list.parallelStream().sorted();
    
  • 使用Comparator 进行排序。请注意,此排序标准与我为Comparable 接口定义的“自然”顺序不同,尽管从结果开始已经从头开始按顺序排列,但结果应该仍然相同。

    Stream<Record> stream = list.stream().parallel().sorted(
        (r1, r2) -> Integer.compare(r1.getCategory2(), r2.getCategory2())
    );
    

如果我不在Stream 上执行以下操作之一,我只能得到这个来保留相遇顺序:

  • 不要打电话给parallel()
  • 不要调用sorted 的任何重载。

有趣的是,parallel() 没有排序保留了顺序。

在上述两种情况下,输出都是:

Verified: true

我的 Java 版本是 1.8.0_05。这个异常还有occurs on Ideone,它似乎正在运行Java 8u25。

更新

我已将我的 JDK 升级到撰写本文时的最新版本 1.8.0_45,问题没有改变。

问题

结果List (output) 中的记录顺序是否因为排序不稳定、遇到顺序未保留或其他原因而出现乱序?

如何确保在创建并行流并对其进行排序时保留相遇顺序?

【问题讨论】:

  • 我会尝试制作最简单的程序来重现问题,在最新的 JDK 版本上运行它,如果重现,则提交错误:排序应该是稳定的:它是这样记录的。跨度>

标签: java parallel-processing java-8 java-stream


【解决方案1】:

看起来Arrays.parallelSort 在某些情况下不稳定。好眼力。流并行排序是根据Arrays.parallelSort 实现的,因此它也会影响流。这是一个简化的示例:

public class StableSortBug {
    static final int SIZE = 50_000;

    static class Record implements Comparable<Record> {
        final int sortVal;
        final int seqNum;

        Record(int i1, int i2) { sortVal = i1; seqNum = i2; }

        @Override
        public int compareTo(Record other) {
            return Integer.compare(this.sortVal, other.sortVal);
        }
    }

    static Record[] genArray() {
        Record[] array = new Record[SIZE];
        Arrays.setAll(array, i -> new Record(i / 10_000, i));
        return array;
    }

    static boolean verify(Record[] array) {
        return IntStream.range(1, array.length)
                        .allMatch(i -> array[i-1].seqNum + 1 == array[i].seqNum);
    }

    public static void main(String[] args) {
        Record[] array = genArray();
        System.out.println(verify(array));
        Arrays.sort(array);
        System.out.println(verify(array));
        Arrays.parallelSort(array);
        System.out.println(verify(array));
    }
}

在我的机器(2 核 x 2 线程)上,这会打印以下内容:

true
true
false

当然,它应该打印三遍true。这是在当前的 JDK 9 开发版本上。鉴于您已经尝试过,如果它出现在迄今为止的所有 JDK 8 版本中,我不会感到惊讶。奇怪的是,减小大小或除数会改变行为。 20,000 和 10,000 的除数是稳定的,50,000 和 1,000 的除数也是稳定的。似乎问题与比较相等与并行拆分大小的足够大的值运行有关。

OpenJDK 问题 JDK-8076446 涵盖了这个错误。

【讨论】:

  • (true, true, false) 同样适用于 Windows7 (64),8u40。
  • @StefanZobel 哦,是的,谢谢,我已将新错误作为旧错误的副本关闭。
  • @StuartMarks 感谢您确定此错误的根本原因。我们知道它什么时候会修复吗?修复将应用于 Java 8 还是仅应用于 Java 9 开发?
  • @rgettman 抱歉,对此没有估计。就我个人而言,我建议将修复程序向后移植到 8 更新版本线,但我不能对是否会发生任何承诺。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-22
  • 2015-04-25
  • 2021-11-25
  • 2018-11-11
  • 1970-01-01
  • 1970-01-01
  • 2011-08-29
相关资源
最近更新 更多