JVM 并发性: Java 和 Scala 并发性基础001

1.Java™ 平台对所有基于 JVM 的语言中的并发编程提供了优秀的支持。Scala 扩展了 Java 语言中的并发性支持,提供了更多在处理器之间共享工作和协调结果的方式。

从那时起,处理器制造商更多地是通过增加核心来提高芯片性能,而不再通过增加时钟速率来提高芯片性能。多核系统现在成为了从手机到企业服务器等所有设备的标准,而这种趋势可能继续并有所加速。开发人员越来越需要在他们的应用程序代码中支持多个核心,这样才能满足性能需求。

您将了解一些针对 Java 和 Scala 语言的并发编程的新方法,包括 Java 如何将 Scala 和其他基于 JVM 的语言中已经探索出来的理念结合在一起

您将了解如何使用 Java ExecutorServiceForkJoinPool 类来简化并发编程。还将了解一些将并发编程选项扩展到纯 Java 中的已有功能之外的基本 Scala 特性。在此过程中,您会看到不同的方法对并发编程性能有何影响。后续几期文章将会介绍 Java 8 中的并发性改进和一些扩展,包括用于执行可扩展的 Java 和 Scala 编程的 Akka 工具包。

Java 并发性支持

在 Java 平台诞生之初,并发性支持就是它的一个特性,线程和同步的实现为它提供了超越其他竞争语言的优势。Scala 基于 Java 并在 JVM 上运行,能够直接访问所有 Java 运行时(包括所有并发性支持)。所以在分析 Scala 特性之前,我首先会快速回顾一下 Java 语言已经提供的功能。


Java 线程基础

在 Java 编程过程中创建和使用线程非常容易。它们由 java.lang.Thread 类表示,线程要执行的代码为 java.lang.Runnable 实例的形式。如果需要的话,可以在应用程序中创建大量线程,您甚至可以创建数千个线程。在有多个核心时,JVM 使用它们来并发执行多个线程;超出核心数量的线程会共享这些核心。

线程操作的协调难以让人理解。只要从程序的角度让所有内容保持一致,Java 编译器和 JVM 就不会对您代码中的操作重新排序,这使得问题变得更加复杂。

例如:如果 a 线程 : 两个相加操作q =  m+b , b  线程 需要 读取 q 的值 ,  (但是 编译器或 JVM 不不会保证按照与指定的顺序来执行 ) 这样 b 线程 可能 在 a 线程执行

的过程中 , 就读取了 q 的数值 , 这样就不对 ;

硬件也有可能带来线程问题。现代系统使用了多种缓存内存级别,一般来讲,不是系统中的所有核心都能同时看到这些缓存。当某个核心修改内存中的一个值时,其他核心可能不会立即看到此更改。

由于这些问题,在一个线程使用另一个线程修改的数据时,您必须显式地控制线程交互方式。Java 使用了特殊的操作来提供这种控制,在不同线程看到的数据视图中建立顺序。基本操作是,线程使用 synchronized 关键字来访问一个对象。当某个线程在一个对象上保持同步时,该线程将会获得此对象所独有的一个锁的独占访问。如果另一个线程已持有该锁,等待获取该锁的线程必须等待,或者被阻塞,直到该锁被释放。当该线程在一个 synchronized 代码块内恢复执行时,Java 会保证该线程可以 “看到了” 以前持有同一个锁的其他线程写入的所有数据,但只是这些线程通过离开自己的 synchronized 锁来释放该锁之前写入的数据。这种保证既适用于编译器或 JVM 所执行的操作的重新排序,也适用于硬件内存缓存。一个 synchronized 块的内部是您代码中的一个稳定性孤岛,其中的线程可依次安全地执行、交互和共享信息。


Java 5:并发性的转折点

Java 从一开始就包含对线程和同步的支持。但在线程间共享数据的最初规范不够完善,这带来了 Java 5 的 Java 语言更新中的重大变化 (JSR-133)。Java Language Specification for Java 5 更正并规范化了 synchronizedvolatile 操作。该规范还规定不变的对象如何使用多线程。(基本上讲,只要在执行构造函数时不允许引用 “转义”,不变的对象始终是线程安全的。)以前,线程间的交互通常需要使用阻塞的 synchronized 操作。这些更改支持使用 volatile 在线程间执行非阻塞协调。因此,在 Java 5 中添加了新的并发集合类来支持非阻塞操作 — 这与早期仅支持阻塞的线程安全方法相比是一项重大改进。

