Executors
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/executors.html
在所有先前的示例中,新线程执行的任务与其Runnable
对象定义的线程本身(由Thread
对象定义)之间存在密切联系。这对于小型应用程序效果很好,但在大型应用程序中,将线程管理和创建与应用程序的其余部分分离是有意义的。封装这些功能的对象称为executors。以下小节详细描述了 executors。
- Executor Interfaces 定义了三种 executor 对象类型。
- Thread Pools 是最常见的 executor 实现类型。
- Fork/Join 是一个利用多处理器的框架(JDK 7 中新增)。
执行器接口
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html
java.util.concurrent
包定义了三个执行器接口:
Executor
是一个简单的接口,支持启动新任务。ExecutorService
是Executor
的子接口,增加了一些功能,有助于管理单个任务和执行器本身的生命周期。ScheduledExecutorService
是ExecutorService
的子接口,支持未来和/或定期执行任务。
通常,引用执行器对象的变量声明为这三种接口类型之一,而不是执行器类类型。
Executor
接口
Executor
接口提供了一个方法 execute
,旨在成为常见线程创建习语的替代品。如果 r
是一个 Runnable
对象,e
是一个 Executor
对象,你可以替换
(new Thread(r)).start();
with
e.execute(r);
然而,execute
的定义不太具体。低级习语创建一个新线程并立即启动它。根据 Executor
的实现,execute
可能会做同样的事情,但更有可能使用现有的工作线程来运行 r
,或者将 r
放入队列等待工作线程可用。(我们将在线程池部分描述工作线程。)
java.util.concurrent
中的执行器实现旨在充分利用更高级的 ExecutorService
和 ScheduledExecutorService
接口,尽管它们也与基本的 Executor
接口一起工作。
ExecutorService
接口
ExecutorService
接口通过类似但更灵活的 submit
方法来补充 execute
。与 execute
一样,submit
接受 Runnable
对象,但也接受 Callable
对象,允许任务返回一个值。submit
方法返回一个 Future
对象,用于检索 Callable
返回值并管理 Callable
和 Runnable
任务的状态。
ExecutorService
还提供了提交大量 Callable
对象的方法。最后,ExecutorService
提供了一些方法来管理执行器的关闭。为了支持立即关闭,任务应正确处理中断。
ScheduledExecutorService
接口
ScheduledExecutorService
接口通过 schedule
补充了其父接口 ExecutorService
的方法,该方法在指定延迟后执行 Runnable
或 Callable
任务。此外,该接口定义了 scheduleAtFixedRate
和 scheduleWithFixedDelay
,以在定义的间隔时间内重复执行指定任务。
线程池
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
java.util.concurrent
中的大多数执行器实现使用线程池,其中包含工作线程。这种类型的线程与它执行的Runnable
和Callable
任务分开存在,并经常用于执行多个任务。
使用工作线程可以最小化由于线程创建而产生的开销。线程对象使用大量内存,在大规模应用程序中,分配和释放许多线程对象会产生显著的内存管理开销。
一种常见的线程池类型是固定线程池。这种类型的池始终有指定数量的线程在运行;如果某个线程在仍在使用时被终止,它将自动被新线程替换。任务通过内部队列提交到池中,当活动任务多于线程时,队列会保存额外的任务。
使用固定线程池的一个重要优势是应用程序在使用它时优雅降级。要理解这一点,考虑一个 Web 服务器应用程序,其中每个 HTTP 请求都由一个单独的线程处理。如果应用程序只是为每个新的 HTTP 请求创建一个新线程,并且系统接收到的请求多于它立即处理的能力,当所有这些线程的开销超过系统容量时,应用程序将突然停止响应所有请求。通过限制可以创建的线程数量,应用程序将不会像请求进来那样快速地为 HTTP 请求提供服务,但它将以系统能够维持的速度为它们提供服务。
创建使用固定线程池的执行器的简单方法是在java.util.concurrent.Executors
中调用newFixedThreadPool
工厂方法。该类还提供以下工厂方法:
newCachedThreadPool
方法创建一个具有可扩展线程池的执行器。此执行器适用于启动许多短暂任务的应用程序。newSingleThreadExecutor
方法创建一个一次执行一个任务的执行器。- 几个工厂方法是上述执行器的
ScheduledExecutorService
版本。
如果上述工厂方法提供的任何执行器都不符合您的需求,构造java.util.concurrent.ThreadPoolExecutor
或java.util.concurrent.ScheduledThreadPoolExecutor
的实例将为您提供额外的选项。
分叉/合并
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
分叉/合并框架是ExecutorService
接口的一种实现,可以帮助你充分利用多个处理器。它专为可以递归地分解为较小片段的工作而设计。目标是利用所有可用的处理能力来提高应用程序的性能。
与任何ExecutorService
实现一样,分叉/合并框架将任务分配给线程池中的工作线程。分叉/合并框架的独特之处在于它使用工作窃取算法。工作线程如果没有任务可执行,可以从其他仍在忙碌的线程中窃取任务。
分叉/合并框架的核心是ForkJoinPool
类,它是AbstractExecutorService
类的扩展。ForkJoinPool
实现了核心的工作窃取算法,并可以执行ForkJoinTask
进程。
基本用法
使用分叉/合并框架的第一步是编写执行一部分工作的代码。你的代码应该类似于以下伪代码:
if (my portion of the work is small enough) do the work directly else split my work into two pieces invoke the two pieces and wait for the results
将这段代码封装在一个ForkJoinTask
子类中,通常使用其中的一个更专门的类型,要么是RecursiveTask
(可以返回结果),要么是RecursiveAction
。
当你的ForkJoinTask
子类准备就绪后,创建代表所有要完成工作的对象,并将其传递给ForkJoinPool
实例的invoke()
方法。
清晰的模糊
为了帮助你理解分叉/合并框架的工作原理,请考虑以下示例。假设你想要对图像进行模糊处理。原始源图像由一个整数数组表示,其中每个整数包含单个像素的颜色值。模糊后的目标图像也由一个与源图像大小相同的整数数组表示。
执行模糊操作是通过逐个像素地处理源数组来完成的。每个像素与其周围像素(红色、绿色和蓝色分量取平均值)进行平均,结果放入目标数组中。由于图像是一个大数组,这个过程可能需要很长时间。你可以利用多处理器系统上的并发处理,使用分叉/合并框架来实现算法。以下是一个可能的实现:
public class ForkBlur extends RecursiveAction { private int[] mSource; private int mStart; private int mLength; private int[] mDestination; // Processing window size; should be odd. private int mBlurWidth = 15; public ForkBlur(int[] src, int start, int length, int[] dst) { mSource = src; mStart = start; mLength = length; mDestination = dst; } protected void computeDirectly() { int sidePixels = (mBlurWidth - 1) / 2; for (int index = mStart; index < mStart + mLength; index++) { // Calculate average. float rt = 0, gt = 0, bt = 0; for (int mi = -sidePixels; mi <= sidePixels; mi++) { int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1); int pixel = mSource[mindex]; rt += (float)((pixel & 0x00ff0000) >> 16) / mBlurWidth; gt += (float)((pixel & 0x0000ff00) >> 8) / mBlurWidth; bt += (float)((pixel & 0x000000ff) >> 0) / mBlurWidth; } // Reassemble destination pixel. int dpixel = (0xff000000 ) | (((int)rt) << 16) | (((int)gt) << 8) | (((int)bt) << 0); mDestination[index] = dpixel; } } ...
现在你要实现抽象的compute()
方法,该方法可以直接执行模糊操作,也可以将其拆分为两个较小的任务。一个简单的数组长度阈值有助于确定是执行工作还是拆分任务。
protected static int sThreshold = 100000; protected void compute() { if (mLength < sThreshold) { computeDirectly(); return; } int split = mLength / 2; invokeAll(new ForkBlur(mSource, mStart, split, mDestination), new ForkBlur(mSource, mStart + split, mLength - split, mDestination)); }
如果前面的方法在RecursiveAction
类的子类中,那么设置任务在ForkJoinPool
中运行就很简单,包括以下步骤:
- 创建一个代表所有要完成工作的任务。
// source image pixels are in src // destination image pixels are in dst ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
- 创建将运行任务的
ForkJoinPool
。
ForkJoinPool pool = new ForkJoinPool();
- 运行任务。
pool.invoke(fb);
要查看完整的源代码,包括一些额外的代码来创建目标图像文件,请参见ForkBlur
示例。
标准实现
除了在多处理器系统上并行执行任务的自定义算法(例如前一节中的ForkBlur.java
示例)中使用分支/合并框架之外,Java SE 中还有一些通用功能已经使用分支/合并框架实现。其中一种实现是在 Java SE 8 中引入的,被java.util.Arrays
类用于其parallelSort()
方法。这些方法类似于sort()
,但通过分支/合并框架利用并发性能。在多处理器系统上运行时,大型数组的并行排序比顺序排序更快。然而,这些方法如何利用分支/合并框架超出了 Java 教程的范围。有关此信息,请参阅 Java API 文档。
另一个实现分支/合并框架的方法是使用java.util.streams
包中的方法,该包是Project Lambda的一部分,计划在 Java SE 8 发布中使用。更多信息,请参阅 Lambda 表达式部分。
并发集合
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/collections.html
java.util.concurrent
包包含了许多对 Java 集合框架的补充。这些最容易通过提供的集合接口进行分类:
BlockingQueue
定义了一个先进先出的数据结构,当尝试向满队列添加或从空队列检索时会阻塞或超时。ConcurrentMap
是java.util.Map
的子接口,定义了有用的原子操作。这些操作仅在键存在时移除或替换键值对,或仅在键不存在时添加键值对。使这些操作原子化有助于避免同步。ConcurrentMap
的标准通用实现是ConcurrentHashMap
,它是HashMap
的并发模拟。ConcurrentNavigableMap
是ConcurrentMap
的子接口,支持近似匹配。ConcurrentNavigableMap
的标准通用实现是ConcurrentSkipListMap
,它是TreeMap
的并发模拟。
所有这些集合都有助于避免内存一致性错误,通过定义将一个对象添加到集合的操作与随后访问或移除该对象的操作之间的 happens-before 关系。
原子变量
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html
java.util.concurrent.atomic
包定义了支持单个变量上原子操作的类。所有类都有类似于对volatile
变量进行读取和写入的get
和set
方法。也就是说,set
与同一变量上的任何后续get
之间存在 happens-before 关系。原子compareAndSet
方法也具有这些内存一致性特性,整数原子变量适用的简单原子算术方法也是如此。
要了解这个包可能如何使用,让我们回到最初用来演示线程干扰的Counter
类:
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
使Counter
免受线程干扰的一种方法是使其方法同步,就像SynchronizedCounter
中那样:
class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
对于这个简单的类,同步是一个可接受的解决方案。但对于一个更复杂的类,我们可能希望避免不必要同步的活跃度影响。用AtomicInteger
替换int
字段可以让我们在不使用同步的情况下防止线程干扰,就像AtomicCounter
中那样:
import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); } }
并发随机数
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/threadlocalrandom.html
在 JDK 7 中,java.util.concurrent
包含一个方便的类,ThreadLocalRandom
,适用于期望从多个线程或ForkJoinTask
中使用随机数的应用程序。
对于并发访问,使用ThreadLocalRandom
而不是Math.random()
会减少争用,最终提高性能。
你只需调用ThreadLocalRandom.current()
,然后调用其中的方法来获取一个随机数。以下是一个示例:
int r = ThreadLocalRandom.current() .nextInt(4, 77);
进一步阅读
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/further.html
- Concurrent Programming in Java: Design Principles and Pattern (2nd Edition) 作者:Doug Lea。这是一部由领先专家撰写的全面作品,他也是 Java 平台并发框架的架构师。
- Java Concurrency in Practice 作者:Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, 和 Doug Lea。一本旨在让初学者易于理解的实用指南。
- Effective Java Programming Language Guide (2nd Edition) 作者:Joshua Bloch。虽然这是一本通用的编程指南,但其中关于线程的章节包含了并发编程的基本“最佳实践”。
- Concurrency: State Models & Java Programs (2nd Edition) 作者:Jeff Magee 和 Jeff Kramer。通过建模和实际示例相结合,介绍并发编程。
- Java Concurrent Animated: 展示并发特性使用的动画。
问题和练习:并发
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/QandE/questions.html
问题
- 你能将
Thread
对象传递给Executor.execute
吗?这样的调用有意义吗?
练习
- 编译并运行
BadThreads.java
:
public class BadThreads { static String message; private static class CorrectorThread extends Thread { public void run() { try { sleep(1000); } catch (InterruptedException e) {} // Key statement 1: message = "Mares do eat oats."; } } public static void main(String args[]) throws InterruptedException { (new CorrectorThread()).start(); message = "Mares do not eat oats."; Thread.sleep(2000); // Key statement 2: System.out.println(message); } }
- 应用程序应该打印出“Mares do eat oats.” 这是一定会发生的吗?如果不是,为什么?改变两次
Sleep
调用的参数会有帮助吗?如何确保所有对message
的更改在主线程中可见? - 修改 Guarded Blocks 中的生产者-消费者示例,使用标准库类代替
Drop
类。
检查你的答案。
课程:平台环境
原文:
docs.oracle.com/javase/tutorial/essential/environment/index.html
应用程序在平台环境中运行,由底层操作系统、Java 虚拟机、类库和应用程序启动时提供的各种配置数据定义。本课程描述了应用程序用于检查和配置其平台环境的一些 API。本课程包括三个部分:
- 配置工具描述了用于访问应用程序部署时提供的配置数据或应用程序用户提供的 API。
- 系统工具描述了在
System
和Runtime
类中定义的各种 API。 - 路径和类路径描述了用于配置 JDK 开发工具和其他应用程序的环境变量。
配置实用程序
原文:
docs.oracle.com/javase/tutorial/essential/environment/config.html
这一部分描述了一些配置实用程序,帮助应用程序访问其启动上下文。
属性
原文:
docs.oracle.com/javase/tutorial/essential/environment/properties.html
属性是作为键/值对管理的配置值。在每对中,键和值都是String
值。键标识并用于检索值,就像使用变量名检索变量的值一样。例如,一个能够下载文件的应用程序可能使用名为“download.lastDirectory”的属性来跟踪用于最后下载的目录。
要管理属性,请创建java.util.Properties
的实例。此类提供以下方法:
- 从流中加载键/值对到
Properties
对象中, - 通过其键检索值,
- 列出键及其值,
- 枚举键,
- 将属性保存到流中。
有关流的介绍,请参阅输入/输出流中的基本 I/O 课程。
Properties
扩展了java.util.Hashtable
。从Hashtable
继承的一些方法支持以下操作:
- 测试特定键或值是否在
Properties
对象中, - 获取当前键/值对的数量,
- 删除键及其值,
- 向
Properties
列表添加键/值对, - 枚举值或键,
- 通过键检索值,
- 查看
Properties
对象是否为空。
安全注意事项: 访问属性需经当前安全管理器批准。本节中的示例代码段假定为独立应用程序,这些应用程序默认没有安全管理器。同样的代码在 applet 中可能无法正常工作,具体取决于运行的浏览器。请参阅 Applet 的功能和限制中的 Java Applets 课程,了解 applet 的安全限制信息。
System
类维护一个定义当前工作环境配置的Properties
对象。有关这些属性的更多信息,请参阅系统属性。本节的其余部分将解释如何使用属性来管理应用程序配置。
应用程序生命周期中的属性
以下图示说明了典型应用程序如何在执行过程中使用Properties
对象管理其配置数据。
启动中
第一个三个框中给出的操作发生在应用程序启动时。首先,应用程序将默认属性从一个众所周知的位置加载到Properties
对象中。通常,默认属性存储在磁盘上的文件中,与应用程序的.class
和其他资源文件一起。
接下来,应用程序创建另一个Properties
对象,并加载上次运行应用程序时保存的属性。许多应用程序按用户为单位存储属性,因此此步骤中加载的属性通常位于应用程序在用户主目录中维护的特定目录中的特定文件中。最后,应用程序使用默认和记忆的属性来初始化自身。
关键在于一致性。应用程序必须始终将属性加载和保存到相同位置,以便下次执行时能够找到它们。运行中
在应用程序执行期间,用户可能会更改一些设置,也许在首选项窗口中,并且Properties
对象将更新以反映这些更改。如果要记住用户更改以供将来的会话使用,则必须保存这些更改。退出
退出时,应用程序将属性保存到其众所周知的位置,以便在下次启动应用程序时再次加载。
设置Properties
对象
以下 Java 代码执行了前一节描述的前两个步骤:加载默认属性和加载记住的属性:
. . . // create and load default properties Properties defaultProps = new Properties(); FileInputStream in = new FileInputStream("defaultProperties"); defaultProps.load(in); in.close(); // create application properties with default Properties applicationProps = new Properties(defaultProps); // now load properties // from last invocation in = new FileInputStream("appProperties"); applicationProps.load(in); in.close(); . . .
Java 中文官方教程 2022 版(十)(2)https://developer.aliyun.com/article/1486348