这是线程同步系列两部分的第二部分。如果您错过了第一篇文章,请查看java线程同步与并发(一)在上一篇文章中,我们介绍了 Java 和 CPU 的线程概念、同步技术以及内存模型。在本文中,我们将重点介绍并发的概念、如何以并发模式执行任务,以及 Java 中提供并发(线程池)的各种类和服务。
多线程的成本
在讨论并发之前,我们必须了解与多线程相关的成本。成本包括应用程序设计、CPU、内存和上下文切换。以下是并发性的一些注意事项
1、程序设计必须非常详细,涵盖所有可能的情况,以支持使用多线程的并发性。最好涵盖设计中的所有极端情况,以便开发人员知道如何处理不会导致应用程序陷入死锁或类似问题的场景。多线程使与共享数据访问、读写锁和临界区相关的程序设计更加复杂。
2、上下文切换------当CPU从执行一个线程切换到另一个线程时,CPU需要保存当前线程的本地数据、程序指针等。然后,CPU加载数据和下一个将要执行的线程的程序指针。这称为上下文切换,如果上下文切换太多,就会占用大量 CPU 周期。
3、上下文切换始终是一种开销,而且成本很高。一个资源和 CPU 密集型进程, CPU 必须为线程堆栈分配内存并为上下文切换分配 CPU 时间
线程池(Executor)
Java Executor 框架由 Executor、ExecutorService 和 Executors 组成。线程池由 ExecutorService 的实例表示。可以将任务提交给ExecutorService。
Executor - 这是核心接口,是并行执行的抽象。这将任务与执行分开(与将两者结合在一起的线程不同)。 Executor 可以运行任意数量的 Runnable 任务,但一个线程只能运行一个任务。
ExecutorService - 这也是一个接口,也是Executor接口的扩展。它提供了返回 Future 对象(结果)、终止或关闭线程池的工具。 ExecutorService 的两个主要方法是execute()(执行任务,不返回对象)和submit()(执行任务并返回Future)。 Future.get() 返回结果,是一个阻塞方法,将等待执行完成。也可以使用 Future 对象取消执行。
Executors - 这是一个实用程序类(类似于 Java Collection 框架中的 Collections)。该类提供工厂方法来创建不同类型的线程池。稍后将对此进行更详细的介绍
Executor 定义了execute() 方法,该方法接受Runnable,而ExecutorService.submit() 接受Runnable 或Callable。 commit() 返回一个 Future 对象。
ExecutorService 提供 shutdown() 和 shutdownNow() 方法来控制线程池。 shutdown() 允许先前提交的任务在终止池之前完成。 shutdownNow() 与 shutdown() 类似,但队列中的待处理任务将被中止。
提供的并发线程池
正如前面提到的,线程池实例由 ExecutorService 的实例表示。可以根据每个用例创建多种类型的线程池:
SingleThreadExecutor - 一种只有一个线程的线程池。所有提交的任务将按顺序执行。通过此方法创建线程池的实例
Executors.newSingleThreadExecutor()。
缓存线程池 - 创建并行执行所需数量的线程池。较旧的可用线程将被重新用于新任务。如果某个线程在过去 60 秒内没有被使用,它将被终止并从池中删除。通过此方法创建线程池
Executors.newCachedThreadPool()。
固定线程池 - 具有固定数量线程的线程池。如果线程不可用于某个任务,则该任务将被放入队列中。该任务保留在队列中,直到其他任务完成或当线程空闲时,它从队列中选取该任务来执行它。通过此方法创建线程池
Executors.newFixedThreadPool()。
计划线程池 - 用于计划未来任务的线程池。如果需要按固定或计划的时间间隔执行任务,请使用此执行器。例如:创建此线程池的实例
Executors.newScheduledThreadPool()。
单线程调度池 - 一种只有一个线程来调度任务的线程池。通过此方法创建线程池
Executors.newSingleThreadScheduledPool() 。
ForkJoinPool
ForkJoinPool 与 ExecutorService 类似,但有一个区别是 ForkJoinPool 将工作单元拆分为更小的任务(fork 进程),然后提交到线程池。 分拆步骤,是一个递归过程。分拆过程将持续下去,直到达到极限,此时无法再分割成进一步的子任务。所有子任务都执行完毕,主任务等待所有子任务执行完毕。主要任务连接所有单独的结果并返回最终的单个结果。这是join过程,其中对结果进行整理并构建单个数据作为最终结果。
分拆任务
合并结果
有两种方法可以将任务提交到 ForkJoinPool: RecursiveAction - 不返回任何值的任务。它执行一些工作(例如,将文件从磁盘复制到远程位置,然后退出)。它可能仍然需要将其工作分解为更小的块,这些块可以由独立的线程或 CPU 执行。 RecursiveAction 可以通过子类化它来实现。 RecursiveTask - 将结果返回到 ForkJoinPool 的任务。它可以将其工作分解为较小的任务,并将较小任务的结果合并为一个结果。将工作分解为子任务并合并可以在多个级别上进行。
总结
希望本系列能够帮助读者更好地理解线程、同步机制、内存模型以及 Java 中并发和并行性的线程池的各个方面。这是一个很大的主题,需要新手深入挖掘,以深入了解如何构建健壮的并发应用程序。