在变量上对 volatile 关键字的使用,为线程间的安全交互提供了一种稍微较弱的形式。synchronized 关键字可确保在您获取该锁时可以看到其他线程的存储,而且在您之后,获取该锁的其他线程也会看到您的存储。volatile 关键字将这一保证分解为两个不同的部分。如果一个线程向 volatile 变量写入数据,那么首先将会擦除它在这之前写入的数据。如果某个线程读取该变量,那么该线程不仅会看到写入该变量的值,还会看到写入的线程所写入的其他所有值。所以读取一个 volatile 变量会提供与输入 一个 synchronized 块相同的内存保证,而且写入一个 volatile 变量会提供与离开 一个 synchronized 块相同的内存保证。但二者之间有很大的差别:volatile 变量的读取或写入绝不会受阻塞。


在变量上对 volatile 关键字的使用,为线程间的安全交互提供了一种稍微较弱的形式。synchronized 关键字可确保在您获取该锁时可以看到其他线程的存储,而且在您之后,获取该锁的其他线程也会看到您的存储。volatile 关键字将这一保证分解为两个不同的部分。如果一个线程向 volatile 变量写入数据,那么首先将会擦除它在这之前写入的数据。如果某个线程读取该变量,那么该线程不仅会看到写入该变量的值,还会看到写入的线程所写入的其他所有值。所以读取一个 volatile 变量会提供与输入 一个 synchronized 块相同的内存保证,而且写入一个 volatile 变量会提供与离开 一个 synchronized 块相同的内存保证。但二者之间有很大的差别:volatile 变量的读取或写入绝不会受阻塞。


ava.util.concurrent 分层结构包含一些集合变形,它们支持并发访问、针对原子操作的包装器类,以及同步原语。这些类中的许多都是为支持非阻塞访问而设计的,这避免了死锁的问题,而且实现了更高效的线程。这些类使得定义和控制线程之间的交互变得更容易,但他们仍然面临着基本线程模型的一些复杂性。

java.util.concurrent 包中的一对抽象,支持采用一种更加分离的方法来处理并发性:Future<T> 接口、ExecutorExecutorService 接口。这些相关的接口进而成为了对 Java 并发性支持的许多 Scala 和 Akka 扩展的基础,所以更详细地了解这些接口和它们的实现是值得的。

java.util.concurrent 包中的一对抽象,支持采用一种更加分离的方法来处理并发性:Future<T> 接口、ExecutorExecutorService 接口。这些相关的接口进而成为了对 Java 并发性支持的许多 Scala 和 Akka 扩展的基础,所以更详细地了解这些接口和它们的实现是值得的。

Future<T> 是一个 T 类型的值的持有者,但奇怪的是该值一般在创建 Future 之后才能使用。正确执行一个同步操作后,才会获得该值。收到 Future 的线程可调用方法来:

  • 查看该值是否可用
  • 等待该值变为可用
  • 在该值可用时获取它
  • 如果不再需要该值,则取消该操作

Future 的具体实现结构支持处理异步操作的不同方式。

Executor 是一种围绕某个执行任务的东西的抽象。这个 “东西” 最终将是一个线程,但该接口隐藏了该线程处理执行的细节。

Executor 本身的适用性有限,ExecutorService 子接口提供了管理终止的扩展方法,并为任务的结果生成了 FutureExecutor 的所有标准实现还会实现 ExecutorService



并发性性能

要充分利用系统上可用的处理器数量,必须为 ExecutorService 配置至少与处理器一样多的线程。您还必须将至少与处理器一样多的任务传递给 ExecutorService 来执行。实际上,您或许希望拥有比处理器多得多的任务,以实现最佳的性能。这样,处理器就会繁忙地处理一个接一个的任务,近在最后才空闲下来。但是因为涉及到开销(在创建任务和 future 的过程中,在任务之间切换线程的过程中,以及最终返回任务的结果时),您必须保持任务足够大,以便开销是按比例减小的。


Fork-Join

Java 7 引入了 ExecutorService 的另一种实现: ForkJoinPool 类。 ForkJoinPool 是为高效处理可反复分解为子任务的任务而设计的,它使用 RecursiveAction 类(在任务未生成结果时)或 RecursiveTask<T> 类(在任务具有一个 T 类型的结果时)来处理任务。 RecursiveTask<T> 提供了一种合并子任务结果的便捷方式


这些结果表明,如果可调节应用程序中的任务大小来实现最佳的性能,那么使用标准 ThreadPoolForkJoin 更好。但请注意,ThreadPool 的 “最佳性能点” 取决于具体任务、可用处理器数量以及您系统的其他因素。一般而言,ForkJoin 以最小的调优需求带来了优秀的性能,所以最好尽可能地使用它。



Java ForkJoin 代码的性能比每种 Scala 实现都更好,但 DirectBlockingDistance 在 1,024 的块大小下提供了更好的性能。两种 Scala 实现在大部分块大小下,都提供了比 清单 1 中的 ThreadPool 代码更好的性能。













©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页