如果您听Oracle的人谈论Java 8背后的设计选择,您会经常听到并行是主要动机。并行化是lambda,流API和其他背后的驱动力。让我们看一下流API的示例。
private long countPrimes(int max) {
return range(1, max).parallel().filter(this::isPrime).count();
}
private boolean isPrime(long n) {
return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
}
在这里,我们有一种方法 countPrimes 可以计算介于1和最大值之间的质数。通过范围方法创建数字流。然后将流切换到并行模式。不是质数的数字将被过滤掉,剩余的数字将被计数。
您会看到流API使我们能够以简洁明了的方式描述问题。而且,并行化只是调用该 parallel() 方法的问题。当我们这样做时,流被分成多个块,每个块被独立处理,并在最后汇总结果。由于我们对该isPrime 方法的实现 效率极低且占用大量CPU,因此我们可以利用并行化的优势并利用所有可用的CPU内核。
让我们看另一个例子:
private List getStockInfo(Stream symbols) {
return symbols.parallel()
.map(this::getStockInfo) //slow network operation
.collect(toList());
}
我们在输入中列出了股票代号列表,我们必须调用慢速网络操作来获取有关股票的一些详细信息。在这里,我们不处理CPU密集型操作,但是我们也可以利用并行化。并行执行多个网络请求是一个好主意。同样,对于并行流来说这是一项不错的任务,您是否同意?
如果这样做,请再次查看前面的示例。有个大错误。你看到了吗?问题在于,所有并行流都使用公共的fork-join线程池,并且,如果您提交长时间运行的任务,则可以有效地阻塞池中的所有线程。因此,您将阻止所有其他使用并行流的任务。想象一下一个servlet环境,其中一个请求调用getStockInfo() 而另一个请求调用 countPrimes()。一个将阻止另一个,即使它们每个都需要不同的资源。更糟糕的是,您无法为并行流指定线程池;整个类加载器必须使用相同的加载器。
让我们在以下示例中对其进行说明:
private void run() throws InterruptedException { ExecutorService es = Executors.newCachedThreadPool(); // Simulating
在这里,我们模拟系统中的六个线程。他们所有人都在执行CPU密集型任务,第一个被“破坏”并在找到质数后立即睡眠一秒钟。这只是一个人为的例子。您可以想象一个线程被卡住或执行阻塞操作。
问题是:执行此代码会发生什么?我们有六个任务;其中之一将需要一整天才能完成,其余的要早得多。毫不奇怪,每次执行代码时,都会得到不同的结果。有时,所有健康的任务都会完成;其他时间,其中一些滞后于缓慢的时间。您是否希望在生产系统中有这种行为?一项破碎的任务使其他应用程序瘫痪了吗?我猜不是。
如何确保这种事情永远不会发生只有两种选择。首先是确保提交到公共fork-join池的所有任务不会卡住并在合理的时间内完成。但这说起来容易做起来难,尤其是在复杂的应用程序中。另一个选择是不使用并行流,等到Oracle允许我们指定要用于并行流的线程池。