Java 8 集合并发处理

我计划在公司内部介绍Java 8中的新功能和概念。

我想关注的是新馆藏库的并行处理能力。

无论我在哪里读到Java 8以及对集合库的更多功能样式迭代器的需求,都提到这将有助于利用当今正常的多核服务器。但很少有人提到这如何成为可能,以及这是否是一个普遍真理,更不用说任何性能基准了。

由于即使是我们公司中声称了解线程的经验丰富的开发人员也不知道实际线程在较低级别是如何工作的,因此我正试图收集该领域的一些知识。我根据阅读了几个博客等做了一组以下断言。

我将不胜感激以下几点的一些反馈(真/假)..

  1. 线程是操作系统中调度的最低单位(是的,基本的东西,但并非所有应用程序程序员都知道这一点;-))

  2. 单线程程序一次只能在一个内核上运行。因此,例如,在四核CPU中,75%的CPU未被利用。

  3. 当前 Java 集合迭代器的问题在于它是一个外部迭代器,不可能(至少开箱即用)将庞大的集合迭代分发到多个线程。新的集合库操作使得无需处理低级别并发问题即可实现并发

  4. Java 8 使得使用增强的集合库使用内部迭代器并行化迭代成为可能

    而不是 Java 7

    for (Shape s : shapes) {if (s.getColor() == RED)s.setColor(BLUE); }

    我们在Java中有8

    shapes.forEach(s -> { if (s.getColor() == RED) s.setColor(BLUE); })

  5. 但是为了并行化上面的迭代,必须显式使用parallel()Stream API

    private static void printUsingCoolLambda (final List<String> names) { names.parallelStream().forEach(s -> System.out.println(s)); System.out.println("Printed using printUsingCoolLambda"); }

    但即便如此,也不能保证该操作将并行完成,因为 Javadoc 会说:“返回一个可能并行的 {@code Stream},并将此集合作为其源。允许此方法返回顺序流”parallelStream()

  6. 最终,不能保证所有内核都会被利用,因为线程调度不是JVM的责任,而是由操作系统决定的。

编辑

我最难得到第5点和第6点。正如各种Java 8博客所说“使用这个新的parallelStream(),你将得到开箱即用的并行处理(免费,你作为一个应用程序程序员不必担心)”,我用一句话提出的问题是,这真的是正确的吗


答案 1

我将不胜感激以下几点的一些反馈(真/假)。

不幸的是,没有一个答案是真的或假的。它们都是“视情况而定”或“这很复杂”。:-)

1:线程是操作系统中调度的最低单位。

这基本上是正确的。操作系统调度线程,并且在大多数情况下,Java 线程对应于操作系统线程。

但是,故事还有更多。我鼓励你不要过多地考虑线程。它们是用于构建并行应用程序的非常低级构造。

当然,可以使用线程编写应用程序,但通常最好使用更高级别的构造。一个这样的构造是任务,它是特定于应用程序的工作块。如果可以将工作负载划分为单独的任务,则可以将这些任务提交给执行程序,Executor 将管理任务到线程上的调度以及线程的创建和销毁。这就是Java SE 5中的东西。java.util.concurrent

构建并行应用程序的另一种方法是使用数据并行性。Java SE 7 引入了 Fork-Join 框架。这是指不是线程的分叉和联接,而是任务的分叉和联接,具体而言,是指表示数据递归可拆分部分的任务。FJ框架对于某些工作负载非常有效,但是任务的拆分和连接是程序员的责任,这可能会很麻烦。

Java SE 8 中的新增功能是 streams API,它以更方便的方式支持数据并行性。

