期待已久的Java 8发行版中的几个主要增强功能与并发相关,包括java.util.concurrent
层次结构中添加的类以及强大的新并行流功能。 流被设计为与lambda表达式一起使用, lambda表达式是Java 8的附加功能,它也使日常编程的许多其他方面变得更加容易。 (有关lambda表达式和相关interface
更改的介绍,请参见Java 8语言扩展的配套文章 。)
在本文中,我首先向您展示新的CompletableFuture
类如何使异步操作更易于协调。 接下来,我将展示如何使用并行流(Java 8中并发的大赢家)对并行值集执行操作。 我最后看一下Java 8的新功能如何执行,包括与本系列第一篇文章中的一些代码进行比较。 (请参阅相关信息的链接,这篇文章完整的示例代码。)
Future
本系列的第一篇文章向您简要介绍了Java和Scala Future
。 (Java 8之前的)Java版本较弱,仅支持两种使用类型:您可以检查将来是否已经完成,或者可以等待将来完成。 Scala版本更加灵活:您可以在将来完成时执行回调,并且异常完成以Throwable
的形式处理。
Java 8添加了CompletableFuture
类,该类实现了新的CompletionStage
接口并扩展了Future
。 (本节中讨论的所有并发类和接口都在java.util.concurrent
包中。) CompletionStage
表示可能进行异步计算的阶段或步骤。 该接口定义了将CompletionStage
实例与其他实例或代码链接的许多不同方式,例如在完成时要调用的方法(总共有59种方法,而Future
接口中有5种方法)。
清单1显示了ChunkDistanceChecker
类,它基于第一篇文章中的编辑距离比较代码。
ChunkDistanceChecker
public class ChunkDistanceChecker {
private final String[] knownWords;
public ChunkDistanceChecker(String[] knowns) {
knownWords = knowns;
}
/**
* Build list of checkers spanning word list.
*
* @param words
* @param block
* @return checkers
*/
public static List buildCheckers(String[] words, int block) {
List checkers = new ArrayList<>();
for (int base = 0; base < words.length; base += block) {
int length = Math.min(block, words.length - base);
checkers.add(new ChunkDistanceChecker(Arrays.copyOfRange(words, base, base + length)));
}
return checkers;
}
...
/**
* Find best distance from target to any known word.
*
* @param target
* @return best
*/
public DistancePair bestDistance(String target) {
int[] v0 = new int[target.length() + 1];
int[] v1 = new int[target.length() + 1];
int bestIndex = -1;
int bestDistance = Integer.MAX_VALUE;
boolean single = false;
for (int i = 0; i < knownWords.length; i++) {
int distance = editDistance(target, knownWords[i], v0, v1);
if (bestDistance > distance) {
bestDistance = distance;
bestIndex = i;
single = true;
} else if (bestDistance == distance) {
single = false;
}
}
return single ? new DistancePair(bestDistance, knownWords[bestIndex]) :
new DistancePair(bestDistance);
}
}
ChunkDistanceChecker
类的每个实例都将根据一系列已知单词检查目标单词,以找到最佳匹配项。 静态buildCheckers()
方法根据整个已知单词数组和所需的块大小创建List
。 从清单2中的CompletableFutureDistance0
类开始,此ChunkDistanceChecker
类是本文中几种最佳匹配搜索并发实现的基础。
CompletableFuture
编辑距离计算 public class CompletableFutureDistance0 extends TimingTestBase {
private final List chunkCheckers;
private final int blockSize;
public CompletableFutureDistance0(String[] words, int block) {
blockSize = block;
chunkCheckers = ChunkDistanceChecker.buildCheckers(words, block);
}
...
public DistancePair bestMatch(String target) {
List> futures = new ArrayList<>();
for (ChunkDistanceChecker checker: chunkCheckers) {
CompletableFuture future =
CompletableFuture.supplyAsync(() -> checker.bestDistance(target));
futures.add(future);
}
DistancePair best = DistancePair.worstMatch();
for (CompletableFuture future: futures) {
best = DistancePair.best(best, future.join());
}
return best;
}
}
清单2的 CompletableFutureDistance0
类显示了使用CompletableFuture
进行并发计算的一种方法。 supplyAsync()
方法采用Supplier
实例(该方法的函数接口返回类型T
的值),并在使Supplier
排队以使其异步运行时返回CompletableFuture
。 我在第一个for
循环supplyAsync()
lambda表达式传递给supplyAsync()
方法,以构建与ChunkDistanceChecker
数组匹配的期货列表。 第二个for
循环等待每个将来完成(尽管在此循环到达它们之前,因为它们是异步执行的,所以最完整了),并从所有结果中累积最佳匹配。
CompletableFuture
之上 在本系列的第一篇文章中,您看到了Scala Future
,您可以附加完成处理程序并以不同的方式组合Future
。 CompletableFuture
为Java 8提供了类似的灵活性。在本节中,您将学习在编辑距离检查代码的上下文中使用这些功能的一些方法。
清单3显示了清单2中 bestMatch()
方法的另一个版本。 此代码使用带有CompletableFuture
的完成处理程序以及一些较旧的并发类。
CompletableFuture
public DistancePair bestMatch(String target) {
AtomicReference best = new AtomicReference<>(DistancePair.worstMatch());
CountDownLatch latch = new CountDownLatch(chunkCheckers.size());
for (ChunkDistanceChecker checker: chunkCheckers) {
CompletableFuture.supplyAsync(() -> checker.bestDistance(target))
.thenAccept(result -> {
best.accumulateAndGet(result, DistancePair::best);
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted during calculations", e);
}
return best.get();
}
在清单3中 , CountDownLatch
初始化为在代码中创建的期货数量。 在创建每个未来时,我使用CompletableFuture.thenAccept()
方法附加一个处理程序(以java.util.function.Consumer
功能接口的lambda实例的形式CompletableFuture.thenAccept()
。 该处理程序在将来正常完成时执行,它使用AtomicReference.accumulateAndGet()
方法(Java 8中已添加)来更新找到的最佳值,然后减小锁存器。 同时,执行主线程进入try-catch
块并等待释放闩锁。 在所有期货都完成之后,主线程将继续,返回找到的最终最佳价值。
清单4显示了清单2中 bestMatch()
方法的又一个变体。
CompletableFuture
public DistancePair bestMatch(String target) {
CompletableFuture last =
CompletableFuture.supplyAsync(bestDistanceLambda(0, target));
for (int i = 1; i < chunkCheckers.size(); i++) {
last = CompletableFuture.supplyAsync(bestDistanceLambda(i, target))
.thenCombine(last, DistancePair::best);
}
return last.join();
}
private Supplier bestDistanceLambda(int i, String target) {
return () -> chunkCheckers.get(i).bestDistance(target);
}
此代码使用CompletableFuture.thenCombine ()
方法将两个CompletableFuture.thenCombine ()
合并,方法是将java.util.function.BiFunction
(在本例中为DistancePair.best()
方法)应用于这两个结果,并为该结果返回Future功能。
清单4是代码的最简洁,也许是最简洁的版本,但是它的缺点是创建了额外的CompletableFuture
层,以表示每个块操作与先前操作的组合。 从最初的Java 8版本开始,这有可能导致StackOverflowException
,该StackOverflowException
在代码中丢失,从而导致最终的将来永远无法完成。 该错误已得到解决,应在近期的将来版本中修复。
CompletableFuture
定义了这些示例中使用的方法的许多变体。 当您将CompletableFuture
用于您的应用程序时,请检查完成方法和组合方法的完整列表,以找到最符合您需求的方法。
当您执行不同类型的操作时, CompletableFuture
最好使用,并且必须协调结果。 当您对许多不同的数据值运行相同的计算时,并行流为您提供了一种更简单的方法,并可能带来更好的性能。 编辑距离检查示例与并行流方法更好地匹配。
流是Java 8的一项主要新功能,可与lambda表达式结合使用。 流本质上是一系列值上的推式迭代器。 流可以与适配器链接在一起以执行诸如过滤和映射之类的操作,这与Scala序列非常相似。 流也具有顺序和并行变化,再次类似于Scala序列(尽管Scala对并行序列具有单独的类层次结构,而Java 8使用内部标志指示串行或并行)。 存在原始int
, long
和double
类型的流以及类型化的对象流。
新的Streams API太复杂,无法在本文中全面介绍,因此我将重点介绍并发方面。 有关流的更多详细信息,请参见“ 相关主题”部分。
清单5显示了编辑距离最佳匹配代码的另一种形式。 此版本使用清单1中的ChunkDistanceChecker
进行距离计算,并使用清单2示例中的CompletableFuture
,但是这次我使用流来获取最佳匹配结果。
CompletableFuture
public class CompletableFutureStreamDistance extends TimingTestBase {
private final List chunkCheckers;
...
public DistancePair bestMatch(String target) {
return chunkCheckers.stream()
.map(checker -> CompletableFuture.supplyAsync(() -> checker.bestDistance(target)))
.collect(Collectors.toList())
.stream()
.map(future -> future.join())
.reduce(DistancePair.worstMatch(), (a, b) -> DistancePair.best(a, b));
}
}
清单5底部的多行语句使用fluent stream API完成了所有工作:
chunkCheckers.stream()
从List
创建一个流。 .map(checker -> ...
将映射应用于流中的值,在这种情况下,使用与清单2示例相同的技术来构造CompletableFuture
,以异步执行ChunkDistanceChecker.bestDistance()
方法的结果。 .collect(Collectors.toList())
将这些值收集到一个列表中,该.stream()
会转换为流。 .map(future -> future.join())
等待每个future的结果可用, .reduce(...
通过对先前的最佳结果重复应用DistancePair.best()
方法来找到最佳值,然后最新结果。 诚然,那是一团糟。 在您停止阅读之前,让我向您保证,下一个变化形式是更干净,更简单。 清单5的要点是显示如何使用流代替常规循环。
清单5的代码在没有从流到列表再到流的多次转换的情况下会更简单。 在这种情况下,需要进行转换,因为否则,代码将仅在创建future以后立即等待CompletableFuture.join()
方法。
幸运的是,与清单5中的繁琐方法相比,有一种在流上实现并行操作的简便方法。 可以将顺序流变成并行流,并且并行流自动在多个线程之间共享工作,同时使结果可以在以后的阶段进行收集。 清单6显示了如何使用这种方法从List
找到最佳匹配。
public class ChunkedParallelDistance extends TimingTestBase {
private final List chunkCheckers;
...
public DistancePair bestMatch(String target) {
return chunkCheckers.parallelStream()
.map(checker -> checker.bestDistance(target))
.reduce(DistancePair.worstMatch(), (a, b) -> DistancePair.best(a, b));
}
}
同样,最后的多行语句完成了所有工作。 如清单5所示 ,该语句首先从列表中创建一个流,但是此版本使用parallelStream()
方法获取为并行处理设置的流。 (您还可以通过在流上调用parallel()
方法,将常规流转换为并行处理。)下一部分.map(checker -> checker.bestDistance(target))
在已知的块中找到最佳匹配话。 最后一部分.reduce(...
在所有块上累积最佳结果,再次如清单5所示 。
并行流并行执行某些步骤,例如map
和filter
操作。 因此,在幕后, 清单6的代码将map步骤分布在多个线程中,然后再将结果合并到reduce步骤中(不一定按任何特定顺序排列,因为结果来自并行执行的操作)。
对要在流中完成的工作进行分区的能力取决于流中使用的新java.util.Spliterator
接口。 您可能会从名称中猜到, Spliterator
与Iterator
相似。 与Spliterator
,与Iterator
,您可以一次处理一组元素-尽管您可以使用tryAdvance()
或forEachRemaining()
方法对元素进行操作,而不是从Spliterator
获取元素。 但是, Spliterator
器也可以提供其拥有多少元素的估计值,并且可以像分裂中的有丝分裂细胞一样将其拆分为两个部分。 这些附加功能使流并行处理代码可以轻松地将要完成的工作分散到可用线程中。
如果您对清单6的代码看起来有点熟悉,那是因为它很像该系列第一篇文章中的Scala并行集合示例:
def bestMatch(target: String) =
matchers.par.map(m => m.bestMatch(target)).
foldLeft(DistancePair.worstMatch)((a, m) => DistancePair.best(a, m))
您可以看到语法和操作上的一些差异,但是从本质上讲,Java 8并行流代码以与Scala并行集合代码相同的方式执行相同的操作。
到目前为止,所有示例都保留了该系列第一篇文章中保留的比较任务的分块结构,这对于有效处理旧版Java中的并行任务是必需的。 Java 8并行流被设计为自己处理工作划分,因此您可以将一组值作为流进行处理,并且内置的并发处理可以分解该组以将工作分散到可用处理器上。
当您尝试将这种方法应用于编辑距离任务时,会发生一些问题。 如果将处理步骤链接到一个管道中 (流操作序列的正式术语),则每个步骤只能将一个结果传递到管道的下一个阶段。 如果要获得多个结果(例如,最佳距离值和编辑距离任务中使用的相应已知单词),则必须将它们作为对象传递。 但是,与分块方法相比,为每个单独比较的结果创建对象将损害直接流方法的性能。 更糟糕的是,编辑距离计算会重用一对分配的数组。 数组不能在并行计算中共享,因此需要为每个计算重新分配它们。
幸运的是,尽管需要进行更多工作,但streams API为您提供了一种有效处理这种情况的方法。 清单7演示了如何使用流来处理全部计算,而无需创建中间对象或工作数组的多余副本。
public class NonchunkedParallelDistance extends TimingTestBase
{
private final String[] knownWords;
...
private static int editDistance(String target, String known, int[] v0, int[] v1) {
...
}
public DistancePair bestMatch(String target) {
int size = target.length() + 1;
Supplier supplier = () -> new WordChecker(size);
ObjIntConsumer accumulator = (t, value) -> t.checkWord(target, knownWords[value]);
BiConsumer combiner = (t, u) -> t.merge(u);
return IntStream.range(0, knownWords.length).parallel()
.collect(supplier, accumulator, combiner).result();
}
private static class WordChecker {
protected final int[] v0;
protected final int[] v1;
protected int bestDistance = Integer.MAX_VALUE;
protected String bestKnown = null;
public WordChecker(int length) {
v0 = new int[length];
v1 = new int[length];
}
protected void checkWord(String target, String known) {
int distance = editDistance(target, known, v0, v1);
if (bestDistance > distance) {
bestDistance = distance;
bestKnown = known;
} else if (bestDistance == distance) {
bestKnown = null;
}
}
protected void merge(WordChecker other) {
if (bestDistance > other.bestDistance) {
bestDistance = other.bestDistance;
bestKnown = other.bestKnown;
} else if (bestDistance == other.bestDistance) {
bestKnown = null;
}
}
protected DistancePair result() {
return (bestKnown == null) ? new DistancePair(bestDistance) : new
DistancePair(bestDistance, bestKnown);
}
}
}
清单7使用可变的结果容器类(这里是WordChecker
类)来组合结果。 bestMatch()
方法使用lambda形式的三个移动部分来实现比较:
Supplier supplier
lambda提供结果容器的实例。 ObjIntConsumer accumulator
lambda将新值累加到结果容器中。 BiConsumer combiner
lambda合并两个结果容器以获得组合值。 在定义了这三个lambda之后, bestMatch()
的最后bestMatch()
语句创建一个并行的int
值流,以将索引值插入到已知单词数组中,并将该流馈送到IntStream.collect()
方法。 collect()
方法使用三个lambda来完成所有实际工作。
图1显示了在使用Oracle的Java 8(用于64位Linux®)的四核AMD系统上运行测试代码时,测得的性能如何随不同的块大小而变化。 与本系列第一篇文章中的时间安排一样,每个输入单词又与12,564个已知单词进行比较,并且每个任务都在已知单词范围内找到最佳匹配。 整个933个拼写错误的输入字会重复运行,在两次传递之间会暂停以使JVM稳定下来。 图1中使用了10次通过后的最佳时间。 最终的块大小为16384,大于已知字的数量,因此这种情况显示了单线程性能。 时序测试中包括的实现是本文的四个主要变体,也是与第一篇文章相比最佳的总体变体:
CompletableFutureDistance0
CompletableFutureStreamDistance
ChunkedParallelDistance
ForkJoinDistance
NonchunkedParallelDistance
图1显示了新的Java 8并行流方法令人印象深刻的结果,尤其是完全流式化的清单7 NchunkPar
。 用于消除对象创建的优化会在计时结果中显示(图表中仅一个值,因为此方法不使用块大小),与其他任何方案的最佳性能相匹配。 CompletableFuture
方法的性能稍有不足,但这并不出乎意料,因为此示例无法发挥班级的优势。 清单5的 ChunkPar
时间与第一篇文章中的ForkJoin
代码大致相同,尽管具有较小的块大小敏感性。 正如您希望看到的那样,所有一次测试单词块的变体在小块大小的情况下都表现出较差的性能,因为相对于实际的计算工作而言,对象创建的开销更高。
就像第一篇文章中的计时结果一样,这些结果只是对您自己的应用程序可能会看到的性能的一般指导。 这里最重要的一点是,新Java 8并行流在正确使用时可以提供卓越的性能。 将良好的性能与流的功能编码样式的开发优势结合在一起,并且在任何时候要对值集合进行计算时,您都将获得成功。
Java 8向开发人员的工具箱添加了一些重要的新功能。 在并发方面,并行流实现是快速且易于使用的,尤其是当与lambda表达式结合使用时,具有类似于函数的编程风格,可以清晰,简洁地表达您的意图。 当您处理单个活动时,新的CompletableFuture
类还有助于简化并发编程,而流模型并不容易应用到这些活动。
下一篇JVM并发性文章将转到Scala方面,并探讨处理异步计算的另一种有趣方法。 使用async
宏,您可以编写看起来像在执行顺序阻塞操作的代码,但是在幕后,Scala会将代码转换为完全非阻塞的结构。 我将给出一些有关此功能如何有用的示例,并介绍其实现方式。 谁知道?Scala的一些新工作也许会将其纳入Java 9。
翻译自: https://www.ibm.com/developerworks/java/library/j-jvmc2/index.html