Java 8提供java.util.Arrays.parallelSort
,它使用fork-join框架并行排序数组。但是对于排序列表没有相应的Collections.parallelSort
。
我可以使用toArray
,对该数组进行排序,并将结果存储回我的列表中,但这会暂时增加内存使用量,如果我使用并行排序已经很高,因为只有并行排序为巨额名单付出代价。我没有使用两倍的内存(列表加上parallelSort&#39的工作内存),而是使用了三次(列表,临时数组和parallelSort的工作内存)。 (Arrays.parallelSort文档说"算法需要的工作空间不大于原始数组的大小"。)
除了内存使用,Collections.parallelSort对于看似合理的常见操作也会更方便。 (我倾向于不直接使用数组,所以我当然比Arrays.parallelSort更频繁地使用它。)
图书馆可以测试RandomAccess,以避免尝试例如快速排序链表,这样就不会有故意遗漏的原因。
如何在不创建临时数组的情况下并行对List进行排序?
答案 0 :(得分:22)
似乎没有任何直接的方法可以在Java 8中并行排序List
。我不认为这在根本上是困难的;它看起来更像是对我的疏忽。
假设Collections.parallelSort(list, cmp)
的难点在于Collections
实现对列表的实现或其内部组织一无所知。通过检查Collections.sort(list, cmp)
的Java 7实现可以看出这一点。正如您所观察到的,它必须将列表元素复制到数组中,对它们进行排序,然后将它们复制回列表中。
这是List.sort(cmp)
扩展方法优于Collections.sort(list, cmp)
的最大优势。看起来这似乎只是一个小的语法优势,能够写myList.sort(cmp)
而不是Collections.sort(myList, cmp)
。区别在于myList.sort(cmp)
是一个接口扩展方法,可以被特定的List
实现覆盖。例如,ArrayList.sort(cmp)
使用Arrays.sort()
对列表进行排序,而默认实现实现旧的copyout-sort-copyback技术。
应该可以向parallelSort
接口添加List
扩展方法,该方法具有与List.sort
类似的语义,但并行进行排序。这样,ArrayList
可以使用Arrays.parallelSort
进行简单的就地排序。 (我并不完全清楚默认实现应该做什么。执行copyout-parallelSort-copyback可能仍然值得。)因为这将是一个API更改,它不会发生,直到Java SE的下一个主要版本。
对于Java 8解决方案,有一些解决方法,没有一个非常漂亮(通常是解决方法)。您可以创建自己的基于数组的List
实现并覆盖sort()
以进行并行排序。或者您可以继承ArrayList
,覆盖sort()
,通过反射获取elementData
数组并在其上调用parallelSort()
。当然,您可以编写自己的List
实现并提供parallelSort()
方法,但覆盖List.sort()
的优势在于,这适用于普通的List
界面,并且您不需要#39; t必须修改代码库中的所有代码才能使用不同的List
子类。
答案 1 :(得分:5)
我认为您注定要使用使用自己的List
扩充的自定义parallelSort
实施,或者更改所有其他代码以将大数据存储在Array
类型中。
这是抽象数据类型层的固有问题。它们意味着将程序员与实现细节隔离开来。但是当实现的细节很重要时 - 例如在排序的底层存储模型的情况下 - 否则精彩的隔离让程序员无能为力。
标准List
排序文档提供了一个示例。在使用 mergesort 的解释之后,他们说
默认实现获取包含此列表中所有元素的数组,对数组进行排序,并迭代此列表,从数组中的相应位置重置每个元素。 (这样可以避免尝试对链接列表进行排序所导致的n2 log(n)性能。)
换句话说,"因为我们不知道List
的基础存储模型,如果我们这样做就无法触及它,我们制作一个已知的副本方式&#34。带括号的表达式基于List
"第i个元素访问器"在链表上是Omega(n),因此用它实现的正常数组mergesort将是一场灾难。实际上,在链表上有效地实现mergesort很容易。 List
实施者被禁止这样做。
List
上的并行排序存在同样的问题。标准顺序排序使用具体sort
实现中的自定义List
进行修复。 Java人们还没有选择去那里。也许在Java 9中。
答案 2 :(得分:3)
使用以下内容:
yourCollection.parallelStream().sorted().collect(Collectors.toList());
由于parallelStream()
,这在排序时将是平行的。我相信这是你的并行排序的意思吗?
答案 3 :(得分:0)
这里只是推测,但我看到通用排序算法更喜欢在数组而不是List
实例上工作的几个很好的理由:
RandomAccess
的列表,与普通数组访问相比,这可能意味着很多开销,这些访问可以很好地优化。List
实例都不容易被复制。必须分配新的清单,这会产生两个问题。首先,这意味着分配一些新对象,这可能比分配数组更昂贵。其次,算法必须选择应为此临时结构分配List
的实现。有两个明显的解决方案,都是坏的:要么只是选择一些硬编码的实现,例如ArrayList
,但它也可以只分配简单的数组(如果我们正在生成数组,那么如果soiurce也是一个数组则更容易)。或者,让用户提供一些列表工厂对象,这会使代码更加复杂。List
接口提供的最佳方法是addAll()
方法,但对于大多数情况来说这可能效率不高(想想将新列表预先分配到其目标大小与逐个添加元素,许多实现都会这样做)。因此,设计人员可能会考虑CPU效率和代码简单性,当API接受数组时,这很容易实现。一些语言,例如Scala,有直接在列表上工作的排序方法,但这需要付出代价,并且在许多情况下排序数组的效率可能较低(或者有时可能只是在幕后执行数组转换)。
答案 4 :(得分:0)
通过结合现有答案,我得出了这段代码。
如果您对创建自定义List类不感兴趣,又不想打扰创建临时数组(Collections.sort
仍在这样做),则此方法有效。
这将使用初始列表,并且不会像parallelStream
解决方案中那样创建新列表。
// Convert List to Array so we can use Arrays.parallelSort rather than Collections.sort.
// Note that Collections.sort begins with this very same conversion, so we're not adding overhead
// in comparaison with Collections.sort.
Foo[] fooArr = fooLst.toArray(new Foo[0]);
// Multithread the TimSort. Automatically fallback to mono-thread when size is less than 8192.
Arrays.parallelSort(fooArr, Comparator.comparingStuff(Foo::yourmethod));
// Refill the List using the sorted Array, the same way Collections.sort does it.
ListIterator<Foo> i = fooLst.listIterator();
for (Foo e : fooArr) {
i.next();
i.set((Foo) e);
}