我从你关于线程的问题中推断了相当多的内容,但是你的问题似乎集中在线程上,并行性比线程要多得多。(我的一位同事最近说:“线程是假神。

2:单线程程序一次只能在一个内核上运行。因此,例如,在四核CPU中,75%的CPU未被利用。

大多数情况下是正确的。如果只考虑应用程序线程,则单个线程使用的四核 CPU 永远不会超过 25%。但是,如果您考虑在 JVM 中运行的 Java 线程,那么即使是单线程 Java 应用程序在多核系统上的运行速度也可能比在单核系统上更快。原因是像垃圾回收器这样的 JVM 服务线程可以与多核系统上的应用程序线程并行运行,而它们必须在单核系统上抢占应用程序线程。

3:当前 Java 集合迭代器的问题在于它是一个外部迭代器,不可能(至少开箱即用)将庞大的集合迭代分发到多个线程。新的集合库操作使得无需处理低级别并发问题即可实现并发。

大多数情况下是的。外部迭代内部迭代是概念。外部迭代由实际接口体现。内部迭代可能使用 一个简单的 for 循环、一组分叉连接任务或其他内容。IteratorIterator

与其说是新的集合库,不如说是 Java 8 中的新 Streams API 将提供一种更方便的方式来跨线程分发工作。

4:Java 8使得使用增强的集合库可以使用内部迭代器并行化迭代(... 示例 ...)shapes.forEach

关闭。同样,是新的 Streams 库(而不是集合)提供了方便的并行性。没有像.若要并行处理集合的元素,必须从中拉取并行流。该类中的数组还有各种并行操作。Collection.parallelForEachjava.util.Arrays

5:但是为了并行化上述迭代,必须显式使用Stream API的方法....但即便如此,也不能保证该操作将并行完成。parallel

是的,您需要请求与 or 方法的并行性,具体取决于您是从流还是集合开始。parallelparallelStream

关于没有保证,当然,生活中从来没有任何保证。:-)毕竟,如果你在单核系统上运行,没有什么可以并行运行。另一种情况是,在小程序中,安全管理器可能会禁止应用程序使用多个线程。实际上,在大多数环境中,请求并行流确实会拆分工作负载并并行运行任务。默认情况下,这些任务在公共分叉连接池中运行,默认情况下,该池的线程数与系统中内核数一样多。但是有人可能已经将线程数设置为不同的数字,甚至设置为1,这是API本身无法提供任何保证的原因之一。

6:最终,不能保证所有内核都会被利用,因为线程调度不是JVM的责任,而是由操作系统决定的。由于各种Java 8博客都说“使用这个新的parallelStream(),你将开箱即用地获得并行处理(免费,作为应用程序程序员的你不必担心)”,我用一句话提出的问题是,这真的是正确的吗?

如上所述,不作任何保证。系统中有许多层,事情可以左转。即使您的公共 FJ 池具有与内核一样多的线程,也不能保证每个 Java 线程都有自己的操作系统线程。(在Hotspot JVM中,我认为这总是正确的。这取决于 JVM。在同一系统上可能还有其他进程(甚至是其他JVM)争夺内核,因此您的应用程序可能不会获得您想要的那么多内核。从这个意义上说,JVM受操作系统的支配,为它安排线程。

我不确定该博客文章的来源,但是关于并行处理“免费”和“您不必担心”情绪的一点被夸大了。事实上,这基本上是错误的。

确实,编写并行流比使用早期 API 更方便。但也有可能把它弄得非常非常错误。如果将副作用放入流管道中,则会出现争用条件,并且每次都可能得到不同的错误答案。或者,即使您注意同步副作用,也可能创建足够的争用,以便并行流可能比顺序流运行得更慢。

即使您已经设法避免了这些陷阱,在N核系统上运行并行流也不会给您带来N倍的加速。它只是不以这种方式工作。对于小型工作负载,拆分和联接并行任务的开销占主导地位,这可能导致计算速度比顺序计算慢。对于较大的工作负载,开销被并行加速所抵消,但开销仍然存在。加速量还取决于工作负载的性质、拆分特性、数据的块状性等。调整并行应用程序是一门黑色的艺术。

根据我的经验,对于易于并行化的工作负载,最大限度地利用双核系统非常容易。四核系统通常可以获得至少3倍的加速。有了更多的内核,获得5x-6x的加速并不难,但是要获得超过这个速度的加速需要实际工作。

对于不那么容易并行化的工作负载,您可能必须对应用程序进行大量思考和重组,然后才能尝试并行运行它。

我不会说Java 8“免费”或“无忧无虑”或类似的东西给你并行性。我想说的是,Java 8让你有机会比以前更方便地编写并行程序。但是你仍然需要努力使它正确,你可能仍然需要努力实现你想要的加速。


答案 2

这真的一直都是正确的吗?

它始终是正确的,你希望它一直是正确的。顺序流也可以的特殊允许对于拥有这个有用的功能是绝对必要的:在很多情况下(可能是测试,调试等),您将需要一个简单的顺序流。大多数并发问题都是通过尝试在非并发设置中重现问题来开始解决的。并发调试要困难得多,首先要确保的是它确实需要。

你永远不应该担心CPU核心的利用率:这是一项古老而稳定的技术,在我使用Java的所有经验中,它们确实得到了利用。如果你在 CPU 利用率仪表板上缺少一些百分比,你几乎可以肯定,通过简化锁和其他线程协调,这些问题在 Java 中是可以解决的,而不是完全正确的 Java 程序成为运行时怪癖的受害者。


推荐