Java7-并发秘籍-全-
Java7 并发秘籍(全)
原文:
zh.annas-archive.org/md5/F8E5EF0E7E4290BD7C1CC58C96A57EB0译者:飞龙
前言
当您使用计算机时,可以同时进行多项任务。您可以在编辑器中编辑文档并听音乐,同时阅读电子邮件。这是因为您的操作系统允许任务并发。并发编程涉及平台提供的元素和机制,使多个任务或程序同时运行并相互通信以交换数据或相互同步。Java 是一个并发平台,并提供了许多类来在 Java 程序中执行并发任务。随着每个版本的更新,Java 增加了为程序员提供的功能,以便更轻松地开发并发程序。本书涵盖了 Java 并发 API 第 7 版中包含的最重要和有用的机制,因此您将能够直接在应用程序中使用它们,具体包括:
-
基本线程管理
-
线程同步机制
-
使用执行器进行线程创建和管理委托
-
Fork/Join 框架以增强应用程序的性能
-
并发程序的数据结构
-
调整一些并发类的默认行为以满足您的需求
-
测试 Java 并发应用程序
本书涵盖的内容
第一章 线程管理 将教读者如何对线程进行基本操作。通过基本示例,解释了线程的创建、执行和状态管理。
第二章 基本线程同步 将教读者如何使用 Java 的低级机制来同步代码。详细解释了锁和 synchronized 关键字。
第三章 线程同步工具 将教读者如何使用 Java 的高级工具来管理 Java 中线程之间的同步。其中包括如何使用新的 Java 7 Phaser 类来同步分阶段的任务。
第四章 线程执行器 将教读者将线程管理委托给执行器。它们允许运行、管理和获取并发任务的结果。
第五章 Fork/Join 框架 将教读者如何使用新的 Java 7 Fork/Join 框架。这是一种特殊类型的执行器,旨在使用分而治之的技术将任务分解为更小的任务。
第六章 并发集合 将教读者如何使用 Java 语言提供的一些并发数据结构。这些数据结构必须在并发程序中使用,以避免在其实现中使用同步代码块。
第七章 自定义并发类 将教读者如何调整 Java 并发 API 中一些最有用的类以满足其需求。
第八章 测试并发应用程序 将教读者如何获取有关 Java 7 并发 API 中一些最有用结构状态的信息。读者还将学习如何使用一些免费工具来调试并发应用程序,例如 Eclipse、NetBeans IDE 或 FindBugs 应用程序,以检测其应用程序可能存在的错误。
第九章 附加信息 不包含在书中,但可以从以下链接免费下载:www.packtpub.com/sites/default/files/downloads/Additional
本章将教读者同步、执行器和 Fork/Join 框架的概念,以及并发数据结构和监视并发对象的内容,这些内容在各自的章节中没有包含。
附录,并发编程设计不在书中,但可以从以下链接免费下载:www.packtpub.com/sites/default/files/downloads/Concurrent
本附录将教读者一些每个程序员在开发并发应用程序时应考虑的技巧。
您需要为本书做好准备的内容
要跟进本书,您需要对 Java 编程语言有基本了解。您应该知道如何使用 IDE,比如 Eclipse 或 NetBeans,但这不是必要的先决条件。
本书适合谁
如果您是一名 Java 开发人员,希望进一步了解并发编程和多线程的知识,以及发现 Java 7 的新并发特性,那么Java 7 并发烹饪书就是为您准备的。您应该已经熟悉一般的 Java 开发实践,并且对线程有基本的了解会是一个优势。
约定
在本书中,您将找到一些不同类型信息的文本样式。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码词显示如下:“扩展Thread类并重写run()方法”。
代码块设置如下:
public Calculator(int number) {
this.number=number;
}
新术语和重要词汇以粗体显示。例如,屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的方式出现在文本中:“在菜单栏的文件菜单中选择新建项目选项创建新项目”。
注意
警告或重要说明会以这样的方式出现在方框中。
提示
提示和技巧会以这样的方式出现。
读者反馈
我们始终欢迎读者的反馈。让我们知道您对本书的看法——您喜欢或不喜欢的地方。读者的反馈对我们开发您真正受益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在消息主题中提及书名。
如果您对某个专题有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的自豪所有者,我们有一些事情可以帮助您充分利用您的购买。
下载示例代码
您可以从您在www.PacktPub.com购买的所有 Packt 书籍中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.PacktPub.com/support并注册,以便将文件直接发送到您的电子邮件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误是难免的。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将不胜感激地希望您向我们报告。通过这样做,您可以帮助其他读者避免挫败,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/support报告,选择您的书,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站上,或者添加到该标题的勘误列表中的任何现有勘误下的勘误部分。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有的勘误。
盗版
互联网上侵犯版权材料的盗版问题是跨媒体持续存在的问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供地址或网站名称,以便我们采取补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您帮助保护我们的作者,以及我们为您提供有价值内容的能力。
问题
如果您在阅读本书的过程中遇到任何问题,请通过<questions@packtpub.com>与我们联系,我们将尽力解决。
第一章:线程管理
在本章中,我们将涵盖:
-
创建和运行线程
-
获取和设置线程信息
-
中断线程
-
控制线程的中断
-
休眠和恢复线程
-
等待线程的最终化
-
创建和运行守护线程
-
在线程中处理不受控制的异常
-
使用本地线程变量
-
将线程分组
-
在一组线程中处理不受控制的异常
-
通过工厂创建线程
介绍
在计算机世界中,当我们谈论并发时,我们谈论的是在计算机中同时运行的一系列任务。如果计算机有多个处理器或多核处理器,这种同时性可以是真实的,或者如果计算机只有一个核心处理器,这种同时性可以是表面的。
所有现代操作系统都允许执行并发任务。您可以在读取电子邮件的同时听音乐和在网页上阅读新闻。我们可以说这种并发是进程级的并发。但在一个进程内部,我们也可以有各种同时进行的任务。在进程内部运行的并发任务称为线程。
与并发相关的另一个概念是并行。并发概念有不同的定义和关系。一些作者在你在单核处理器上使用多个线程执行应用程序时谈论并发,因此同时你可以看到你的程序执行是表面的。此外,当您在多核处理器或具有多个处理器的计算机上使用多个线程执行应用程序时,您也可以谈论并行。其他作者在应用程序的线程在没有预定义顺序的情况下执行时谈论并发,并在使用各种线程简化问题解决方案时谈论并行,其中所有这些线程都以有序的方式执行。
本章介绍了一些示例,展示了如何使用 Java 7 API 执行线程的基本操作。您将看到如何在 Java 程序中创建和运行线程,如何控制它们的执行,以及如何将一些线程分组以将它们作为一个单元进行操作。
创建和运行线程
在这个示例中,我们将学习如何在 Java 应用程序中创建和运行线程。与 Java 语言中的每个元素一样,线程都是对象。在 Java 中创建线程有两种方式:
-
扩展
Thread类并重写run()方法 -
构建一个实现
Runnable接口的类,然后创建一个Thread类的对象,将Runnable对象作为参数传递
在这个示例中,我们将使用第二种方法创建一个简单的程序,创建并运行 10 个线程。每个线程计算并打印 1 到 10 之间的数字的乘法表。
准备工作
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Calculator的类,实现Runnable接口。
public class Calculator implements Runnable {
- 声明一个名为
number的private整数属性,并实现初始化其值的类的构造函数。
private int number;
public Calculator(int number) {
this.number=number;
}
- 实现
run()方法。这个方法将执行我们正在创建的线程的指令,因此这个方法将计算数字的乘法表。
@Override
public void run() {
for (int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread.currentThread().getName(),number,i,i*number);
}
}
- 现在,实现应用程序的主类。创建一个名为
Main的类,其中包含main()方法。
public class Main {
public static void main(String[] args) {
- 在
main()方法中,创建一个有 10 次迭代的for循环。在循环内,创建一个Calculator类的对象,一个Thread类的对象,将Calculator对象作为参数传递,并调用线程对象的start()方法。
for (int i=1; i<=10; i++){
Calculator calculator=new Calculator(i);
Thread thread=new Thread(calculator);
thread.start();
}
- 运行程序,看看不同的线程如何并行工作。
它是如何工作的...
程序的输出部分如下截图所示。我们可以看到,我们创建的所有线程都并行运行以完成它们的工作,如下截图所示:

每个 Java 程序至少有一个执行线程。运行程序时,JVM 会运行调用程序的main()方法的执行线程。
当我们调用Thread对象的start()方法时,我们正在创建另一个执行线程。我们的程序将有多少执行线程,就会调用多少次start()方法。
Java 程序在所有线程完成时结束(更具体地说,当所有非守护线程完成时)。如果初始线程(执行main()方法的线程)结束,其余线程将继续执行直到完成。如果其中一个线程使用System.exit()指令来结束程序的执行,所有线程都将结束执行。
创建Thread类的对象并不会创建新的执行线程。调用实现Runnable接口的类的run()方法也不会创建新的执行线程。只有调用start()方法才会创建新的执行线程。
还有更多...
正如我们在本示例的介绍中提到的,还有另一种创建新执行线程的方法。您可以实现一个继承Thread类并重写这个类的run()方法的类。然后,您可以创建这个类的对象并调用start()方法来创建一个新的执行线程。
另请参阅
- 在第一章的通过工厂创建线程示例中,线程管理
获取和设置线程信息
Thread类保存了一些信息属性,可以帮助我们识别线程、了解其状态或控制其优先级。这些属性包括:
-
ID:此属性为每个
Thread存储一个唯一标识符。 -
名称:此属性存储
Thread的名称。 -
优先级:此属性存储
Thread对象的优先级。线程的优先级可以在 1 到 10 之间,其中 1 是最低优先级,10 是最高优先级。不建议更改线程的优先级,但如果需要,可以使用这个选项。 -
状态:此属性存储
Thread的状态。在 Java 中,Thread可以处于以下六种状态之一:new、runnable、blocked、waiting、time``waiting或terminated。
在本示例中,我们将开发一个程序,为 10 个线程设置名称和优先级,然后显示它们的状态信息,直到它们完成。这些线程将计算一个数字的乘法表。
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
Calculator的类,并指定它实现Runnable接口。
public class Calculator implements Runnable {
- 声明一个名为
number的int私有属性,并实现初始化该属性的类的构造函数。
private int number;
public Calculator(int number) {
this.number=number;
}
- 实现
run()方法。这个方法将执行我们正在创建的线程的指令,因此这个方法将计算并打印一个数字的乘法表。
@Override
public void run() {
for (int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread.currentThread().getName(),number,i,i*number);
}
}
- 现在,我们实现这个示例的主类。创建一个名为
Main的类,并实现main()方法。
public class Main {
public static void main(String[] args) {
- 创建一个包含 10 个
threads和 10 个Thread.State的数组,用于存储我们将要执行的线程及其状态。
Thread threads[]=new Thread[10];
Thread.State status[]=new Thread.State[10];
- 创建 10 个
Calculator类的对象,每个对象都初始化为不同的数字,并创建 10 个threads来运行它们。将其中五个的优先级设置为最大值,将其余的优先级设置为最小值。
for (int i=0; i<10; i++){
threads[i]=new Thread(new Calculator(i));
if ((i%2)==0){
threads[i].setPriority(Thread.MAX_PRIORITY);
} else {
threads[i].setPriority(Thread.MIN_PRIORITY);
}
threads[i].setName("Thread "+i);
}
- 创建一个
PrintWriter对象来写入线程状态的文件。
try (FileWriter file = new FileWriter(".\\data\\log.txt");
PrintWriter pw = new PrintWriter(file);){
- 在这个文件上写下 10 个“线程”的状态。现在,它变成了
NEW。
for (int i=0; i<10; i++){
pw.println("Main : Status of Thread "+i+" : " + threads[i].getState());
status[i]=threads[i].getState();
}
- 开始执行这 10 个线程。
for (int i=0; i<10; i++){
threads[i].start();
}
- 直到这 10 个线程结束,我们将检查它们的状态。如果我们检测到线程状态的变化,我们就把它们写在文件中。
boolean finish=false;
while (!finish) {
for (int i=0; i<10; i++){
if (threads[i].getState()!=status[i]) {
writeThreadInfo(pw, threads[i],status[i]);
status[i]=threads[i].getState();
}
}
finish=true;
for (int i=0; i<10; i++){
finish=finish &&(threads[i].getState()==State.TERMINATED);
}
}
- 实现
writeThreadInfo()方法,该方法写入Thread的 ID、名称、优先级、旧状态和新状态。
private static void writeThreadInfo(PrintWriter pw, Thread thread, State state) {
pw.printf("Main : Id %d - %s\n",thread.getId(),thread.getName());
pw.printf("Main : Priority: %d\n",thread.getPriority());
pw.printf("Main : Old State: %s\n",state);
pw.printf("Main : New State: %s\n",thread.getState());
pw.printf("Main : ************************************\n");
}
- 运行示例并打开
log.txt文件,查看这 10 个线程的演变。
它是如何工作的...
下面的截图显示了该程序执行过程中log.txt文件的一些行。在这个文件中,我们可以看到优先级最高的线程在优先级最低的线程之前结束。我们还可以看到每个线程状态的演变。

在控制台显示的程序是线程计算的乘法表和文件log.txt中不同线程状态的演变。通过这种方式,你可以更好地看到线程的演变。
Thread类有属性来存储线程的所有信息。JVM 使用线程的优先级来选择在每个时刻使用 CPU 的线程,并根据每个线程的情况更新每个线程的状态。
如果你没有为线程指定名称,JVM 会自动分配一个格式为 Thread-XX 的名称,其中 XX 是一个数字。你不能修改线程的 ID 或状态。Thread类没有实现setId()和setStatus()方法来允许它们的修改。
还有更多...
在这个示例中,你学会了如何使用Thread对象访问信息属性。但你也可以从Runnable接口的实现中访问这些属性。你可以使用Thread类的静态方法currentThread()来访问运行Runnable对象的Thread对象。
你必须考虑到,如果你尝试设置一个不在 1 到 10 之间的优先级,setPriority()方法可能会抛出IllegalArgumentException异常。
另请参阅
- 中断线程在第一章中的线程管理中的示例
中断线程
一个具有多个执行线程的 Java 程序只有在所有线程的执行结束时才会结束(更具体地说,当所有非守护线程结束执行或其中一个线程使用System.exit()方法时)。有时,你需要结束一个线程,因为你想终止一个程序,或者程序的用户想取消Thread对象正在执行的任务。
Java 提供了中断机制来指示线程我们想要结束它。这种机制的一个特点是Thread必须检查它是否被中断,它可以决定是否响应最终化请求。Thread可以忽略它并继续执行。
在这个示例中,我们将开发一个程序,创建Thread,并在 5 秒后使用中断机制强制结束它。
准备就绪
本示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
PrimeGenerator的类,该类扩展了Thread类。
public class PrimeGenerator extends Thread{
- 重写
run()方法,包括一个将无限运行的循环。在这个循环中,我们将处理从 1 开始的连续数字。对于每个数字,我们将计算它是否是一个质数,如果是,我们将把它写入控制台。
@Override
public void run() {
long number=1L;
while (true) {
if (isPrime(number)) {
System.out.printf("Number %d is Prime",number);
}
- 处理完一个数字后,通过调用
isInterrupted()方法来检查线程是否被中断。如果这个方法返回true,我们就写一条消息并结束线程的执行。
if (isInterrupted()) {
System.out.printf("The Prime Generator has been Interrupted");
return;
}
number++;
}
}
- 实现
isPrime()方法。它返回一个boolean值,指示接收的参数是否为质数(true)还是不是(false)。
private boolean isPrime(long number) {
if (number <=2) {
return true;
}
for (long i=2; i<number; i++){
if ((number % i)==0) {
return false;
}
}
return true;
}
- 现在,通过实现一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建并启动
PrimeGenerator类的对象。
Thread task=new PrimeGenerator();
task.start();
- 等待 5 秒并中断
PrimeGenerator线程。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.interrupt();
- 运行示例并查看结果。
它是如何工作的...
以下屏幕截图显示了上一个示例的执行结果。我们可以看到PrimeGenerator线程在检测到被中断时写入消息并结束其执行。请参考以下屏幕截图:

Thread类有一个属性,用于存储一个boolean值,指示线程是否已被中断。当您调用线程的interrupt()方法时,您将该属性设置为true。isInterrupted()方法只返回该属性的值。
还有更多...
Thread类还有另一个方法来检查Thread是否已被中断。它是静态方法interrupted(),用于检查当前执行线程是否已被中断。
注意
isInterrupted()和interrupted()方法之间有一个重要的区别。第一个不会改变interrupted属性的值,但第二个会将其设置为false。由于interrupted()方法是一个静态方法,建议使用isInterrupted()方法。
如我之前提到的,Thread可以忽略其中断,但这不是预期的行为。
控制线程的中断
在上一个示例中,您学习了如何中断线程的执行以及如何控制Thread对象中的中断。在上一个示例中展示的机制可以用于可以被中断的简单线程。但是,如果线程实现了分为一些方法的复杂算法,或者它具有具有递归调用的方法,我们可以使用更好的机制来控制线程的中断。Java 为此提供了InterruptedException异常。当检测到线程中断时,您可以抛出此异常并在run()方法中捕获它。
在本示例中,我们将实现一个Thread,它在文件夹及其所有子文件夹中查找具有确定名称的文件,以展示如何使用InterruptedException异常来控制线程的中断。
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
FileSearch的类,并指定它实现Runnable接口。
public class FileSearch implements Runnable {
- 声明两个
private属性,一个用于要搜索的文件名,另一个用于初始文件夹。实现类的构造函数,初始化这些属性。
private String initPath;
private String fileName;
public FileSearch(String initPath, String fileName) {
this.initPath = initPath;
this.fileName = fileName;
}
- 实现
FileSearch类的run()方法。它检查属性fileName是否为目录,如果是,则调用processDirectory()方法。该方法可能会抛出InterruptedException异常,因此我们必须捕获它们。
@Override
public void run() {
File file = new File(initPath);
if (file.isDirectory()) {
try {
directoryProcess(file);
} catch (InterruptedException e) {
System.out.printf("%s: The search has been interrupted",Thread.currentThread().getName());
}
}
}
- 实现
directoryProcess()方法。该方法将获取文件夹中的文件和子文件夹并对它们进行处理。对于每个目录,该方法将使用递归调用并将目录作为参数传递。对于每个文件,该方法将调用fileProcess()方法。在处理所有文件和文件夹后,该方法检查Thread是否已被中断,如果是,则抛出InterruptedException异常。
private void directoryProcess(File file) throws InterruptedException {
File list[] = file.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
directoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
- 实现
processFile()方法。此方法将比较其正在处理的文件的名称与我们正在搜索的名称。如果名称相等,我们将在控制台中写入一条消息。在此比较之后,Thread将检查它是否已被中断,如果是,则抛出InterruptedException异常。
private void fileProcess(File file) throws InterruptedException {
if (file.getName().equals(fileName)) {
System.out.printf("%s : %s\n",Thread.currentThread().getName() ,file.getAbsolutePath());
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
- 现在,让我们实现示例的主类。实现一个名为
Main的类,其中包含main()方法。
public class Main {
public static void main(String[] args) {
- 创建并初始化
FileSearch类的对象和Thread以执行其任务。然后,开始执行Thread。
FileSearch searcher=new FileSearch("C:\\","autoexec.bat");
Thread thread=new Thread(searcher);
thread.start();
- 等待 10 秒并中断
Thread。
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
- 运行示例并查看结果。
工作原理...
以下屏幕截图显示了此示例执行的结果。您可以看到FileSearch对象在检测到已被中断时结束其执行。请参考以下屏幕截图:

在此示例中,我们使用 Java 异常来控制Thread的中断。运行示例时,程序开始通过检查文件夹来检查它们是否有文件。例如,如果您进入文件夹\b\c\d,程序将对processDirectory()方法进行三次递归调用。当它检测到已被中断时,它会抛出InterruptedException异常,并在run()方法中继续执行,无论已经进行了多少次递归调用。
还有更多...
InterruptedException异常由一些与并发 API 相关的 Java 方法抛出,例如sleep()。
另请参阅
- 第一章中的中断线程示例,线程管理
休眠和恢复线程
有时,您可能会对在一定时间内中断Thread的执行感兴趣。例如,程序中的一个线程每分钟检查一次传感器状态。其余时间,线程什么也不做。在此期间,线程不使用计算机的任何资源。此时间结束后,当 JVM 选择执行时,线程将准备好继续执行。您可以使用Thread类的sleep()方法来实现这一目的。该方法接收一个整数作为参数,表示线程暂停执行的毫秒数。当休眠时间结束时,线程在sleep()方法调用后的指令中继续执行,当 JVM 分配给它们 CPU 时间时。
另一种可能性是使用TimeUnit枚举的元素的sleep()方法。此方法使用Thread类的sleep()方法将当前线程置于休眠状态,但它以表示的单位接收参数,并将其转换为毫秒。
在本示例中,我们将开发一个程序,使用sleep()方法每秒写入实际日期。
准备就绪
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
FileClock的类,并指定它实现Runnable接口。
public class FileClock implements Runnable {
- 实现
run()方法。
@Override
public void run() {
- 编写一个具有 10 次迭代的循环。在每次迭代中,创建一个
Date对象,将其写入文件,并调用TimeUnit类的SECONDS属性的sleep()方法,以暂停线程的执行一秒钟。使用此值,线程将大约休眠一秒钟。由于sleep()方法可能会抛出InterruptedException异常,因此我们必须包含捕获它的代码。在线程被中断时,包括释放或关闭线程正在使用的资源的代码是一个良好的实践。
for (int i = 0; i < 10; i++) {
System.out.printf("%s\n", new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.printf("The FileClock has been interrupted");
}
}
}
- 我们已经实现了线程。现在,让我们实现示例的主类。创建一个名为
FileMain的类,其中包含main()方法。
public class FileMain {
public static void main(String[] args) {
- 创建一个
FileClock类的对象和一个线程来执行它。然后,开始执行Thread。
FileClock clock=new FileClock();
Thread thread=new Thread(clock);
thread.start();
- 在主
Thread中调用TimeUnit类的 SECONDS 属性的sleep()方法,等待 5 秒。
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
};
- 中断
FileClock线程。
thread.interrupt();
- 运行这个例子并查看结果。
它是如何工作的...
当你运行这个例子时,你可以看到程序每秒写入一个Date对象,然后显示FileClock线程已被中断的消息。
当你调用sleep()方法时,Thread离开 CPU 并停止执行一段时间。在这段时间内,它不会消耗 CPU 时间,所以 CPU 可以执行其他任务。
当Thread正在睡眠并被中断时,该方法会立即抛出InterruptedException异常,而不会等到睡眠时间结束。
还有更多...
Java 并发 API 还有另一个方法,可以让Thread对象离开 CPU。这就是yield()方法,它告诉 JVMThread对象可以离开 CPU 去做其他任务。JVM 不能保证会遵守这个请求。通常,它只用于调试目的。
等待线程的最终化
在某些情况下,我们需要等待线程的最终化。例如,我们可能有一个程序,在继续执行之前需要开始初始化所需的资源。我们可以将初始化任务作为线程运行,并在继续程序的其余部分之前等待其最终化。
为此,我们可以使用Thread类的join()方法。当我们使用一个线程对象调用这个方法时,它会暂停调用线程的执行,直到被调用的对象完成执行。
在这个示例中,我们将学习如何在初始化示例中使用这个方法。
准备工作
这个示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
DataSourcesLoader的类,并指定它实现Runnable接口。
public class DataSourcesLoader implements Runnable {
- 实现
run()方法。它写入一个消息表示它开始执行,睡眠 4 秒,然后写入另一个消息表示它结束执行。
@Override
public void run() {
System.out.printf("Beginning data sources loading: %s\n",new Date());
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Data sources loading has finished: %s\n",new Date());
}
-
创建一个名为
NetworkConnectionsLoader的类,并指定它实现Runnable接口。实现run()方法。它将与DataSourcesLoader类的run()方法相同,但这将睡眠 6 秒。 -
现在,创建一个包含
main()方法的Main类。
public class Main {
public static void main(String[] args) {
- 创建一个
DataSourcesLoader类的对象和一个Thread来运行它。
DataSourcesLoader dsLoader = new DataSourcesLoader();
Thread thread1 = new Thread(dsLoader,"DataSourceThread");
- 创建一个
NetworkConnectionsLoader类的对象和一个Thread来运行它。
NetworkConnectionsLoader ncLoader = new NetworkConnectionsLoader();
Thread thread2 = new Thread(ncLoader,"NetworkConnectionLoader");
- 调用两个
Thread对象的start()方法。
thread1.start();
thread2.start();
- 等待使用
join()方法来完成两个线程的最终化。这个方法可能会抛出InterruptedException异常,所以我们必须包含捕获它的代码。
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写一个消息表示程序结束。
System.out.printf("Main: Configuration has been loaded: %s\n",new Date());
- 运行程序并查看结果。
它是如何工作的...
当你运行这个程序时,你可以看到两个Thread对象开始执行。首先,DataSourcesLoader线程完成执行。然后,NetworkConnectionsLoader类完成执行,此时,主Thread对象继续执行并写入最终消息。
还有更多...
Java 提供了join()方法的另外两种形式:
-
join (long milliseconds)
-
join (long milliseconds, long nanos)
在join()方法的第一个版本中,调用线程不是无限期地等待被调用的线程的最终化,而是等待方法参数指定的毫秒数。例如,如果对象thread1有代码thread2.join(1000),线程thread1会暂停执行,直到以下两种情况之一为真:
-
thread2完成了它的执行 -
已经过去了 1000 毫秒
当这两个条件中的一个为真时,join()方法返回。
join()方法的第二个版本与第一个版本类似,但接收毫秒数和纳秒数作为参数。
创建和运行守护线程
Java 有一种特殊类型的线程称为守护线程。这种类型的线程具有非常低的优先级,通常只有在程序中没有其他线程运行时才会执行。当守护线程是程序中唯一运行的线程时,JVM 会结束程序并完成这些线程。
具有这些特性,守护线程通常用作运行在同一程序中的普通(也称为用户)线程的服务提供者。它们通常有一个无限循环,等待服务请求或执行线程的任务。它们不能执行重要的工作,因为我们不知道它们何时会有 CPU 时间,并且如果没有其他线程运行,它们随时可以结束。这种类型线程的典型例子是 Java 垃圾收集器。
在这个示例中,我们将学习如何创建一个守护线程,开发一个包含两个线程的示例;一个用户线程在队列中写入事件,一个守护线程清理队列,删除超过 10 秒前生成的事件。
准备工作
这个示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
-
创建
Event类。这个类只存储我们的程序将使用的事件的信息。声明两个私有属性,一个叫做date,类型为java.util.Date,另一个叫做event,类型为String。生成方法来写入和读取它们的值。 -
创建
WriterTask类并指定它实现Runnable接口。
public class WriterTask implements Runnable {
- 声明存储事件的队列并实现类的构造函数,初始化这个队列。
private Deque<Event> deque;
public WriterTask (Deque<Event> deque){
this.deque=deque;
}
- 实现这个任务的
run()方法。这个方法将有一个循环,循环 100 次。在每次迭代中,我们创建一个新的Event,将其保存在队列中,并休眠一秒。
@Override
public void run() {
for (int i=1; i<100; i++) {
Event event=new Event();
event.setDate(new Date());
event.setEvent(String.format("The thread %s has generated an event",Thread.currentThread().getId()));
deque.addFirst(event);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 创建
CleanerTask类并指定它扩展Thread类。
public class CleanerTask extends Thread {
- 声明存储事件的队列并实现类的构造函数,初始化这个队列。在构造函数中,使用
setDaemon()方法将这个Thread标记为守护线程。
private Deque<Event> deque;
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
setDaemon(true);
}
- 实现
run()方法。它有一个无限循环,获取实际日期并调用clean()方法。
@Override
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
}
- 实现
clean()方法。获取最后一个事件,如果它是在 10 秒前创建的,就删除它并检查下一个事件。如果删除了一个事件,就写入事件的消息和队列的新大小,这样你就可以看到它的演变。
private void clean(Date date) {
long difference;
boolean delete;
if (deque.size()==0) {
return;
}
delete=false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n",e.getEvent());
deque.removeLast();
delete=true;
}
} while (difference > 10000);
if (delete){
System.out.printf("Cleaner: Size of the queue: %d\n",deque.size());
}
}
- 现在,实现主类。创建一个名为
Main的类,其中包含一个main()方法。
public class Main {
public static void main(String[] args) {
- 使用
Deque类创建队列来存储事件。
Deque<Event> deque=new ArrayDeque<Event>();
- 创建并启动三个
WriterTask线程和一个CleanerTask。
WriterTask writer=new WriterTask(deque);
for (int i=0; i<3; i++){
Thread thread=new Thread(writer);
thread.start();
}
CleanerTask cleaner=new CleanerTask(deque);
cleaner.start();
- 运行程序并查看结果。
工作原理...
如果分析程序的一次执行输出,可以看到队列开始增长,直到有 30 个事件,然后在执行结束之前,它的大小将在 27 和 30 个事件之间变化。
程序以三个WriterTask线程开始。每个Thread写入一个事件并休眠一秒。在第一个 10 秒之后,我们在队列中有 30 个线程。在这 10 秒内,CleanerTasks一直在执行,而三个WriterTask线程在休眠,但它没有删除任何事件,因为它们都是在不到 10 秒前生成的。在执行的其余时间里,CleanerTask每秒删除三个事件,而三个WriterTask线程写入另外三个事件,所以队列的大小在 27 和 30 个事件之间变化。
您可以调整WriterTask线程睡眠的时间。如果使用较小的值,您会发现CleanerTask的 CPU 时间较少,并且队列的大小会增加,因为CleanerTask不会删除任何事件。
还有更多...
在调用start()方法之前,您只能调用setDaemon()方法。一旦线程正在运行,就无法修改其守护进程状态。
您可以使用isDaemon()方法来检查线程是否是守护线程(方法返回true)还是用户线程(方法返回`false)。
处理线程中的未受控异常
Java 中有两种异常:
-
已检查的异常:这些异常必须在方法的
throws子句中指定或在其中捕获。例如,IOException或ClassNotFoundException。 -
未检查的异常:这些异常不必指定或捕获。例如,
NumberFormatException。
当在Thread对象的run()方法中抛出已检查的异常时,我们必须捕获和处理它们,因为run()方法不接受throws子句。当在Thread对象的run()方法中抛出未检查的异常时,默认行为是在控制台中写入堆栈跟踪并退出程序。
幸运的是,Java 为我们提供了一种机制来捕获和处理Thread对象中抛出的未检查异常,以避免程序结束。
在这个示例中,我们将使用一个示例来学习这个机制。
准备工作
这个示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 首先,我们必须实现一个类来处理未检查的异常。这个类必须实现
UncaughtExceptionHandler接口,并实现该接口中声明的uncaughtException()方法。在我们的情况下,将这个类命名为ExceptionHandler,并使该方法写入有关抛出异常的Exception和Thread的信息。以下是代码:
public class ExceptionHandler implements UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n",t.getId());
System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
- 现在,实现一个抛出未检查异常的类。将这个类命名为
Task,指定它实现Runnable接口,实现run()方法,并强制异常,例如,尝试将string值转换为int值。
public class Task implements Runnable {
@Override
public void run() {
int numero=Integer.parseInt("TTT");
}
}
- 现在,实现示例的主类。使用
main()方法实现一个名为Main的类。
public class Main {
public static void main(String[] args) {
- 创建一个
Task对象和Thread来运行它。使用setUncaughtExceptionHandler()方法设置未检查的异常处理程序,并开始执行Thread。
Task task=new Task();
Thread thread=new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
- 运行示例并查看结果。
它是如何工作的...
在下面的屏幕截图中,您可以看到示例执行的结果。异常被抛出并被处理程序捕获,该处理程序在控制台中写入有关抛出异常的Exception和Thread的信息。请参考以下屏幕截图:

当线程中抛出异常并且未被捕获(必须是未检查的异常)时,JVM 会检查线程是否有相应方法设置的未捕获异常处理程序。如果有,JVM 将使用Thread对象和Exception作为参数调用此方法。
如果线程没有未捕获的异常处理程序,JVM 会在控制台中打印堆栈跟踪并退出程序。
还有更多...
Thread类还有另一个与未捕获异常处理相关的方法。这是静态方法setDefaultUncaughtExceptionHandler(),它为应用程序中的所有Thread对象建立异常处理程序。
当在Thread中抛出未捕获的异常时,JVM 会寻找此异常的三个可能处理程序。
首先,查找Thread对象的未捕获异常处理程序,就像我们在这个示例中学到的那样。如果这个处理程序不存在,那么 JVM 将查找Thread对象的ThreadGroup的未捕获异常处理程序,就像在在一组线程中处理不受控制的异常示例中解释的那样。如果这个方法不存在,JVM 将查找默认的未捕获异常处理程序,就像我们在这个示例中学到的那样。
如果没有处理程序退出,JVM 会在控制台中写入异常的堆栈跟踪,并退出程序。
另请参阅
- 第一章中的在一组线程中处理不受控制的异常示例,线程管理
使用本地线程变量
并发应用程序中最关键的一个方面是共享数据。这在那些扩展了Thread类或实现了Runnable接口的对象中尤为重要。
如果你创建了一个实现了Runnable接口的类的对象,然后使用相同的Runnable对象启动各种Thread对象,所有线程都共享相同的属性。这意味着,如果你在一个线程中改变了一个属性,所有线程都会受到这个改变的影响。
有时,你可能会对一个属性感兴趣,这个属性不会在运行相同对象的所有线程之间共享。Java 并发 API 提供了一个称为线程本地变量的清晰机制,性能非常好。
在这个示例中,我们将开发一个程序,其中包含第一段中暴露的问题,以及使用线程本地变量机制解决这个问题的另一个程序。
准备工作
这个示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 首先,我们将实现一个程序,其中包含先前暴露的问题。创建一个名为
UnsafeTask的类,并指定它实现了Runnable接口。声明一个private``java.util.Date属性。
public class UnsafeTask implements Runnable{
private Date startDate;
- 实现
UnsafeTask对象的run()方法。这个方法将初始化startDate属性,将它的值写入控制台,休眠一段随机时间,然后再次写入startDate属性的值。
@Override
public void run() {
startDate=new Date();
System.out.printf("Starting Thread: %s : %s\n",Thread.currentThread().getId(),startDate);
try {
TimeUnit.SECONDS.sleep( (int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread.currentThread().getId(),startDate);
}
- 现在,让我们实现这个有问题的应用程序的主类。创建一个名为
Main的类,其中包含一个main()方法。这个方法将创建一个UnsafeTask类的对象,并使用该对象启动三个线程,在每个线程之间休眠 2 秒。
public class Core {
public static void main(String[] args) {
UnsafeTask task=new UnsafeTask();
for (int i=0; i<10; i++){
Thread thread=new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
-
在下面的截图中,你可以看到这个程序执行的结果。每个
Thread有不同的开始时间,但当它们完成时,所有的startDate属性都有相同的值。![如何做...]()
-
如前所述,我们将使用线程本地变量机制来解决这个问题。
-
创建一个名为
SafeTask的类,并指定它实现了Runnable接口。
public class SafeTask implements Runnable {
- 声明一个
ThreadLocal<Date>类的对象。这个对象将具有一个包含initialValue()方法的隐式实现。这个方法将返回实际的日期。
private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
protected Date initialValue(){
return new Date();
}
};
- 实现
run()方法。它具有与UnsafeClass的run()方法相同的功能,但它改变了访问startDate属性的方式。
@Override
public void run() {
System.out.printf("Starting Thread: %s : %s\n",Thread.currentThread().getId(),startDate.get());
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread.currentThread().getId(),startDate.get());
}
-
这个示例的主类与不安全的示例相同,只是改变了
Runnable类的名称。 -
运行示例并分析差异。
它是如何工作的...
在下面的截图中,你可以看到安全示例执行的结果。现在,三个Thread对象都有自己的startDate属性的值。参考下面的截图:

线程本地变量为使用这些变量的每个Thread存储一个属性的值。您可以使用get()方法读取该值,并使用set()方法更改该值。第一次访问线程本地变量的值时,如果它对于调用它的Thread对象没有值,则线程本地变量将调用initialValue()方法为该Thread分配一个值,并返回初始值。
还有更多...
线程本地类还提供了remove()方法,用于删除调用它的线程的线程本地变量中存储的值。
Java 并发 API 包括InheritableThreadLocal类,它提供了从线程创建的线程继承值的功能。如果线程 A 在线程本地变量中有一个值,并且它创建另一个线程 B,则线程 B 将在线程本地变量中具有与线程 A 相同的值。您可以重写childValue()方法,该方法用于初始化线程本地变量中子线程的值。它将父线程在线程本地变量中的值作为参数。
将线程分组
Java 并发 API 提供的一个有趣功能是能够对线程进行分组。这使我们能够将组中的线程视为单个单位,并提供对属于组的Thread对象的访问,以对它们进行操作。例如,如果有一些线程执行相同的任务,并且您想要控制它们,无论有多少线程正在运行,每个线程的状态都将通过单个调用中断所有线程。
Java 提供了ThreadGroup类来处理线程组。ThreadGroup对象可以由Thread对象和另一个ThreadGroup对象组成,生成线程的树形结构。
在这个示例中,我们将学习如何使用ThreadGroup对象开发一个简单的示例。我们将有 10 个线程在随机时间段内休眠(例如模拟搜索),当其中一个完成时,我们将中断其余的线程。
准备工作
这个示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
-
首先,创建一个名为
Result的类。它将存储首先完成的Thread的名称。声明一个名为name的private字符串属性和用于读取和设置该值的方法。 -
创建一个名为
SearchTask的类,并指定它实现Runnable接口。
public class SearchTask implements Runnable {
- 声明
Result类的private属性并实现该类的构造函数以初始化此属性。
private Result result;
public SearchTask(Result result) {
this.result=result;
}
- 实现
run()方法。它将调用doTask()方法并等待其完成或出现InterruptedException异常。该方法将写入消息以指示此Thread的开始、结束或中断。
@Override
public void run() {
String name=Thread.currentThread().getName();
System.out.printf("Thread %s: Start\n",name);
try {
doTask();
result.setName(name);
} catch (InterruptedException e) {
System.out.printf("Thread %s: Interrupted\n",name);
return;
}
System.out.printf("Thread %s: End\n",name);
}
- 实现
doTask()方法。它将创建一个Random对象来生成一个随机数,并调用sleep()方法来休眠该随机数的时间。
private void doTask() throws InterruptedException {
Random random=new Random((new Date()).getTime());
int value=(int)(random.nextDouble()*100);
System.out.printf("Thread %s: %d\n",Thread.currentThread().getName(),value);
TimeUnit.SECONDS.sleep(value);
}
- 现在,通过创建一个名为
Main的类并实现main()方法来创建示例的主类。
public class Main {
public static void main(String[] args) {
- 首先,创建一个
ThreadGroup对象并将其命名为Searcher。
ThreadGroup threadGroup = new ThreadGroup("Searcher");
- 然后,创建一个
SearchTask对象和一个Result对象。
Result result=new Result();
SearchTask searchTask=new SearchTask(result);
- 现在,使用
SearchTask对象创建 10 个Thread对象。当调用Thread类的构造函数时,将其作为ThreadGroup对象的第一个参数传递。
for (int i=0; i<5; i++) {
Thread thread=new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 使用
list()方法写入关于ThreadGroup对象的信息。
System.out.printf("Number of Threads: %d\n",threadGroup.activeCount());
System.out.printf("Information about the Thread Group\n");
threadGroup.list();
- 使用
activeCount()和enumerate()方法来了解有多少Thread对象与ThreadGroup对象相关联,并获取它们的列表。我们可以使用此方法来获取每个Thread的状态,例如。
Thread[] threads=new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
for (int i=0; i<threadGroup.activeCount(); i++) {
System.out.printf("Thread %s: %s\n",threads[i].getName(),threads[i].getState());
}
- 调用
waitFinish()方法。我们稍后将实现此方法。它将等待直到ThreadGroup对象的一个线程结束。
waitFinish(threadGroup);
- 使用
interrupt()方法中断组中其余的线程。
threadGroup.interrupt();
- 实现
waitFinish()方法。它将使用activeCount()方法来控制其中一个线程的结束。
private static void waitFinish(ThreadGroup threadGroup) {
while (threadGroup.activeCount()>9) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 运行示例并查看结果。
它是如何工作的...
在下面的屏幕截图中,您可以看到list()方法的输出以及当我们写入每个Thread对象的状态时生成的输出,如下面的屏幕截图所示:

ThreadGroup类存储Thread对象和与之关联的其他ThreadGroup对象,因此它可以访问它们所有的信息(例如状态)并对其所有成员执行操作(例如中断)。
还有更多...
ThreadGroup类有更多的方法。查看 API 文档以获得所有这些方法的完整解释。
在一组线程中处理不受控制的异常
在每种编程语言中,一个非常重要的方面是提供管理应用程序中错误情况的机制。Java 语言,就像几乎所有现代编程语言一样,实现了基于异常的机制来管理错误情况。它提供了许多类来表示不同的错误。当检测到错误情况时,Java 类会抛出这些异常。您也可以使用这些异常,或者实现自己的异常来管理类中产生的错误。
Java 还提供了一种机制来捕获和处理这些异常。有些异常必须使用方法的throws子句捕获或重新抛出。这些异常称为已检查异常。有些异常不必指定或捕获。这些是未检查的异常。
在这个示例中,控制线程的中断,你学会了如何使用一个通用方法来处理Thread对象中抛出的所有未捕获的异常。
另一种可能性是建立一个方法,捕获ThreadGroup类的任何Thread抛出的所有未捕获的异常。
在这个示例中,我们将学习使用一个例子来设置这个处理程序。
准备工作
这个示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE 如 NetBeans,打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 首先,我们必须通过创建一个名为
MyThreadGroup的类来扩展ThreadGroup类,该类从ThreadGroup类扩展。我们必须声明一个带有一个参数的构造函数,因为ThreadGroup类没有没有参数的构造函数。
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
- 重写
uncaughtException()方法。当ThreadGroup类的一个线程抛出异常时,将调用此方法。在这种情况下,此方法将在控制台中写入有关异常和抛出异常的Thread的信息,并中断ThreadGroup类中的其余线程。
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("The thread %s has thrown an Exception\n",t.getId());
e.printStackTrace(System.out);
System.out.printf("Terminating the rest of the Threads\n");
interrupt();
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 实现
run()方法。在这种情况下,我们将引发一个AritmethicException异常。为此,我们将在随机数之间除以 1000,直到随机生成器生成零并抛出异常。
@Override
public void run() {
int result;
Random random=new Random(Thread.currentThread().getId());
while (true) {
result=1000/((int)(random.nextDouble()*1000));
System.out.printf("%s : %f\n",Thread.currentThread().getId(),result);
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d : Interrupted\n",Thread.currentThread().getId());
return;
}
}
}
- 现在,我们将通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
MyThreadGroup类的对象。
MyThreadGroup threadGroup=new MyThreadGroup("MyThreadGroup");
- 创建一个
Task类的对象。
Task task=new Task();
- 创建两个带有这个
Task的Thread对象并启动它们。
for (int i=0; i<2; i++){
Thread t=new Thread(threadGroup,task);
t.start();
}
- 运行示例并查看结果。
它是如何工作的...
当你运行示例时,你会看到其中一个Thread对象抛出了异常,另一个被中断了。
当在Thread中抛出未捕获的异常时,JVM 会寻找这个异常的三个可能的处理程序。
首先,查找线程的未捕获异常处理程序,就像在处理线程中的不受控制的异常配方中所解释的那样。如果这个处理程序不存在,那么 JVM 会查找线程的ThreadGroup类的未捕获异常处理程序,就像我们在这个配方中学到的那样。如果这个方法不存在,JVM 会查找默认的未捕获异常处理程序,就像在处理线程中的不受控制的异常配方中所解释的那样。
如果没有处理程序退出,JVM 会在控制台中写入异常的堆栈跟踪,并退出程序。
另请参阅
- 第一章中的处理线程中的不受控制的异常配方,线程管理
通过工厂创建线程
工厂模式是面向对象编程世界中最常用的设计模式之一。它是一种创建模式,其目标是开发一个使命将是创建一个或多个类的其他对象的对象。然后,当我们想要创建其中一个类的对象时,我们使用工厂而不是使用new运算符。
有了这个工厂,我们可以集中创建对象,并获得一些优势:
-
很容易改变创建的对象的类或创建这些对象的方式。
-
很容易限制为有限资源创建对象。例如,我们只能有一个类型的n个对象。
-
很容易生成有关对象创建的统计数据。
Java 提供了一个接口,即ThreadFactory接口,用于实现Thread对象工厂。Java 并发 API 的一些高级工具使用线程工厂来创建线程。
在这个配方中,我们将学习如何实现ThreadFactory接口,以创建具有个性化名称的Thread对象,同时保存创建的Thread对象的统计数据。
准备工作
这个配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyThreadFactory的类,并指定它实现ThreadFactory接口。
public class MyThreadFactory implements ThreadFactory {
- 声明三个属性:一个名为
counter的整数,我们将用它来存储创建的Thread对象的数量,一个名为name的String,它是每个创建的Thread的基本名称,以及一个名为stats的String对象列表,用于保存有关创建的Thread对象的统计数据。我们还实现了初始化这些属性的类的构造函数。
private int counter;
private String name;
private List<String> stats;
public MyThreadFactory(String name){
counter=0;
this.name=name;
stats=new ArrayList<String>();
}
- 实现
newThread()方法。这个方法将接收一个Runnable接口,并为这个Runnable接口返回一个Thread对象。在我们的例子中,我们生成Thread对象的名称,创建新的Thread对象,并保存统计数据。
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread(r,name+"-Thread_"+counter);
counter++;
stats.add(String.format("Created thread %d with name %s on %s\n",t.getId(),t.getName(),new Date()));
return t;
}
- 实现
getStatistics()方法,返回包含所有创建的Thread对象的统计数据的String对象。
public String getStats(){
StringBuffer buffer=new StringBuffer();
Iterator<String> it=stats.iterator();
while (it.hasNext()) {
buffer.append(it.next());
buffer.append("\n");
}
return buffer.toString();
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。在这个例子中,这些任务除了睡一秒钟之外什么也不做。
public class Task implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 创建示例的主类。创建一个名为
Main的类,并实现main()方法。
public class Main {
public static void main(String[] args) {
- 创建一个
MyThreadFactory对象和一个Task对象。
MyThreadFactory factory=new MyThreadFactory("MyThreadFactory");
Task task=new Task();
- 使用
MyThreadFactory对象创建 10 个Thread对象并启动它们。
Thread thread;
System.out.printf("Starting the Threads\n");
for (int i=0; i<10; i++){
thread=factory.newThread(task);
thread.start();
}
- 在控制台中写入线程工厂的统计数据。
System.out.printf("Factory stats:\n");
System.out.printf("%s\n",factory.getStats());
- 运行示例并查看结果。
它是如何工作的...
ThreadFactory接口只有一个名为newThread的方法。它接收一个Runnable对象作为参数,并返回一个Thread对象。当您实现ThreadFactory接口时,您必须实现该接口并重写此方法。大多数基本的ThreadFactory只有一行。
return new Thread(r);
您可以通过添加一些变体来改进这个实现:
-
创建个性化的线程,就像示例中使用特殊格式的名称或甚至创建我们自己的
thread类一样,该类继承了 Java 的Thread类。 -
保存线程创建统计信息,如前面的示例所示
-
限制创建的线程数量
-
验证线程的创建
-
以及您可以想象的任何其他内容
使用工厂设计模式是一种良好的编程实践,但是,如果您实现了ThreadFactory接口来集中创建线程,您必须审查代码以确保所有线程都是使用该工厂创建的。
参见
-
第七章中的实现 ThreadFactory 接口生成自定义线程配方,自定义并发类
-
第七章中的在 Executor 对象中使用我们的 ThreadFactory配方,自定义并发类
第二章:基本线程同步
在本章中,我们将涵盖:
-
同步一个方法
-
在同步类中排列独立属性
-
在同步代码中使用条件
-
使用锁同步代码块
-
使用读/写锁同步数据访问
-
修改锁的公平性
-
在锁中使用多个条件
介绍
并发编程中最常见的情况之一是多个执行线程共享资源。在并发应用程序中,多个线程读取或写入相同的数据,或者访问相同的文件或数据库连接是正常的。这些共享资源可能引发错误情况或数据不一致,我们必须实现机制来避免这些错误。
这些问题的解决方案是通过关键部分的概念得到的。关键部分是指访问共享资源的代码块,不能同时由多个线程执行。
为了帮助程序员实现关键部分,Java(以及几乎所有编程语言)提供了同步机制。当一个线程想要访问关键部分时,它使用这些同步机制之一来查找是否有其他线程正在执行关键部分。如果没有,线程就进入关键部分。否则,线程被同步机制挂起,直到正在执行关键部分的线程结束。当多个线程等待一个线程完成关键部分的执行时,JVM 会选择其中一个,其余的等待他们的轮到。
本章介绍了一些教授如何使用 Java 语言提供的两种基本同步机制的方法:
-
关键字
synchronized -
Lock接口及其实现
同步一个方法
在这个示例中,我们将学习如何使用 Java 中最基本的同步方法之一,即使用Synchronized关键字来控制对方法的并发访问。只有一个执行线程将访问使用Synchronized关键字声明的对象的方法。如果另一个线程尝试访问同一对象的任何使用Synchronized关键字声明的方法,它将被挂起,直到第一个线程完成方法的执行。
换句话说,使用Synchronized关键字声明的每个方法都是一个关键部分,Java 只允许执行对象的一个关键部分。
静态方法有不同的行为。只有一个执行线程将访问使用Synchronized关键字声明的静态方法之一,但另一个线程可以访问该类对象的其他非静态方法。在这一点上你必须非常小心,因为如果一个是静态的,另一个不是,两个线程可以访问两个不同的Synchronized方法。如果这两个方法都改变了相同的数据,就可能出现数据不一致的错误。
为了学习这个概念,我们将实现一个示例,其中有两个线程访问一个共同的对象。我们将有一个银行账户和两个线程;一个向账户转账,另一个从账户取款。没有同步方法,我们可能会得到不正确的结果。同步机制确保账户的最终余额是正确的。
准备就绪
这个示例已经在 Eclipse IDE 中实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Account的类来模拟我们的银行账户。它只有一个名为balance的double属性。
public class Account {
private double balance;
- 实现
setBalance()和getBalance()方法来写入和读取属性的值。
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
- 实现一个名为
addAmount()的方法,该方法增加传递给方法的特定金额的余额值。只有一个线程应该更改余额的值,因此使用synchronized关键字将此方法转换为临界区。
public synchronized void addAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp+=amount;
balance=tmp;
}
- 实现一个名为
subtractAmount()的方法,该方法减少传递给方法的特定金额的余额值。只有一个线程应该更改余额的值,因此使用synchronized关键字将此方法转换为临界区。
public synchronized void subtractAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp-=amount;
balance=tmp;
}
- 实现一个模拟 ATM 的类。它将使用
subtractAmount()方法来减少账户的余额。这个类必须实现Runnable接口以作为线程执行。
public class Bank implements Runnable {
- 将一个
Account对象添加到这个类中。实现初始化该Account对象的类的构造函数。
private Account account;
public Bank(Account account) {
this.account=account;
}
- 实现
run()方法。它对一个账户进行100次subtractAmount()方法的调用以减少余额。
@Override
public void run() {
for (int i=0; i<100; i++){
account.sustractAmount(1000);
}
}
- 实现一个模拟公司的类,并使用
Account类的addAmount()方法来增加账户的余额。这个类必须实现Runnable接口以作为线程执行。
public class Company implements Runnable {
- 将一个
Account对象添加到这个类中。实现初始化该账户对象的类的构造函数。
private Account account;
public Company(Account account) {
this.account=account;
}
- 实现
run()方法。它对一个账户进行100次addAmount()方法的调用以增加余额。
@Override
public void run() {
for (int i=0; i<100; i++){
account.addAmount(1000);
}
}
- 通过创建一个名为
Main的类并包含main()方法来实现应用程序的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
Account对象并将其余额初始化为1000。
Account account=new Account();
account.setBalance(1000);
- 创建一个
Company对象和一个Thread来运行它。
Company company=new Company(account);
Thread companyThread=new Thread(company);
- 创建一个
Bank对象和一个Thread来运行它。
Bank bank=new Bank(account);
Thread bankThread=new Thread(bank);
- 将初始余额写入控制台。
System.out.printf("Account : Initial Balance: %f\n",account.getBalance());
Start the threads.
companyThread.start();
bankThread.start();
- 使用
join()方法等待两个线程的完成,并在控制台中打印出账户的最终余额。
try {
companyThread.join();
bankThread.join();
System.out.printf("Account : Final Balance: %f\n",account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
它是如何工作的...
在这个示例中,您已经开发了一个应用程序,该应用程序增加和减少了模拟银行账户余额的类的余额。该程序对addAmount()方法进行了100次调用,每次调用都会将余额增加1000,并对subtractAmount()方法进行了100次调用,每次调用都会将余额减少1000。您应该期望最终余额和初始余额相等。
您已经尝试使用一个名为tmp的变量来存储账户余额的值,因此您读取了账户余额,增加了临时变量的值,然后再次设置了账户余额的值。此外,您还使用了Thread类的sleep()方法引入了一点延迟,以便执行该方法的线程休眠 10 毫秒,因此如果另一个线程执行该方法,它可能会修改账户余额,从而引发错误。正是synchronized关键字机制避免了这些错误。
如果您想看到共享数据并发访问的问题,请删除addAmount()和subtractAmount()方法的synchronized关键字并运行程序。没有synchronized关键字,当一个线程在读取账户余额的值后休眠时,另一个方法将读取账户余额,因此两个方法都将修改相同的余额,其中一个操作不会反映在最终结果中。
正如您在下面的屏幕截图中所看到的,您可能会得到不一致的结果:

如果您经常运行程序,您将获得不同的结果。线程的执行顺序不受 JVM 保证。因此,每次执行它们时,线程都将以不同的顺序读取和修改账户的余额,因此最终结果将不同。
现在,按照之前学到的方法添加synchronize关键字,并再次运行程序。如下截图所示,现在您可以获得预期的结果。如果经常运行程序,您将获得相同的结果。请参考以下截图:

使用synchronized关键字,我们可以保证并发应用程序中对共享数据的正确访问。
正如我们在本节介绍中提到的,只有一个线程可以访问使用synchronized关键字声明的对象的方法。如果一个线程(A)正在执行一个synchronized方法,另一个线程(B)想要执行同一对象的其他synchronized方法,它将被阻塞,直到线程(A)结束。但是如果 threadB 可以访问同一类的不同对象,则它们都不会被阻塞。
还有更多...
synchronized关键字会降低应用程序的性能,因此您只能在并发环境中修改共享数据的方法上使用它。如果有多个线程调用synchronized方法,只有一个线程会一次执行它们,而其他线程将等待。如果操作不使用synchronized关键字,则所有线程可以同时执行操作,从而减少总执行时间。如果您知道某个方法不会被多个线程调用,请不要使用synchronized关键字。
您可以使用带有synchronized方法的递归调用。由于线程可以访问对象的synchronized方法,因此可以调用该对象的其他synchronized方法,包括正在执行的方法。它不必再次访问synchronized方法。
我们可以使用synchronized关键字来保护对一段代码的访问,而不是整个方法。我们应该以这种方式使用synchronized关键字来保护对共享数据的访问,将其余操作排除在此块之外,从而获得更好的应用性能。目标是使关键部分(一次只能由一个线程访问的代码块)尽可能短。我们已经使用synchronized关键字来保护对更新建筑物中人数的指令的访问,排除了不使用共享数据的此块的长操作。当您以这种方式使用synchronized关键字时,必须将对象引用作为参数传递。只有一个线程可以访问该对象的synchronized代码(块或方法)。通常,我们会使用this关键字来引用执行方法的对象。
synchronized (this) {
// Java code
}
安排同步类中的独立属性
当您使用synchronized关键字来保护一段代码时,您必须将一个对象引用作为参数传递。通常,您会使用this关键字来引用执行方法的对象,但您也可以使用其他对象引用。通常,这些对象将专门为此目的创建。例如,如果一个类中有两个独立的属性被多个线程共享,您必须同步对每个变量的访问,但如果一个线程同时访问其中一个属性,另一个线程访问另一个属性,则不会有问题。
在本节中,您将学习如何通过一个示例来解决这种情况的编程,该示例模拟了一个具有两个屏幕和两个售票处的电影院。当售票处出售票时,它们是为两个电影院中的一个而不是两个,因此每个电影院中的空座位数是独立的属性。
准备工作
本节示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Cinema的类,并向其添加两个名为vacanciesCinema1和vacanciesCinema2的long属性。
public class Cinema {
private long vacanciesCinema1;
private long vacanciesCinema2;
- 在
Cinema类中添加两个额外的Object属性,命名为controlCinema1和controlCinema2。
private final Object controlCinema1, controlCinema2;
- 实现
Cinema类的构造函数,初始化类的所有属性。
public Cinema(){
controlCinema1=new Object();
controlCinema2=new Object();
vacanciesCinema1=20;
vacanciesCinema2=20;
}
- 实现
sellTickets1()方法,当第一个电影院的一些票被售出时调用。它使用controlCinema1对象来控制对同步代码块的访问。
public boolean sellTickets1 (int number) {
synchronized (controlCinema1) {
if (number<vacanciesCinema1) {
vacanciesCinema1-=number;
return true;
} else {
return false;
}
}
}
- 实现
sellTickets2()方法,当第二个电影院的一些票被售出时调用。它使用controlCinema2对象来控制对同步代码块的访问。
public boolean sellTickets2 (int number){
synchronized (controlCinema2) {
if (number<vacanciesCinema2) {
vacanciesCinema2-=number;
return true;
} else {
return false;
}
}
}
- 实现
returnTickets1()方法,当第一个电影院的一些票被退回时调用。它使用controlCinema1对象来控制对同步代码块的访问。
public boolean returnTickets1 (int number) {
synchronized (controlCinema1) {
vacanciesCinema1+=number;
return true;
}
}
- 实现
returnTickets2()方法,当第二个电影院的一些票被退回时调用。它使用controlCinema2对象来控制对同步代码块的访问。
public boolean returnTickets2 (int number) {
synchronized (controlCinema2) {
vacanciesCinema2+=number;
return true;
}
}
- 实现另外两个方法,返回每个电影院的空位数。
public long getVacanciesCinema1() {
return vacanciesCinema1;
}
public long getVacanciesCinema2() {
return vacanciesCinema2;
}
- 实现
TicketOffice1类,并指定它实现Runnable接口。
public class TicketOffice1 implements Runnable {
- 声明一个
Cinema对象,并实现该类的构造函数来初始化该对象。
private Cinema cinema;
public TicketOffice1 (Cinema cinema) {
this.cinema=cinema;
}
- 实现
run()方法,模拟对两个电影院的一些操作。
@Override
public void run() {
cinema.sellTickets1(3);
cinema.sellTickets1(2);
cinema.sellTickets2(2);
cinema.returnTickets1(3);
cinema.sellTickets1(5);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
}
- 实现
TicketOffice2类,并指定它实现Runnable接口。
public class TicketOffice2 implements Runnable {
- 声明一个
Cinema对象,并实现该类的构造函数来初始化该对象。
private Cinema cinema;
public TicketOffice2(Cinema cinema){
this.cinema=cinema;
}
- 实现
run()方法,模拟对两个电影院的一些操作。
@Override
public void run() {
cinema.sellTickets2(2);
cinema.sellTickets2(4);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTickets2(2);
cinema.sellTickets1(3);
cinema.sellTickets2(2);
cinema.sellTickets1(2);
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 声明并创建一个
Cinema对象。
Cinema cinema=new Cinema();
- 创建一个
TicketOffice1对象和Thread来执行它。
TicketOffice1 ticketOffice1=new TicketOffice1(cinema);
Thread thread1=new Thread(ticketOffice1,"TicketOffice1");
- 创建一个
TicketOffice2对象和Thread来执行它。
TicketOffice2 ticketOffice2=new TicketOffice2(cinema);
Thread thread2=new Thread(ticketOffice2,"TicketOffice2");
- 启动两个线程。
thread1.start();
thread2.start();
- 等待线程完成。
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
- 将两个电影院的空位数写入控制台。
System.out.printf("Room 1 Vacancies: %d\n",cinema.getVacanciesCinema1());
System.out.printf("Room 2 Vacancies: %d\n",cinema.getVacanciesCinema2());
它是如何工作的...
当使用同步关键字保护一段代码时,使用一个对象作为参数。JVM 保证只有一个线程可以访问使用该对象保护的所有代码块(请注意,我们总是谈论对象,而不是类)。
注意
在这个示例中,我们有一个对象来控制对vacanciesCinema1属性的访问,因此每次只有一个线程可以修改这个属性,另一个对象控制对vacanciesCinema2属性的访问,因此每次只有一个线程可以修改这个属性。但可能会有两个线程同时运行,一个修改vacancesCinema1属性,另一个修改vacanciesCinema2属性。
当运行此示例时,您可以看到最终结果始终是每个电影院预期的空位数。在下面的屏幕截图中,您可以看到应用程序执行的结果:

还有更多...
同步关键字还有其他重要的用途。请参阅另请参阅部分,了解其他解释此关键字用法的示例。
另请参阅
- 在第二章的基本线程同步中的在同步代码中使用条件示例中
在同步代码中使用条件
并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个生产者将数据保存在缓冲区中,一个或多个消费者从缓冲区中取数据。
由于缓冲区是共享数据结构,我们必须使用同步机制来控制对它的访问,比如同步关键字,但我们有更多的限制。如果缓冲区已满,生产者就不能将数据保存在缓冲区中,如果缓冲区为空,消费者就不能从缓冲区中取数据。
对于这种情况,Java 提供了在Object类中实现的wait()、notify()和notifyAll()方法。线程可以在同步代码块中调用wait()方法。如果它在同步代码块之外调用wait()方法,JVM 会抛出IllegalMonitorStateException异常。当线程调用wait()方法时,JVM 会让线程进入睡眠状态,并释放控制同步代码块的对象,允许其他线程执行由该对象保护的其他同步代码块。要唤醒线程,必须在由相同对象保护的代码块中调用notify()或notifyAll()方法。
在这个示例中,您将学习如何使用同步关键字和wait()、notify()和notifyAll()方法来实现生产者-消费者问题。
准备工作
这个示例的实现使用了 Eclipse IDE。如果您使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
EventStorage的类。它有两个属性:一个名为maxSize的int属性和一个名为storage的LinkedList<Date>属性。
public class EventStorage {
private int maxSize;
private List<Date> storage;
- 实现初始化类属性的类构造函数。
public EventStorage(){
maxSize=10;
storage=new LinkedList<>();
}
- 实现
同步方法set()以将事件存储在存储中。首先,检查存储是否已满。如果满了,调用wait()方法直到存储有空余空间。在方法结束时,调用notifyAll()方法唤醒所有在wait()方法中睡眠的线程。
public synchronized void set(){
while (storage.size()==maxSize){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.offer(new Date());
System.out.printf("Set: %d",storage.size());
notifyAll();
}
- 实现
同步方法get()以获取存储的事件。首先,检查存储是否有事件。如果没有事件,调用wait()方法,直到存储有事件为止。在方法结束时,调用notifyAll()方法唤醒所有在wait()方法中睡眠的线程。
public synchronized void get(){
while (storage.size()==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("Get: %d: %s",storage.size(),((LinkedList<?>)storage).poll());
notifyAll();
}
- 创建一个名为
Producer的类,并指定它实现Runnable接口。它将实现示例的生产者。
public class Producer implements Runnable {
- 声明一个
EventStore对象并实现初始化该对象的类构造函数。
private EventStorage storage;
public Producer(EventStorage storage){
this.storage=storage;
}
- 实现调用
EventStorage对象的set()方法100次的run()方法。
@Override
public void run() {
for (int i=0; i<100; i++){
storage.set();
}
}
- 创建一个名为
Consumer的类,并指定它实现Runnable接口。它将实现示例的消费者。
public class Consumer implements Runnable {
- 声明一个
EventStorage对象并实现初始化该对象的类构造函数。
private EventStorage storage;
public Consumer(EventStorage storage){
this.storage=storage;
}
- 实现
run()方法。它调用EventStorage对象的get()方法100次。
@Override
public void run() {
for (int i=0; i<100; i++){
storage.get();
}
}
- 通过实现一个名为
Main的类并添加main()方法来创建示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
EventStorage对象。
EventStorage storage=new EventStorage();
- 创建一个
Producer对象和一个Thread来运行它。
Producer producer=new Producer(storage);
Thread thread1=new Thread(producer);
- 创建一个
Consumer对象和一个Thread来运行它。
Consumer consumer=new Consumer(storage);
Thread thread2=new Thread(consumer);
- 启动两个线程。
thread2.start();
thread1.start();
它是如何工作的...
这个例子的关键是EventStorage类的set()和get()方法。首先,set()方法检查存储属性中是否有空闲空间。如果满了,调用wait()方法等待空闲空间。当其他线程调用notifyAll()方法时,线程会被唤醒并再次检查条件。notifyAll()方法不能保证线程会被唤醒。这个过程会重复,直到存储中有空闲空间并且可以生成新的事件并存储它。
get()方法的行为类似。首先,它检查存储中是否有事件。如果EventStorage类为空,调用wait()方法等待事件。当其他线程调用notifyAll()方法时,线程会被唤醒并再次检查条件,直到存储中有事件为止。
注意
您必须不断检查条件,并在while循环中调用wait()方法。直到条件为true为止,您才能继续。
如果您运行此示例,您将看到生产者和消费者如何设置和获取事件,但存储中从未有超过 10 个事件。
还有更多...
synchronized关键字还有其他重要的用途。请参阅另请参阅部分,了解解释此关键字用法的其他配方。
另请参阅
- 第二章中的在同步类中排列独立属性配方,基本线程同步
使用锁同步代码块
Java 提供了另一种用于同步代码块的机制。这是一种比synchronized关键字更强大和灵活的机制。它基于Lock接口和实现它的类(如ReentrantLock)。这种机制具有一些优势,如下所示:
-
它允许以更灵活的方式构造同步块。使用
synchronized关键字,您必须以结构化的方式获取和释放同步代码块的控制权。Lock接口允许您获得更复杂的结构来实现您的临界区。 -
Lock接口提供了比synchronized关键字更多的功能。其中一个新功能是tryLock()方法。此方法尝试获取锁的控制权,如果无法获取(因为它被其他线程使用),则返回该锁。使用synchronized关键字时,当线程(A)尝试执行同步代码块时,如果有另一个线程(B)正在执行它,线程(A)将被挂起,直到线程(B)完成同步块的执行。使用锁,您可以执行tryLock()方法。此方法返回一个Boolean值,指示是否有另一个线程运行由此锁保护的代码。 -
Lock接口允许对读和写操作进行分离,具有多个读取者和仅一个修改者。 -
Lock接口的性能比synchronized关键字更好。
在这个配方中,您将学习如何使用锁来同步代码块,并使用Lock接口和实现它的ReentrantLock类创建临界区,实现一个模拟打印队列的程序。
准备就绪...
这个配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
PrintQueue的类,它将实现打印队列。
public class PrintQueue {
- 声明一个
Lock对象,并使用ReentrantLock类的新对象对其进行初始化。
private final Lock queueLock=new ReentrantLock();
- 实现
printJob()方法。它将接收Object作为参数,并不会返回任何值。
public void printJob(Object document){
- 在
printJob()方法内部,通过调用lock()方法获取Lock对象的控制权。
queueLock.lock();
- 然后,包括以下代码来模拟打印文档:
try {
Long duration=(long)(Math.random()*10000);
System.out.println(Thread.currentThread().getName()+ ": PrintQueue: Printing a Job during "+(duration/1000)+
" seconds");
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 最后,使用
unlock()方法释放Lock对象的控制权。
finally {
queueLock.unlock();
}
- 创建一个名为
Job的类,并指定它实现Runnable接口。
public class Job implements Runnable {
- 声明一个
PrintQueue类的对象,并实现初始化该对象的类的构造函数。
private PrintQueue printQueue;
public Job(PrintQueue printQueue){
this.printQueue=printQueue;
}
- 实现
run()方法。它使用PrintQueue对象发送打印作业。
@Override
public void run() {
System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
- 通过实现一个名为
Main的类并向其中添加main()方法,创建应用程序的主类。
public class Main {
public static void main (String args[]){
- 创建一个共享的
PrintQueue对象。
PrintQueue printQueue=new PrintQueue();
- 创建 10 个
Job对象和 10 个线程来运行它们。
Thread thread[]=new Thread[10];
for (int i=0; i<10; i++){
thread[i]=new Thread(new Job(printQueue),"Thread "+ i);
}
- 启动 10 个线程。
for (int i=0; i<10; i++){
thread[i].start();
}
它是如何工作的...
在下面的屏幕截图中,您可以看到一个执行的部分输出,例如:

示例的关键在于PrintQueue类的printJob()方法。当我们想要使用锁实现临界区并确保只有一个执行线程运行代码块时,我们必须创建一个ReentrantLock对象。在临界区的开始,我们必须使用lock()方法获取锁的控制权。当一个线程(A)调用此方法时,如果没有其他线程控制着锁,该方法将给予线程(A)锁的控制权,并立即返回以允许该线程执行临界区。否则,如果有另一个线程(B)执行由此锁控制的临界区,lock()方法将使线程(A)进入休眠状态,直到线程(B)完成临界区的执行。
在临界区的结束,我们必须使用unlock()方法释放锁的控制权,并允许其他线程运行此临界区。如果在临界区结束时不调用unlock()方法,那些正在等待该块的其他线程将永远等待,导致死锁情况。如果在临界区中使用 try-catch 块,请不要忘记将包含unlock()方法的语句放在finally部分中。
还有更多...
Lock接口(以及ReentrantLock类)包括另一个方法来获取锁的控制权。这就是tryLock()方法。与lock()方法最大的区别在于,如果使用它的线程无法获得Lock接口的控制权,该方法将立即返回,而不会使线程进入休眠状态。该方法返回一个boolean值,如果线程获得了锁的控制权,则返回true,否则返回false。
注意
请注意,程序员有责任考虑此方法的结果并相应地采取行动。如果该方法返回false值,则预期您的程序不会执行临界区。如果执行了,您的应用程序可能会产生错误的结果。
ReentrantLock类还允许使用递归调用。当一个线程控制着一个锁并进行递归调用时,它将继续控制着锁,因此调用lock()方法将立即返回,线程将继续执行递归调用。此外,我们还可以调用其他方法。
更多信息
您必须非常小心地使用Locks以避免死锁。当两个或更多线程被阻塞等待永远不会被解锁的锁时,就会发生这种情况。例如,一个线程(A)锁定了一个锁(X),而另一个线程(B)锁定了一个锁(Y)。如果现在,线程(A)尝试锁定锁(Y),而线程(B)同时尝试锁定锁(X),那么两个线程将无限期地被阻塞,因为它们正在等待永远不会被释放的锁。请注意,问题出现在于两个线程尝试以相反的顺序获取锁。附录并发编程设计解释了一些设计并发应用程序并避免这些死锁问题的好建议。
另请参阅
-
在第二章的基本线程同步中的同步方法配方
-
在第二章的基本线程同步中的在锁中使用多个条件配方中
-
在第八章的测试并发应用中的监视锁接口配方
使用读/写锁同步数据访问
锁提供的最重要的改进之一是ReadWriteLock接口和ReentrantReadWriteLock类,它是唯一实现它的类。这个类有两个锁,一个用于读操作,一个用于写操作。可以有多个线程同时使用读操作,但只能有一个线程使用写操作。当一个线程执行写操作时,不能有任何线程执行读操作。
在本示例中,您将学习如何使用ReadWriteLock接口来实现一个程序,该程序使用它来控制对存储两种产品价格的对象的访问。
准备就绪...
您应该阅读Synchronizing a block of code with a Lock一节,以更好地理解本节。
如何做...
按照以下步骤实现示例:
- 创建一个名为
PricesInfo的类,用于存储两种产品的价格信息。
public class PricesInfo {
- 声明两个名为
price1和price2的double属性。
private double price1;
private double price2;
- 声明一个名为
lock的ReadWriteLock对象。
private ReadWriteLock lock;
- 实现初始化三个属性的类的构造函数。对于
lock属性,我们创建一个新的ReentrantReadWriteLock对象。
public PricesInfo(){
price1=1.0;
price2=2.0;
lock=new ReentrantReadWriteLock();
}
- 实现
getPrice1()方法,该方法返回price1属性的值。它使用读锁来控制对该属性值的访问。
public double getPrice1() {
lock.readLock().lock();
double value=price1;
lock.readLock().unlock();
return value;
}
- 实现
getPrice2()方法,该方法返回price2属性的值。它使用读锁来控制对该属性值的访问。
public double getPrice2() {
lock.readLock().lock();
double value=price2;
lock.readLock().unlock();
return value;
}
- 实现
setPrices()方法,用于设置两个属性的值。它使用写锁来控制对它们的访问。
public void setPrices(double price1, double price2) {
lock.writeLock().lock();
this.price1=price1;
this.price2=price2;
lock.writeLock().unlock();
}
- 创建一个名为
Reader的类,并指定它实现Runnable接口。该类实现了PricesInfo类属性值的读取器。
public class Reader implements Runnable {
- 声明一个名为
PricesInfo的对象,并实现初始化该对象的类的构造函数。
private PricesInfo pricesInfo;
public Reader (PricesInfo pricesInfo){
this.pricesInfo=pricesInfo;
}
- 为这个类实现
run()方法。它读取两个价格的值 10 次。
@Override
public void run() {
for (int i=0; i<10; i++){
System.out.printf("%s: Price 1: %f\n", Thread.currentThread().getName(),pricesInfo.getPrice1());
System.out.printf("%s: Price 2: %f\n", Thread.currentThread().getName(),pricesInfo.getPrice2());
}
}
- 创建一个名为
Writer的类,并指定它实现Runnable接口。该类实现了PricesInfo类属性值的修改器。
public class Writer implements Runnable {
- 声明一个名为
PricesInfo的对象,并实现初始化该对象的类的构造函数。
private PricesInfo pricesInfo;
public Writer(PricesInfo pricesInfo){
this.pricesInfo=pricesInfo;
}
- 实现
run()方法。它在修改两个价格的值之间休眠两秒,共修改三次。
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.printf("Writer: Attempt to modify the prices.\n");
pricesInfo.setPrices(Math.random()*10, Math.random()*8);
System.out.printf("Writer: Prices have been modified.\n");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
PricesInfo对象。
PricesInfo pricesInfo=new PricesInfo();
- 创建五个
Reader对象和五个Thread来执行它们。
Reader readers[]=new Reader[5];
Thread threadsReader[]=new Thread[5];
for (int i=0; i<5; i++){
readers[i]=new Reader(pricesInfo);
threadsReader[i]=new Thread(readers[i]);
}
- 创建一个
Writer对象和一个Thread来执行它。
Writer writer=new Writer(pricesInfo);
Thread threadWriter=new Thread(writer);
- 启动线程。
for (int i=0; i<5; i++){
threadsReader[i].start();
}
threadWriter.start();
它是如何工作的...
在下面的截图中,您可以看到此示例的一个执行输出的一部分:

正如我们之前提到的,ReentrantReadWriteLock类有两个锁,一个用于读操作,一个用于写操作。在读操作中使用的锁是通过ReadWriteLock接口中声明的readLock()方法获得的。这个锁是一个实现了Lock接口的对象,所以我们可以使用lock()、unlock()和tryLock()方法。在写操作中使用的锁是通过ReadWriteLock接口中声明的writeLock()方法获得的。这个锁是一个实现了Lock接口的对象,所以我们可以使用lock()、unlock()和tryLock()方法。程序员有责任确保正确使用这些锁,使用它们的目的与它们设计的目的相同。当您获得Lock接口的读锁时,您不能修改变量的值。否则,您可能会遇到数据不一致的错误。
另请参阅
-
在第二章的Synchronizing a block of code with a Lock一节中,基本线程同步
-
在第八章的监视锁接口食谱中,测试并发应用程序
修改锁的公平性
ReentrantLock和ReentrantReadWriteLock类的构造函数接受一个名为fair的boolean参数,允许您控制这两个类的行为。false值是默认值,称为非公平模式。在此模式下,当有一些线程等待锁(ReentrantLock或ReentrantReadWriteLock)并且锁必须选择其中一个来访问临界区时,它会选择一个而没有任何标准。true值称为公平模式。在此模式下,当有一些线程等待锁(ReentrantLock或ReentrantReadWriteLock)并且锁必须选择一个来访问临界区时,它会选择等待时间最长的线程。请注意,前面解释的行为仅用于lock()和unlock()方法。由于tryLock()方法在使用Lock接口时不会使线程进入睡眠状态,因此公平属性不会影响其功能。
在本食谱中,我们将修改在使用锁同步代码块食谱中实现的示例,以使用此属性并查看公平和非公平模式之间的区别。
做好准备...
我们将修改在使用锁同步代码块食谱中实现的示例,因此请阅读该食谱以实现此示例。
如何做...
按照以下步骤实现示例:
-
实现在使用锁同步代码块食谱中解释的示例。
-
在
PrintQueue类中,修改Lock对象的构造。新的指令如下所示:
private Lock queueLock=new ReentrantLock(true);
- 修改
printJob()方法。将打印模拟分为两个代码块,在它们之间释放锁。
public void printJob(Object document){
queueLock.lock();
try {
Long duration=(long)(Math.random()*10000);
System.out.println(Thread.currentThread().getName()+": PrintQueue: Printing a Job during "+(duration/1000)+" seconds");
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
Long duration=(long)(Math.random()*10000);
System.out.println(Thread.currentThread().getName()+": PrintQueue: Printing a Job during "+(duration/1000)+" seconds");
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
- 在
Main类中修改启动线程的代码块。新的代码块如下所示:
for (int i=0; i<10; i++){
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
它是如何工作的...
在下面的屏幕截图中,您可以看到此示例的一次执行输出的一部分:

所有线程的创建间隔为 0.1 秒。请求控制锁的第一个线程是线程 0,然后是线程 1,依此类推。当线程 0运行由锁保护的第一个代码块时,我们有九个线程等待执行该代码块。当线程 0释放锁时,立即再次请求锁,因此我们有 10 个线程尝试获取锁。由于启用了公平模式,Lock接口将选择线程 1,因此它是等待时间最长的线程。然后选择线程 2,然后是线程 3,依此类推。直到所有线程都通过了由锁保护的第一个代码块,它们才会执行由锁保护的第二个代码块。
一旦所有线程执行了由锁保护的第一个代码块,再次轮到线程 0。然后是线程 1,依此类推。
要查看与非公平模式的区别,请更改传递给锁构造函数的参数并将其设置为false值。在下面的屏幕截图中,您可以看到修改后示例的执行结果:

在这种情况下,线程按照它们被创建的顺序执行,但每个线程都执行两个受保护的代码块。但是,这种行为不能保证,因为如前所述,锁可以选择任何线程来让其访问受保护的代码。在这种情况下,JVM 不能保证线程的执行顺序。
还有更多...
读/写锁在其构造函数中也有公平参数。此参数在这种类型的锁中的行为与我们在本食谱介绍中解释的相同。
另请参阅
-
在第二章中的使用锁同步代码块示例中,基本线程同步
-
在第二章中的使用读/写锁同步数据访问示例中,基本线程同步
-
在第七章中的实现自定义锁类示例中,自定义并发类
在锁中使用多个条件
一个锁可以与一个或多个条件关联。这些条件在Condition接口中声明。这些条件的目的是允许线程控制锁,并检查条件是否为true,如果为false,则暂停,直到另一个线程唤醒它们。Condition接口提供了挂起线程和唤醒挂起线程的机制。
并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个将数据保存在缓冲区中的生产者,以及一个或多个从缓冲区中取出数据的消费者,正如本章前面所述
在这个示例中,您将学习如何使用锁和条件来实现生产者-消费者问题。
准备就绪...
您应该阅读使用锁同步代码块示例,以更好地理解这个示例。
如何做...
按照以下步骤实现示例:
- 首先,让我们实现一个类,模拟文本文件。创建一个名为
FileMock的类,具有两个属性:一个名为content的String数组和一个名为index的int。它们将存储文件的内容和将被检索的模拟文件的行。
public class FileMock {
private String content[];
private int index;
- 实现类的构造函数,初始化文件内容为随机字符。
public FileMock(int size, int length){
content=new String[size];
for (int i=0; i<size; i++){
StringBuilder buffer=new StringBuilder(length);
for (int j=0; j<length; j++){
int indice=(int)Math.random()*255;
buffer.append((char)indice);
}
content[i]=buffer.toString();
}
index=0;
}
- 实现
hasMoreLines()方法,如果文件有更多行要处理,则返回true,如果已经到达模拟文件的末尾,则返回false。
public boolean hasMoreLines(){
return index<content.length;
}
- 实现
getLine()方法,返回由索引属性确定的行并增加其值。
public String getLine(){
if (this.hasMoreLines()) {
System.out.println("Mock: "+(content.length-index));
return content[index++];
}
return null;
}
- 现在,实现一个名为
Buffer的类,它将实现生产者和消费者共享的缓冲区。
public class Buffer {
- 这个类有六个属性:
-
一个名为
buffer的LinkedList<String>属性,用于存储共享数据 -
定义一个名为
maxSize的int类型,用于存储缓冲区的长度 -
一个名为
lock的ReentrantLock对象,用于控制修改缓冲区的代码块的访问 -
两个名为
lines和space的Condition属性 -
一个名为
pendingLines的boolean类型,它将指示缓冲区中是否有行
private LinkedList<String> buffer;
private int maxSize;
private ReentrantLock lock;
private Condition lines;
private Condition space;
private boolean pendingLines;
- 实现类的构造函数。它初始化先前描述的所有属性。
public Buffer(int maxSize) {
this.maxSize=maxSize;
buffer=new LinkedList<>();
lock=new ReentrantLock();
lines=lock.newCondition();
space=lock.newCondition();
pendingLines=true;
}
- 实现
insert()方法。它接收String作为参数,并尝试将其存储在缓冲区中。首先,它获取锁的控制权。当它拥有它时,它会检查缓冲区是否有空间。如果缓冲区已满,它会调用space条件中的await()方法等待空闲空间。当另一个线程调用space条件中的signal()或signalAll()方法时,线程将被唤醒。发生这种情况时,线程将行存储在缓冲区中,并调用lines条件上的signallAll()方法。正如我们将在下一刻看到的,这个条件将唤醒所有等待缓冲区中行的线程。
public void insert(String line) {
lock.lock();
try {
while (buffer.size() == maxSize) {
space.await();
}
buffer.offer(line);
System.out.printf("%s: Inserted Line: %d\n", Thread.currentThread().getName(),buffer.size());
lines.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
- 实现
get()方法。它返回缓冲区中存储的第一个字符串。首先,它获取锁的控制权。当它拥有它时,它会检查缓冲区中是否有行。如果缓冲区为空,它会调用lines条件中的await()方法等待缓冲区中的行。当另一个线程调用lines条件中的signal()或signalAll()方法时,该线程将被唤醒。当发生这种情况时,该方法获取缓冲区中的第一行,调用space条件上的signalAll()方法,并返回String。
public String get() {
String line=null;
lock.lock();
try {
while ((buffer.size() == 0) &&(hasPendingLines())) {
lines.await();
}
if (hasPendingLines()) {
line = buffer.poll();
System.out.printf("%s: Line Readed: %d\n",Thread.currentThread().getName(),buffer.size());
space.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return line;
}
- 实现
setPendingLines()方法,建立pendingLines属性的值。当生产者没有更多行要生产时,将调用它。
public void setPendingLines(boolean pendingLines) {
this.pendingLines=pendingLines;
}
- 实现
hasPendingLines()方法。如果有更多行要处理,则返回true,否则返回false。
public boolean hasPendingLines() {
return pendingLines || buffer.size()>0;
}
- 现在轮到生产者了。实现一个名为
Producer的类,并指定它实现Runnable接口。
public class Producer implements Runnable {
- 声明两个属性:
FileMock类的一个对象和Buffer类的另一个对象。
private FileMock mock;
private Buffer buffer;
- 实现初始化两个属性的类的构造函数。
public Producer (FileMock mock, Buffer buffer){
this.mock=mock;
this.buffer=buffer;
}
- 实现
run()方法,读取FileMock对象中创建的所有行,并使用insert()方法将它们存储在缓冲区中。完成后,使用setPendingLines()方法通知缓冲区不会再生成更多行。
@Override
public void run() {
buffer.setPendingLines(true);
while (mock.hasMoreLines()){
String line=mock.getLine();
buffer.insert(line);
}
buffer.setPendingLines(false);
}
- 接下来是消费者的轮次。实现一个名为
Consumer的类,并指定它实现Runnable接口。
public class Consumer implements Runnable {
- 声明一个
Buffer对象并实现初始化它的类的构造函数。
private Buffer buffer;
public Consumer (Buffer buffer) {
this.buffer=buffer;
}
- 实现
run()方法。在缓冲区有待处理的行时,它尝试获取并处理其中的一行。
@Override
public void run() {
while (buffer.hasPendingLines()) {
String line=buffer.get();
processLine(line);
}
}
- 实现辅助方法
processLine()。它只休眠 10 毫秒,模拟对行进行某种处理。
private void processLine(String line) {
try {
Random random=new Random();
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
FileMock对象。
FileMock mock=new FileMock(100, 10);
- 创建一个
Buffer对象。
Buffer buffer=new Buffer(20);
- 创建一个
Producer对象和一个Thread来运行它。
Producer producer=new Producer(mock, buffer);
Thread threadProducer=new Thread(producer,"Producer");
- 创建三个
Consumer对象和三个线程来运行它。
Consumer consumers[]=new Consumer[3];
Thread threadConsumers[]=new Thread[3];
for (int i=0; i<3; i++){
consumers[i]=new Consumer(buffer);
threadConsumers[i]=new Thread(consumers[i],"Consumer "+i);
}
- 启动生产者和三个消费者。
threadProducer.start();
for (int i=0; i<3; i++){
threadConsumers[i].start();
}
它是如何工作的...
所有的Condition对象都与一个锁相关联,并且是使用Lock接口中声明的newCondition()方法创建的。在我们可以对条件进行任何操作之前,必须控制与条件相关联的锁,因此条件的操作必须在以Lock对象的lock()方法调用开始的代码块中,并以相同Lock对象的unlock()方法结束。
当一个线程调用条件的await()方法时,它会自动释放锁的控制权,以便另一个线程可以获取它并开始执行相同的临界区或由该锁保护的另一个临界区。
注意
当一个线程调用条件的signal()或signallAll()方法时,等待该条件的一个或所有线程被唤醒,但这并不保证使它们休眠的条件现在是true,因此必须将await()调用放在while循环中。在条件为true之前,不能离开该循环。条件为false时,必须再次调用await()。
在使用await()和signal()时必须小心。如果在条件中调用await()方法,但从未在该条件中调用signal()方法,线程将永远休眠。
在休眠时,线程可能会被中断,在调用await()方法后,因此必须处理InterruptedException异常。
还有更多...
Condition接口有await()方法的其他版本,如下所示:
-
await(long time, TimeUnit unit): 线程将休眠直到: -
它被中断了
-
另一个线程在条件中调用
signal()或signalAll()方法 -
指定的时间已经过去
-
TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS -
awaitUninterruptibly(): 线程将休眠直到另一个线程调用signal()或signalAll()方法,这是不可中断的 -
awaitUntil(Date date): 线程将休眠直到: -
它被中断了
-
另一个线程在条件中调用
signal()或signalAll()方法 -
指定的日期到达
您可以使用条件与读/写锁的ReadLock和WriteLock锁。
另请参阅
-
在《第二章》(ch02.html“第二章基本线程同步”)的使用锁同步代码块配方中
-
在《第二章》(ch02.html“第二章基本线程同步”)的使用读/写锁同步数据访问配方
第三章:线程同步工具
在本章中,我们将涵盖:
-
控制对资源的并发访问
-
控制对多个资源副本的并发访问
-
等待多个并发事件
-
在一个公共点同步任务
-
运行并发分阶段任务
-
控制并发分阶段任务中的阶段变化
-
在并发任务之间交换数据
介绍
在第二章,基本线程同步,我们学习了同步和关键部分的概念。基本上,当多个并发任务共享一个资源时,例如一个对象或对象的属性时,我们谈论同步。访问这个共享资源的代码块被称为关键部分。
如果不使用适当的机制,可能会出现错误的结果、数据不一致或错误条件,因此我们必须采用 Java 语言提供的同步机制之一来避免所有这些问题。
第二章,基本线程同步,教会了我们以下基本同步机制:
-
同步关键字 -
Lock接口及其实现类:ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock
在本章中,我们将学习如何使用高级机制来实现多个线程的同步。这些高级机制如下:
-
信号量:信号量是控制对一个或多个共享资源的访问的计数器。这种机制是并发编程的基本工具之一,并且大多数编程语言都提供了它。
-
CountDownLatch:
CountDownLatch类是 Java 语言提供的一种机制,允许线程等待多个操作的完成。 -
CyclicBarrier:
CyclicBarrier类是 Java 语言提供的另一种机制,允许多个线程在一个公共点同步。 -
Phaser:
Phaser类是 Java 语言提供的另一种机制,用于控制分阶段并发任务的执行。所有线程必须在继续下一个阶段之前完成一个阶段。这是 Java 7 API 的一个新特性。 -
Exchanger:
Exchanger类是 Java 语言提供的另一种机制,提供了两个线程之间的数据交换点。
信号量是一种通用的同步机制,您可以用它来保护任何问题中的关键部分。其他机制被认为是用于具有特定特征的应用程序,正如之前所描述的。请根据您的应用程序的特点选择适当的机制。
本章介绍了七个示例,展示了如何使用所描述的机制。
控制对资源的并发访问
在这个示例中,您将学习如何使用 Java 语言提供的信号量机制。信号量是保护对一个或多个共享资源的访问的计数器。
注
信号量的概念是由 Edsger Dijkstra 于 1965 年引入的,并且首次在 THEOS 操作系统中使用。
当一个线程想要访问其中一个共享资源时,首先必须获取信号量。如果信号量的内部计数器大于0,则信号量会减少计数器并允许访问共享资源。计数器大于0意味着有空闲资源可以使用,因此线程可以访问并使用其中一个。
否则,如果信号量的计数器为0,则信号量将线程置于休眠状态,直到计数器大于0。计数器为0表示所有共享资源都被其他线程使用,因此想要使用其中一个的线程必须等待直到有一个空闲。
当线程完成对共享资源的使用时,它必须释放信号量,以便其他线程可以访问共享资源。这个操作会增加信号量的内部计数器。
在这个示例中,您将学习如何使用Semaphore类来实现特殊类型的信号量,称为二进制信号量。这些类型的信号量保护对唯一共享资源的访问,因此信号量的内部计数器只能取值1或0。为了演示如何使用它,您将实现一个打印队列,可以供并发任务使用来打印它们的作业。这个打印队列将受到二进制信号量的保护,因此一次只能有一个线程打印。
准备工作
这个示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
PrintQueue的类,它将实现打印队列。
public class PrintQueue {
- 声明一个
Semaphore对象。将其命名为semaphore。
private final Semaphore semaphore;
- 实现类的构造函数。它初始化了将保护对打印队列的访问的
semaphore对象。
public PrintQueue(){
semaphore=new Semaphore(1);
}
- 实现
printJob()方法来模拟打印文档。它接收名为document的Object作为参数。
public void printJob (Object document){
- 在方法内部,首先必须调用
acquire()方法来获取信号量。这个方法可能会抛出InterruptedException异常,所以您必须包含一些代码来处理它。
try {
semaphore.acquire();
- 然后,实现模拟打印文档并等待随机时间段的行。
long duration=(long)(Math.random()*10);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),duration);
Thread.sleep(duration);
- 最后,通过调用信号量的
release()方法释放信号量。
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
- 创建一个名为
Job的类,并指定它实现Runnable接口。这个类实现了向打印机发送文档的作业。
public class Job implements Runnable {
- 声明一个
PrintQueue对象。将其命名为printQueue。
private PrintQueue printQueue;
- 实现类的构造函数。它初始化了类中声明的
PrintQueue对象。
public Job(PrintQueue printQueue){
this.printQueue=printQueue;
}
- 实现
run()方法。
@Override
public void run() {
- 首先,该方法向控制台写入一条消息,显示作业已经开始执行。
System.out.printf("%s: Going to print a job\n",Thread.currentThread().getName());
- 然后,调用
PrintQueue对象的printJob()方法。
printQueue.printJob(new Object());
- 最后,该方法向控制台写入一条消息,显示它已经完成了执行。
System.out.printf("%s: The document has been printed\n",Thread.currentThread().getName());
}
- 通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main (String args[]){
- 创建一个名为
printQueue的PrintQueue对象。
PrintQueue printQueue=new PrintQueue();
- 创建 10 个线程。这些线程中的每一个都将执行一个
Job对象,该对象将向打印队列发送一个文档。
Thread thread[]=new Thread[10];
for (int i=0; i<10; i++){
thread[i]=new Thread(new Job(printQueue),"Thread"+i);
}
- 最后,启动 10 个线程。
for (int i=0; i<10; i++){
thread[i].start();
}
它是如何工作的...
这个示例的关键在PrintQueue类的printJob()方法中。这个方法展示了在使用信号量实现临界区并保护对共享资源访问时,您必须遵循的三个步骤:
-
首先,使用
acquire()方法获取信号量。 -
然后,执行与共享资源的必要操作。
-
最后,通过调用
release()方法释放信号量。
这个示例中的另一个重要点是PrintQueue类的构造函数和Semaphore对象的初始化。您将1作为这个构造函数的参数传递,因此您正在创建一个二进制信号量。内部计数器的初始值为1,因此您将保护对一个共享资源的访问,即打印队列。
当您启动 10 个线程时,第一个线程会获取信号量并获得对临界区的访问。其余线程被信号量阻塞,直到已经获取信号量的线程释放它。当这种情况发生时,信号量会选择一个等待的线程并允许其访问临界区。所有的作业都会打印它们的文档,但是一个接一个地进行。
还有更多...
Semaphore类有两个额外版本的acquire()方法:
-
acquireUninterruptibly():acquire()方法;当信号量的内部计数器为0时,阻塞线程直到信号量被释放。在此阻塞时间内,线程可能会被中断,然后此方法抛出InterruptedException异常。此版本的获取操作忽略线程的中断,并且不会抛出任何异常。 -
tryAcquire(): 此方法尝试获取信号量。如果可以,该方法返回true值。但是如果不能,该方法返回false值,而不是被阻塞并等待信号量的释放。根据return值,您有责任采取正确的操作。
信号量中的公平性
公平性的概念被 Java 语言用于所有可以有各种线程阻塞等待同步资源释放的类(例如信号量)。默认模式称为非公平模式。在这种模式下,当同步资源被释放时,会选择等待的线程中的一个来获取此资源,但是选择是没有任何标准的。公平模式改变了这种行为,并强制选择等待时间更长的线程。
与其他类一样,Semaphore类在其构造函数中接受第二个参数。此参数必须采用Boolean值。如果给定false值,则创建一个将以非公平模式工作的信号量。如果不使用此参数,将获得相同的行为。如果给定true值,则创建一个将以公平模式工作的信号量。
另请参阅
-
第八章中的监视锁接口配方,测试并发应用程序
-
第二章中的修改锁公平性配方,基本线程同步
控制对资源的多个副本的并发访问
在控制对资源的并发访问配方中,您学习了信号量的基础知识。
在那个配方中,您使用了二进制信号量来实现一个示例。这些类型的信号量用于保护对一个共享资源的访问,或者只能由一个线程执行的临界区。但是当您需要保护资源的多个副本时,或者当您有一个可以同时由多个线程执行的临界区时,也可以使用信号量。
在这个配方中,您将学习如何使用信号量来保护多个资源的副本。您将实现一个示例,其中有一个打印队列,可以在三台不同的打印机上打印文档。
准备工作
本配方的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
在本章中实现控制对资源的并发访问配方中描述的示例。
如何做...
按照以下步骤实现示例:
- 正如我们之前提到的,您将修改使用信号量实现的打印队列示例。打开
PrintQueue类并声明一个名为freePrinters的boolean数组。该数组存储可以打印作业的空闲打印机和正在打印文档的打印机。
private boolean freePrinters[];
- 还要声明一个名为
lockPrinters的Lock对象。您将使用此对象来保护对freePrinters数组的访问。
private Lock lockPrinters;
- 修改类的构造函数以初始化新声明的对象。
freePrinters数组有三个元素,全部初始化为true值。信号量的初始值为3。
public PrintQueue(){
semaphore=new Semaphore(3);
freePrinters=new boolean[3];
for (int i=0; i<3; i++){
freePrinters[i]=true;
}
lockPrinters=new ReentrantLock();
}
- 还要修改
printJob()方法。它接收一个名为document的Object作为唯一参数。
public void printJob (Object document){
- 首先,该方法调用
acquire()方法来获取对信号量的访问。由于此方法可能会抛出InterruptedException异常,因此必须包含处理它的代码。
try {
semaphore.acquire();
- 然后,使用私有方法
getPrinter()获取分配打印此作业的打印机的编号。
int assignedPrinter=getPrinter();
- 然后,实现模拟打印文档并等待随机时间段的行。
long duration=(long)(Math.random()*10);
System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n",Thread.currentThread().getName(),assignedPrinter,duration);
TimeUnit.SECONDS.sleep(duration);
- 最后,调用
release()方法释放信号量,并将使用的打印机标记为自由,将true分配给freePrinters数组中的相应索引。
freePrinters[assignedPrinter]=true;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
- 实现
getPrinter()方法。这是一个返回int值的私有方法,没有参数。
private int getPrinter() {
- 首先,声明一个
int变量来存储打印机的索引。
int ret=-1;
- 然后,获取
lockPrinters对象的访问权限。
try {
lockPrinters.lock();
- 然后,在
freePrinters数组中找到第一个true值,并将其索引保存在一个变量中。修改此值为false,因为这台打印机将忙碌。
for (int i=0; i<freePrinters.length; i++) {
if (freePrinters[i]){
ret=i;
freePrinters[i]=false;
break;
}
}
- 最后,释放
lockPrinters对象并返回true值的索引。
} catch (Exception e) {
e.printStackTrace();
} finally {
lockPrinters.unlock();
}
return ret;
Job和Core类没有修改。
它是如何工作的...
这个例子的关键在于PrintQueue类。使用3作为构造函数的参数创建Semaphore对象。调用acquire()方法的前三个线程将获得对这个例子的关键部分的访问,而其余的线程将被阻塞。当一个线程完成关键部分并释放信号量时,另一个线程将获取它。
在这个关键部分,线程获取分配打印此作业的打印机的索引。这个例子的这一部分用于使例子更加真实,但它不使用与信号量相关的任何代码。
以下屏幕截图显示了此示例的执行输出:

每个文档都在其中一个打印机上打印。第一个是空闲的。
还有更多...
acquire()、acquireUninterruptibly()、tryAcquire()和release()方法有一个额外的版本,它们有一个int参数。这个参数表示使用它们的线程想要获取或释放的许可数,也就是说,这个线程想要删除或添加到信号量的内部计数器的单位数。在acquire()、acquireUninterruptibly()和tryAcquire()方法的情况下,如果这个计数器的值小于这个值,线程将被阻塞,直到计数器达到这个值或更大的值。
另请参阅
-
第三章中的控制对资源的并发访问食谱,线程同步工具
-
第八章中的监视锁接口食谱,测试并发应用
-
第二章中的修改锁公平性食谱,基本线程同步
等待多个并发事件
Java 并发 API 提供了一个类,允许一个或多个线程等待一组操作完成。这就是CountDownLatch类。这个类用一个整数数初始化,这个整数是线程要等待的操作数。当一个线程想要等待这些操作的执行时,它使用await()方法。这个方法使线程进入睡眠状态,直到操作完成。当其中一个操作完成时,它使用countDown()方法来减少CountDownLatch类的内部计数器。当计数器到达0时,类唤醒所有在await()方法中睡眠的线程。
在这个食谱中,您将学习如何使用CountDownLatch类实现视频会议系统。视频会议系统将等待所有参与者到达后才开始。
准备工作
这个食谱的例子是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Videoconference的类,并指定其实现Runnable接口。该类将实现视频会议系统。
public class Videoconference implements Runnable{
- 声明一个名为
controller的CountDownLatch对象。
private final CountDownLatch controller;
- 实现初始化
CountDownLatch属性的类的构造函数。Videoconference类将等待作为参数接收到的参与者数量的到达。
public Videoconference(int number) {
controller=new CountDownLatch(number);
}
- 实现
arrive()方法。每次参与者到达视频会议时,将调用此方法。它接收一个名为name的String类型的参数。
public void arrive(String name){
- 首先,它使用接收到的参数编写一条消息。
System.out.printf("%s has arrived.",name);
- 然后,它调用
CountDownLatch对象的countDown()方法。
controller.countDown();
- 最后,使用
CountDownLatch对象的getCount()方法编写另一条消息,指示到达的参与者数量。
System.out.printf("VideoConference: Waiting for %d participants.\n",controller.getCount());
- 实现视频会议系统的主方法。这是每个
Runnable对象必须具有的run()方法。
@Override
public void run() {
- 首先,使用
getCount()方法编写一条消息,指示视频会议中的参与者数量。
System.out.printf("VideoConference: Initialization: %d participants.\n",controller.getCount());
- 然后,使用
await()方法等待所有参与者。由于此方法可能引发InterruptedException异常,因此必须包含处理它的代码。
try {
controller.await();
- 最后,编写一条消息,指示所有参与者都已到达。
System.out.printf("VideoConference: All the participants have come\n");
System.out.printf("VideoConference: Let's start...\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
- 创建
Participant类并指定其实现Runnable接口。该类代表视频会议中的每个参与者。
public class Participant implements Runnable {
- 声明一个名为
conference的私有Videoconference属性。
private Videoconference conference;
- 声明一个名为
name的私有String属性。
private String name;
- 实现初始化两个属性的类的构造函数。
public Participant(Videoconference conference, String name) {
this.conference=conference;
this.name=name;
}
- 实现参与者的
run()方法。
@Override
public void run() {
- 首先,让线程休眠一段随机时间。
long duration=(long)(Math.random()*10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 然后,使用
Videoconference对象的arrive()方法指示该参与者的到达。
conference.arrive(name);
- 最后,通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个名为
conference的Videoconference对象,等待 10 个参与者。
Videoconference conference=new Videoconference(10);
- 创建
Thread来运行此Videoconference对象并启动它。
Thread threadConference=new Thread(conference);
threadConference.start();
- 创建 10 个
Participant对象,一个Thread对象来运行每个参与者,并启动所有线程。
for (int i=0; i<10; i++){
Participant p=new Participant(conference, "Participant "+i);
Thread t=new Thread(p);
t.start();
}
它的工作原理...
CountDownLatch类有三个基本元素:
-
确定
CountDownLatch类等待多少个事件的初始化值 -
await()方法由等待所有事件完成的线程调用 -
countDown()方法由事件在完成执行时调用
当创建一个CountDownLatch对象时,对象使用构造函数的参数来初始化内部计数器。每次线程调用countDown()方法时,CountDownLatch对象将内部计数器减少一个单位。当内部计数器达到0时,CountDownLatch对象唤醒所有在await()方法中等待的线程。
无法重新初始化CountDownLatch对象的内部计数器或修改其值。一旦计数器被初始化,您可以使用的唯一方法来修改其值是前面解释的countDown()方法。当计数器达到0时,对await()方法的所有调用立即返回,并且对countDown()方法的所有后续调用都没有效果。
与其他同步方法相比,有一些不同之处,如下所示:
-
CountDownLatch机制不用于保护共享资源或临界区。它用于将一个或多个线程与执行各种任务同步。 -
它只允许一次使用。正如我们之前解释的,一旦
CountDownLatch的计数器达到0,对其方法的所有调用都没有效果。如果要再次进行相同的同步,必须创建一个新对象。
以下屏幕截图显示了示例执行的输出:

您可以看到最后的参与者到达,一旦内部计数器到达0,CountDownLatch对象会唤醒Videoconference对象,写入指示视频会议应该开始的消息。
还有更多...
CountDownLatch类有另一个版本的await()方法,如下所示:
await(long``time,``TimeUnit``unit): 线程将睡眠,直到被中断;CountDownLatch的内部计数器到达0或指定的时间过去。TimeUnit类是一个枚举,包含以下常量:DAYS,HOURS,MICROSECONDS,MILLISECONDS,MINUTES,NANOSECONDS, 和SECONDS。
在一个共同点同步任务
Java 并发 API 提供了一个同步工具,允许在确定点同步两个或多个线程。这就是CyclicBarrier类。这个类类似于本章中等待多个并发事件一节中解释的CountDownLatch类,但有一些不同之处,使它成为一个更强大的类。
CyclicBarrier类用一个整数初始化,这个整数是将在确定点同步的线程数。当其中一个线程到达确定点时,它调用await()方法等待其他线程。当线程调用该方法时,CyclicBarrier类会阻塞正在睡眠的线程,直到其他线程到达。当最后一个线程调用CyclicBarrier类的await()方法时,它会唤醒所有等待的线程并继续执行任务。
CyclicBarrier类的一个有趣的优势是,您可以将一个额外的Runnable对象作为初始化参数传递给它,当所有线程到达共同点时,CyclicBarrier类会执行这个对象作为一个线程。这个特性使得这个类适合使用分治编程技术并行化任务。
在这个示例中,您将学习如何使用CyclicBarrier类来同步一组线程到一个确定的点。您还将使用一个Runnable对象,在所有线程到达该点后执行。在这个示例中,您将在一个数字矩阵中查找一个数字。矩阵将被分成子集(使用分治技术),因此每个线程将在一个子集中查找数字。一旦所有线程完成了它们的工作,最终任务将统一它们的结果。
准备开始
这个示例已经在 Eclipse IDE 中实现。如果您使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 我们将通过实现两个辅助类来开始这个示例。首先,创建一个名为
MatrixMock的类。这个类将生成一个随机的数字矩阵,数字在 1 到 10 之间,线程将在其中查找一个数字。
public class MatrixMock {
- 声明一个名为
data的private``int矩阵。
private int data[][];
- 实现类的构造函数。这个构造函数将接收矩阵的行数、每行的长度和要查找的数字作为参数。所有三个参数的类型都是
int。
public MatrixMock(int size, int length, int number){
- 初始化构造函数中使用的变量和对象。
int counter=0;
data=new int[size][length];
Random random=new Random();
- 用随机数填充矩阵。每次生成一个数字,都要与要查找的数字进行比较。如果它们相等,就增加计数器。
for (int i=0; i<size; i++) {
for (int j=0; j<length; j++){
data[i][j]=random.nextInt(10);
if (data[i][j]==number){
counter++;
}
}
}
- 最后,在控制台打印一条消息,显示在生成的矩阵中要查找的数字的出现次数。这条消息将用于检查线程是否得到了正确的结果。
System.out.printf("Mock: There are %d ocurrences of number in generated data.\n",counter,number);
- 实现
getRow()方法。这个方法接收一个矩阵中的行号,并返回该行(如果存在),如果不存在则返回null。
public int[] getRow(int row){
if ((row>=0)&&(row<data.length)){
return data[row];
}
return null;
}
- 现在,实现一个名为
Results的类。这个类将在一个数组中存储矩阵每一行中搜索到的数字的出现次数。
public class Results {
- 声明一个名为
data的私有int数组。
private int data[];
- 实现类的构造函数。这个构造函数接收一个整数参数,表示数组的元素个数。
public Results(int size){
data=new int[size];
}
- 实现
setData()方法。这个方法接收一个数组中的位置和一个值作为参数,并确定数组中该位置的值。
public void setData(int position, int value){
data[position]=value;
}
- 实现
getData()方法。这个方法返回结果数组的数组。
public int[] getData(){
return data;
}
- 现在你有了辅助类,是时候实现线程了。首先,实现
Searcher类。这个类将在随机数字矩阵的确定行中查找一个数字。创建一个名为Searcher的类,并指定它实现Runnable接口。
public class Searcher implements Runnable {
- 声明两个私有的
int属性,名为firstRow和lastRow。这两个属性将确定这个对象将在哪些行中查找。
private int firstRow;
private int lastRow;
- 声明一个名为
mock的私有MatrixMock属性。
private MatrixMock mock;
- 声明一个名为
results的私有Results属性。
private Results results;
- 声明一个名为
number的私有int属性,将存储我们要查找的数字。
private int number;
- 声明一个名为
barrier的CyclicBarrier对象。
private final CyclicBarrier barrier;
- 实现类的构造函数,初始化之前声明的所有属性。
public Searcher(int firstRow, int lastRow, NumberMock mock, Results results, int number, CyclicBarrier barrier){
this.firstRow=firstRow;
this.lastRow=lastRow;
this.mock=mock;
this.results=results;
this.number=number;
this.barrier=barrier;
}
- 实现
run()方法,用于搜索数字。它使用一个名为counter的内部变量,用于存储每一行中数字的出现次数。
@Override
public void run() {
int counter;
- 在控制台中打印一个消息,指定给这个对象分配的行。
System.out.printf("%s: Processing lines from %d to %d.\n",Thread.currentThread().getName(),firstRow,lastRow);
- 处理分配给这个线程的所有行。对于每一行,计算你要搜索的数字出现的次数,并将这个数字存储在
Results对象的相应位置。
for (int i=firstRow; i<lastRow; i++){
int row[]=mock.getRow(i);
counter=0;
for (int j=0; j<row.length; j++){
if (row[j]==number){
counter++;
}
}
results.setData(i, counter);
}
- 在控制台中打印一条消息,指示这个对象已经完成了搜索。
System.out.printf("%s: Lines processed.\n",Thread.currentThread().getName());
- 调用
CyclicBarrier对象的await()方法,并添加必要的代码来处理这个方法可能抛出的InterruptedException和BrokenBarrierException异常。
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
- 现在,实现计算矩阵中数字总出现次数的类。它使用存储矩阵每一行中数字出现次数的
Results对象来进行计算。创建一个名为Grouper的类,并指定它实现Runnable接口。
public class Grouper implements Runnable {
- 声明一个名为
results的私有Results属性。
private Results results;
- 实现类的构造函数,初始化
Results属性。
public Grouper(Results results){
this.results=results;
}
- 实现
run()方法,该方法将计算结果数组中数字的总出现次数。
@Override
public void run() {
- 声明一个
int变量,并在控制台中写入一条消息,指示进程的开始。
int finalResult=0;
System.out.printf("Grouper: Processing results...\n");
- 使用
results对象的getData()方法获取每一行中数字的出现次数。然后,处理数组的所有元素,并将它们的值加到finalResult变量中。
int data[]=results.getData();
for (int number:data){
finalResult+=number;
}
- 在控制台中打印结果。
System.out.printf("Grouper: Total result: %d.\n",finalResult);
- 最后,通过创建一个名为
Main的类并添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 声明并初始化五个常量,用于存储应用程序的参数。
final int ROWS=10000;
final int NUMBERS=1000;
final int SEARCH=5;
final int PARTICIPANTS=5;
final int LINES_PARTICIPANT=2000;
- 创建一个名为
mock的MatrixMock对象。它将有 10000 行,每行 1000 个元素。现在,你要搜索数字 5。
MatrixMock mock=new MatrixMock(ROWS, NUMBERS,SEARCH);
- 创建一个名为
results的Results对象。它将有 10000 个元素。
Results results=new Results(ROWS);
- 创建一个名为
grouper的Grouper对象。
Grouper grouper=new Grouper(results);
- 创建一个名为
barrier的CyclicBarrier对象。这个对象将等待五个线程。当这个线程完成时,它将执行之前创建的Grouper对象。
CyclicBarrier barrier=new CyclicBarrier(PARTICIPANTS,grouper);
- 创建五个
Searcher对象,五个线程来执行它们,并启动这五个线程。
Searcher searchers[]=new Searcher[PARTICIPANTS];
for (int i=0; i<PARTICIPANTS; i++){
searchers[i]=new Searcher(i*LINES_PARTICIPANT, (i*LINES_PARTICIPANT)+LINES_PARTICIPANT, mock, results, 5,barrier);
Thread thread=new Thread(searchers[i]);
thread.start();
}
System.out.printf("Main: The main thread has finished.\n");
它是如何工作的...
以下截图显示了此示例执行的结果:

在示例中解决的问题很简单。我们有一个随机整数数字的大矩阵,你想知道这个矩阵中某个数字出现的总次数。为了获得更好的性能,我们使用分而治之的技术。我们将矩阵分成五个子集,并使用一个线程在每个子集中查找数字。这些线程是Searcher类的对象。
我们使用CyclicBarrier对象来同步五个线程的完成,并执行Grouper任务来处理部分结果,并计算最终结果。
正如我们之前提到的,CyclicBarrier类有一个内部计数器来控制有多少线程必须到达同步点。每当一个线程到达同步点时,它调用await()方法来通知CyclicBarrier对象已经到达了同步点。CyclicBarrier会让线程休眠,直到所有线程都到达同步点。
当所有线程都到达同步点时,CyclicBarrier对象会唤醒所有在await()方法中等待的线程,并且可以创建一个新的线程来执行在CyclicBarrier构造函数中传递的Runnable对象(在我们的例子中是Grouper对象)来执行额外的任务。
还有更多...
CyclicBarrier类还有另一个版本的await()方法:
await(long``time,``TimeUnit``unit):线程将休眠,直到被中断;CyclicBarrier的内部计数器到达0或指定的时间过去。TimeUnit类是一个枚举,包含以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
该类还提供了getNumberWaiting()方法,返回在await()方法中被阻塞的线程数,以及getParties()方法,返回将与CyclicBarrier同步的任务数。
重置 CyclicBarrier 对象
CyclicBarrier类与CountDownLatch类有一些共同点,但也有一些不同之处。其中最重要的一个区别是CyclicBarrier对象可以重置为其初始状态,将其内部计数器分配给其初始化时的值。
可以使用CyclicBarrier类的reset()方法来执行此重置操作。当发生这种情况时,所有在await()方法中等待的线程都会收到BrokenBarrierException异常。在本配方中的示例中,这个异常通过打印堆栈跟踪来处理,但在更复杂的应用程序中,它可能执行其他操作,比如重新启动它们的执行或在中断点恢复它们的操作。
破碎的 CyclicBarrier 对象
CyclicBarrier对象可以处于特殊状态,用broken表示。当有多个线程在await()方法中等待,其中一个被中断时,这个线程会收到InterruptedException异常,但其他等待的线程会收到BrokenBarrierException异常,CyclicBarrier会处于破碎状态。
CyclicBarrier类提供了isBroken()方法,如果对象处于破碎状态,则返回true;否则返回false。
另请参阅
- 在第三章的等待多个并发事件配方中,线程同步工具
运行并发分阶段任务
Java 并发 API 提供的最复杂和强大的功能之一是使用Phaser类执行并发分阶段任务的能力。当我们有一些并发任务分为步骤时,这种机制非常有用。Phaser类为我们提供了在每个步骤结束时同步线程的机制,因此在所有线程完成第一步之前,没有线程开始第二步。
与其他同步工具一样,我们必须使用参与同步操作的任务数量初始化Phaser类,但是我们可以通过增加或减少这个数量来动态修改它。
在这个示例中,你将学习如何使用Phaser类来同步三个并发任务。这三个任务分别在三个不同的文件夹及其子文件夹中寻找最近 24 小时内修改的扩展名为.log的文件。这个任务分为三个步骤:
-
获取分配文件夹及其子文件夹中扩展名为
.log的文件列表。 -
通过删除 24 小时前修改的文件来过滤第一步创建的列表。
-
在控制台打印结果。
在步骤 1 和 2 结束时,我们检查列表是否有任何元素。如果没有任何元素,线程将结束执行并从phaser类中被删除。
准备工作
这个示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他类似 NetBeans 的 IDE,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
FileSearch的类,并指定它实现Runnable接口。这个类实现了在文件夹及其子文件夹中搜索特定扩展名的文件,并且这些文件在最近 24 小时内被修改的操作。
public class FileSearch implements Runnable {
- 声明一个私有的
String属性来存储搜索操作将开始的文件夹。
private String initPath;
- 声明另一个私有的
String属性来存储我们要查找的文件的扩展名。
private String end;
- 声明一个私有的
List属性来存储具有所需特征的文件的完整路径。
private List<String> results;
- 最后,声明一个私有的
Phaser属性来控制任务不同阶段的同步。
private Phaser phaser;
- 实现类的构造函数,初始化类的属性。它的参数是初始文件夹的完整路径、文件的扩展名和 phaser。
public FileSearch(String initPath, String end, Phaser phaser) {
this.initPath = initPath;
this.end = end;
this.phaser=phaser;
results=new ArrayList<>();
}
- 现在,你需要实现一些辅助方法,这些方法将被
run()方法使用。第一个是directoryProcess()方法。它接收一个File对象作为参数,并处理它的所有文件和子文件夹。对于每个文件夹,该方法将通过传递文件夹作为参数进行递归调用。对于每个文件,该方法将调用fileProcess()方法:
private void directoryProcess(File file) {
File list[] = file.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
directoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
}
- 现在,实现
fileProcess()方法。它接收一个File对象作为参数,并检查它的扩展名是否与我们要查找的扩展名相等。如果相等,这个方法将文件的绝对路径添加到结果列表中。
private void fileProcess(File file) {
if (file.getName().endsWith(end)) {
results.add(file.getAbsolutePath());
}
}
- 现在,实现
filterResults()方法。它不接收任何参数,并过滤在第一阶段获取的文件列表,删除修改时间超过 24 小时的文件。首先,创建一个新的空列表并获取当前日期。
private void filterResults() {
List<String> newResults=new ArrayList<>();
long actualDate=new Date().getTime();
- 然后,遍历结果列表的所有元素。对于结果列表中的每个路径,为该文件创建一个
File对象,并获取它的最后修改日期。
for (int i=0; i<results.size(); i++){
File file=new File(results.get(i));
long fileDate=file.lastModified();
- 然后,将该日期与当前日期进行比较,如果差值小于一天,则将文件的完整路径添加到新的结果列表中。
if (actualDate-fileDate< TimeUnit.MILLISECONDS.convert(1,TimeUnit.DAYS)){
newResults.add(results.get(i));
}
}
- 最后,将旧的结果列表更改为新的列表。
results=newResults;
}
- 现在,实现
checkResults()方法。这个方法将在第一阶段和第二阶段结束时被调用,它将检查结果列表是否为空。这个方法没有任何参数。
private boolean checkResults() {
- 首先,检查结果列表的大小。如果为
0,对象会向控制台写入一条消息,指示这种情况,然后调用Phaser对象的arriveAndDeregister()方法,通知它该线程已完成当前阶段,并离开阶段操作。
if (results.isEmpty()) {
System.out.printf("%s: Phase %d: 0 results.\n",Thread.currentThread().getName(),phaser.getPhase());
System.out.printf("%s: Phase %d: End.\n",Thread.currentThread().getName(),phaser.getPhase());
phaser.arriveAndDeregister();
return false;
- 否则,如果结果列表有元素,对象会向控制台写入一条消息,指示这种情况,然后调用
Phaser对象的arriveAndAwaitAdvance()方法,通知它该线程已完成当前阶段,并希望被阻塞,直到所有参与的线程完成当前阶段。
} else {
System.out.printf("%s: Phase %d: %d results.\n",Thread.currentThread().getName(),phaser.getPhase(),results.size());
phaser.arriveAndAwaitAdvance();
return true;
}
}
- 最后一个辅助方法是
showInfo()方法,它将结果列表的元素打印到控制台。
private void showInfo() {
for (int i=0; i<results.size(); i++){
File file=new File(results.get(i));
System.out.printf("%s: %s\n",Thread.currentThread().getName(),file.getAbsolutePath());
}
phaser.arriveAndAwaitAdvance();
}
- 现在,是时候实现
run()方法了,该方法使用前面描述的辅助方法和Phaser对象来控制阶段之间的变化。首先,调用phaser对象的arriveAndAwaitAdvance()方法。在创建所有线程之前,搜索不会开始。
@Override
public void run() {
phaser.arriveAndAwaitAdvance();
- 然后,向控制台写入一条消息,指示搜索任务的开始。
System.out.printf("%s: Starting.\n",Thread.currentThread().getName());
- 检查
initPath属性是否存储了一个文件夹的名称,并使用directoryProcess()方法在该文件夹及其所有子文件夹中查找指定扩展名的文件。
File file = new File(initPath);
if (file.isDirectory()) {
directoryProcess(file);
}
- 使用
checkResults()方法检查是否有任何结果。如果没有结果,则使用return关键字结束线程的执行。
if (!checkResults()){
return;
}
- 使用
filterResults()方法过滤结果列表。
filterResults();
- 再次使用
checkResults()方法检查是否有任何结果。如果没有结果,则使用return关键字结束线程的执行。
if (!checkResults()){
return;
}
- 使用
showInfo()方法将最终的结果列表打印到控制台,注销线程,并打印一条指示线程最终化的消息。
showInfo();
phaser.arriveAndDeregister();
System.out.printf("%s: Work completed.\n",Thread.currentThread().getName());
- 现在,通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个具有三个参与者的
Phaser对象。
Phaser phaser=new Phaser(3);
- 为三个不同的初始文件夹创建三个
FileSearch对象。查找扩展名为.log的文件。
FileSearch system=new FileSearch("C:\\Windows", "log", phaser);
FileSearch apps=
new FileSearch("C:\\Program Files","log",phaser);
FileSearch documents=
new FileSearch("C:\\Documents And Settings","log",phaser);
- 创建并启动一个线程来执行第一个
FileSearch对象。
Thread systemThread=new Thread(system,"System");
systemThread.start();
- 创建并启动一个线程来执行第二个
FileSearch对象。
Thread appsThread=new Thread(apps,"Apps");
appsThread.start();
- 创建并启动一个线程来执行第三个
FileSearch对象。
Thread documentsThread=new Thread(documents, "Documents");
documentsThread.start();
- 等待三个线程的最终化。
try {
systemThread.join();
appsThread.join();
documentsThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
isFinalized()方法写入Phaser对象的最终化标志的值。
System.out.println("Terminated: "+ phaser.isTerminated());
它是如何工作的...
程序开始创建一个Phaser对象,该对象将控制每个阶段结束时线程的同步。Phaser的构造函数接收参与者的数量作为参数。在我们的例子中,Phaser有三个参与者。这个数字告诉Phaser在Phaser改变阶段并唤醒正在睡眠的线程之前,有多少个线程必须执行arriveAndAwaitAdvance()方法。
一旦创建了Phaser,我们启动三个线程,执行三个不同的FileSearch对象。
注意
在这个例子中,我们使用 Windows 操作系统的路径。如果您使用另一个操作系统,请修改路径以适应您环境中现有的路径。
这个FileSearch对象的run()方法中的第一条指令是调用Phaser对象的arriveAndAwaitAdvance()方法。正如我们之前提到的,Phaser知道我们想要同步的线程数量。当一个线程调用这个方法时,Phaser减少了必须完成当前阶段的线程数量,并将该线程置于睡眠状态,直到所有剩余的线程完成此阶段。在run()方法的开始调用这个方法,使得FileSearch线程中的任何一个都不会开始工作,直到所有线程都被创建。
在第一阶段和第二阶段结束时,我们检查阶段是否生成了结果,结果列表是否有元素,否则阶段没有生成结果,列表为空。在第一种情况下,checkResults()方法调用arriveAndAwaitAdvance(),如前所述。在第二种情况下,如果列表为空,线程没有继续执行的意义,所以返回。但是你必须通知屏障将会少一个参与者。为此,我们使用了arriveAndDeregister()。这通知屏障,这个线程已经完成了当前阶段,但不会参与未来的阶段,所以屏障不需要等待它继续。
在showInfo()方法中实现的第三阶段结束时,调用了屏障的arriveAndAwaitAdvance()方法。通过这个调用,我们保证所有线程同时结束。当这个方法执行结束时,会调用屏障的arriveAndDeregister()方法。通过这个调用,我们取消注册屏障的线程,因此当所有线程结束时,屏障将没有参与者。
最后,main()方法等待三个线程的完成,并调用屏障的isTerminated()方法。当一个屏障没有参与者时,它进入所谓的终止状态,这个方法返回true。由于我们取消注册了屏障的所有线程,它将处于终止状态,这个调用将在控制台上打印true。
Phaser对象可以处于两种状态:
-
活跃:当
Phaser接受新参与者的注册并在每个阶段结束时进行同步时,Phaser进入这个状态。在这个状态下,Phaser的工作方式如本文所述。这个状态在 Java 并发 API 中没有提到。 -
终止:默认情况下,当所有
Phaser的参与者都被取消注册时,Phaser进入这个状态,所以Phaser没有参与者。更详细地说,当onAdvance()方法返回true值时,Phaser处于终止状态。如果你重写了这个方法,你可以改变默认行为。当Phaser处于这个状态时,同步方法arriveAndAwaitAdvance()会立即返回,不执行任何同步操作。
Phaser类的一个显著特点是,你不需要控制与屏障相关的方法中的任何异常。与其他同步工具不同,处于屏障中休眠的线程不会响应中断事件,也不会抛出InterruptedException异常。下面的还有更多部分中只有一个例外情况。
下面的截图显示了示例执行的结果:

它显示了执行的前两个阶段。你可以看到Apps线程在第二阶段结束时结束了执行,因为它的结果列表为空。当你执行示例时,你会看到一些线程在其他线程之前完成了一个阶段,但它们会等待所有线程完成一个阶段后才继续执行。
还有更多...
Phaser类提供了与阶段变化相关的其他方法。这些方法如下:
-
arrive(): 此方法通知屏障,一个参与者已经完成了当前阶段,但不需要等待其他参与者继续执行。要小心使用此方法,因为它不会与其他线程同步。 -
awaitAdvance(int``phase): 此方法将当前线程休眠,直到屏障的所有参与者完成屏障的当前阶段,如果我们传递的参数等于屏障的实际阶段。如果参数和屏障的实际阶段不相等,方法会立即返回。 -
awaitAdvanceInterruptibly(int phaser): 此方法与前面解释的方法相同,但如果在此方法中休眠的线程被中断,则会抛出InterruptedException异常。
在 Phaser 中注册参与者
当您创建Phaser对象时,您会指示该 phaser 将有多少参与者。但是Phaser类有两种方法来增加 phaser 的参与者数量。这些方法如下:
-
register(): 此方法向Phaser添加一个新的参与者。这个新的参与者将被视为未到达当前阶段。 -
bulkRegister(int Parties): 此方法向 phaser 添加指定数量的参与者。这些新参与者将被视为未到达当前阶段。
Phaser类提供的唯一方法来减少参与者数量是arriveAndDeregister()方法,该方法通知 phaser 线程已完成当前阶段,并且不希望继续进行分阶段操作。
强制终止 Phaser
当 phaser 没有参与者时,它进入由终止表示的状态。Phaser类提供了forceTermination()来改变 phaser 的状态,并使其独立于 phaser 中注册的参与者数量进入终止状态。当参与者中有一个出现错误情况时,强制终止 phaser 可能会有用。
当 phaser 处于终止状态时,awaitAdvance()和arriveAndAwaitAdvance()方法立即返回一个负数,而不是通常返回的正数。如果您知道您的 phaser 可能被终止,您应该验证这些方法的返回值,以了解 phaser 是否已终止。
另请参阅
- 在第八章中的监视 Phaser示例,测试并发应用程序*
控制并发分阶段任务中的相位变化
Phaser类提供了一个在 phaser 改变相位时执行的方法。这是onAdvance()方法。它接收两个参数:当前相位的编号和注册参与者的数量;它返回一个Boolean值,如果 phaser 继续执行,则返回false,如果 phaser 已完成并且必须进入终止状态,则返回true。
此方法的默认实现在注册的参与者数量为零时返回true,否则返回false。但是,如果您扩展Phaser类并覆盖此方法,则可以修改此行为。通常,当您必须在从一个阶段前进到下一个阶段时执行一些操作时,您会对此感兴趣。
在这个示例中,您将学习如何控制实现自己版本的Phaser类中的相位变化,该类覆盖了onAdvance()方法以在每个相位变化时执行一些操作。您将实现一个考试的模拟,其中将有一些学生需要完成三个练习。所有学生都必须在进行下一个练习之前完成一个练习。
准备就绪
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyPhaser的类,并指定它从Phaser类扩展。
public class MyPhaser extends Phaser {
- 覆盖
onAdvance()方法。根据阶段属性的值,我们调用不同的辅助方法。如果阶段等于零,你必须调用studentsArrived()方法。如果阶段等于一,你必须调用finishFirstExercise()方法。如果阶段等于二,你必须调用finishSecondExercise()方法,如果阶段等于三,你必须调用finishExam()方法。否则,我们返回true值以指示 phaser 已经终止。
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
return studentsArrived();
case 1:
return finishFirstExercise();
case 2:
return finishSecondExercise();
case 3:
return finishExam();
default:
return true;
}
}
- 实现辅助方法
studentsArrived()。它在控制台上写入两条日志消息,并返回false值以指示 phaser 继续执行。
private boolean studentsArrived() {
System.out.printf("Phaser: The exam are going to start. The students are ready.\n");
System.out.printf("Phaser: We have %d students.\n",getRegisteredParties());
return false;
}
- 实现辅助方法
finishFirstExercise()。它在控制台上写入两条消息,并返回false值以指示 phaser 继续执行。
private boolean finishFirstExercise() {
System.out.printf("Phaser: All the students have finished the first exercise.\n");
System.out.printf("Phaser: It's time for the second one.\n");
return false;
}
- 实现辅助方法
finishSecondExercise()。它在控制台上写入两条消息,并返回false值以指示 phaser 继续执行。
private boolean finishSecondExercise() {
System.out.printf("Phaser: All the students have finished the second exercise.\n");
System.out.printf("Phaser: It's time for the third one.\n");
return false;
}
- 实现辅助方法
finishExam()。它在控制台上写入两条消息,并返回true值以指示 phaser 已经完成了它的工作。
private boolean finishExam() {
System.out.printf("Phaser: All the students have finished the exam.\n");
System.out.printf("Phaser: Thank you for your time.\n");
return true;
}
- 创建一个名为
Student的类,并指定它实现Runnable接口。这个类将模拟考试的学生。
public class Student implements Runnable {
- 声明一个名为
phaser的Phaser对象。
private Phaser phaser;
- 实现初始化
Phaser对象的类的构造函数。
public Student(Phaser phaser) {
this.phaser=phaser;
}
- 实现将模拟考试的
run()方法。
@Override
public void run() {
- 首先,该方法在控制台中写入一条消息,指示该学生已经到达考试,并调用 phaser 的
arriveAndAwaitAdvance()方法等待其他线程完成第一个练习。
System.out.printf("%s: Has arrived to do the exam. %s\n",Thread.currentThread().getName(),new Date());
phaser.arriveAndAwaitAdvance();
- 然后,在控制台上写一条消息,调用私有的
doExercise1()方法来模拟考试的第一个练习,再在控制台上写一条消息,并调用 phaser 的arriveAndAwaitAdvance()方法等待其他学生完成第一个练习。
System.out.printf("%s: Is going to do the first exercise. %s\n",Thread.currentThread().getName(),new Date());
doExercise1();
System.out.printf("%s: Has done the first exercise. %s\n",Thread.currentThread().getName(),new Date());
phaser.arriveAndAwaitAdvance();
- 为第二个练习和第三个练习实现相同的代码。
System.out.printf("%s: Is going to do the second exercise. %s\n",Thread.currentThread().getName(),new Date());
doExercise2();
System.out.printf("%s: Has done the second exercise. %s\n",Thread.currentThread().getName(),new Date());
phaser.arriveAndAwaitAdvance();
System.out.printf("%s: Is going to do the third exercise. %s\n",Thread.currentThread().getName(),new Date());
doExercise3();
System.out.printf("%s: Has finished the exam. %s\n",Thread.currentThread().getName(),new Date());
phaser.arriveAndAwaitAdvance();
- 实现辅助方法
doExercise1()。这个方法让线程睡眠一段随机时间。
private void doExercise1() {
try {
long duration=(long)(Math.random()*10);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 实现辅助方法
doExercise2()。这个方法让线程睡眠一段随机时间。
private void doExercise2() {
try {
long duration=(long)(Math.random()*10);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 实现辅助方法
doExercise3()。这个方法让线程睡眠一段随机时间。
private void doExercise3() {
try {
long duration=(long)(Math.random()*10);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
MyPhaser对象。
MyPhaser phaser=new MyPhaser();
- 创建五个
Student对象,并使用register()方法在 phaser 中注册它们。
Student students[]=new Student[5];
for (int i=0; i<students.length; i++){
students[i]=new Student(phaser);
phaser.register();
}
- 创建五个线程来运行
students并启动它们。
Thread threads[]=new Thread[students.length];
for (int i=0; i<students.length; i++){
threads[i]=new Thread(students[i],"Student "+i);
threads[i].start();
}
- 等待五个线程的完成。
for (int i=0; i<threads.length; i++){
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 写一条消息来显示 phaser 处于终止状态,使用
isTerminated()方法。
System.out.printf("Main: The phaser has finished: %s.\n",phaser.isTerminated());
它是如何工作的...
这个练习模拟了一个有三个练习的考试的实现。所有学生都必须在开始下一个练习之前完成一个练习。为了实现这个同步要求,我们使用了Phaser类,但你已经实现了自己的 phaser,扩展了原始类以覆盖onAdvance()方法。
这个方法在 phaser 在进行阶段改变之前和在唤醒所有在arriveAndAwaitAdvance()方法中睡眠的线程之前被 phaser 调用。这个方法接收实际阶段的编号作为参数,其中0是第一个阶段的编号,注册参与者的数量。最有用的参数是实际阶段。如果根据实际阶段执行不同的操作,你必须使用一个替代结构(if/else或switch)来选择你想要执行的操作。在这个例子中,我们使用了一个switch结构来选择每个阶段变化的不同方法。
onAdvance()方法返回一个Boolean值,指示 phaser 是否已终止。如果 phaser 返回false值,则表示它尚未终止,因此线程将继续执行其他阶段。如果 phaser 返回true值,则 phaser 仍然唤醒挂起的线程,但将 phaser 移动到终止状态,因此对 phaser 的任何方法的未来调用都将立即返回,并且isTerminated()方法返回true值。
在Core类中,当您创建MyPhaser对象时,您没有指定 phaser 中参与者的数量。您为每个创建的Student对象调用register()方法来注册 phaser 中的参与者。这种调用并不建立Student对象或执行它的线程与 phaser 之间的关系。实际上,phaser 中的参与者数量只是一个数字。phaser 和参与者之间没有关系。
以下屏幕截图显示了此示例的执行结果:

您可以看到学生们在不同时间完成第一个练习。当所有人都完成了那个练习时,phaser 调用onAdvance()方法在控制台中写入日志消息,然后所有学生同时开始第二个练习。
另请参阅
-
第三章中的运行并发分阶段任务食谱,线程同步实用程序
-
第八章中的监视 Phaser食谱,测试并发应用程序
在并发任务之间交换数据
Java 并发 API 提供了一个同步实用程序,允许在两个并发任务之间交换数据。更详细地说,Exchanger类允许在两个线程之间定义同步点。当两个线程到达此点时,它们交换一个数据结构,因此第一个线程的数据结构传递给第二个线程,第二个线程的数据结构传递给第一个线程。
这个类在类似生产者-消费者问题的情况下可能非常有用。这是一个经典的并发问题,其中有一个共同的数据缓冲区,一个或多个数据生产者和一个或多个数据消费者。由于Exchanger类只同步两个线程,所以如果你有一个只有一个生产者和一个消费者的生产者-消费者问题,你可以使用它。
在这个示例中,您将学习如何使用Exchanger类来解决只有一个生产者和一个消费者的生产者-消费者问题。
准备工作
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他类似 NetBeans 的 IDE,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 首先,让我们开始实现生产者。创建一个名为
Producer的类,并指定它实现Runnable接口。
public class Producer implements Runnable {
- 声明一个名为
buffer的List<String>对象。这将是生产者与消费者进行交换的数据结构。
private List<String> buffer;
- 声明一个名为
exchanger的Exchanger<List<String>>对象。这将是用于同步生产者和消费者的交换对象。
private final Exchanger<List<String>> exchanger;
- 实现初始化两个属性的类的构造函数。
public Producer (List<String> buffer, Exchanger<List<String>> exchanger){
this.buffer=buffer;
this.exchanger=exchanger;
}
- 实现
run()方法。在其中,实现 10 个交换周期。
@Override
public void run() {
int cycle=1;
for (int i=0; i<10; i++){
System.out.printf("Producer: Cycle %d\n",cycle);
- 在每个循环中,向缓冲区添加 10 个字符串。
for (int j=0; j<10; j++){
String message="Event "+((i*10)+j);
System.out.printf("Producer: %s\n",message);
buffer.add(message);
}
- 调用
exchange()方法与消费者交换数据。由于这个方法可能抛出InterruptedException异常,你必须添加处理它的代码。
try {
buffer=exchanger.exchange(buffer);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Producer: "+buffer.size());
cycle++;
}
- 现在,让我们实现消费者。创建一个名为
Consumer的类,并指定它实现Runnable接口。
public class Consumer implements Runnable {
- 声明一个名为
buffer的List<String>对象。这将是生产者与消费者进行交换的数据结构。
private List<String> buffer;
- 声明一个名为
exchanger的Exchanger<List<String>>对象。这将是用于同步生产者和消费者的交换对象。
private final Exchanger<List<String>> exchanger;
- 实现初始化两个属性的类的构造函数。
public Consumer(List<String> buffer, Exchanger<List<String>> exchanger){
this.buffer=buffer;
this.exchanger=exchanger;
}
- 实现
run()方法。在其中,实现 10 个交换周期。
@Override
public void run() {
int cycle=1;
for (int i=0; i<10; i++){
System.out.printf("Consumer: Cycle %d\n",cycle);
- 在每个周期中,首先调用
exchange()方法与生产者同步。消费者需要数据来消费。由于此方法可能抛出InterruptedException异常,因此您必须添加处理它的代码。
try {
buffer=exchanger.exchange(buffer);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 将生产者发送到其缓冲区的 10 个字符串写入控制台并从缓冲区中删除它们,使其保持为空。
System.out.println("Consumer: "+buffer.size());
for (int j=0; j<10; j++){
String message=buffer.get(0);
System.out.println("Consumer: "+message);
buffer.remove(0);
}
cycle++;
}
- 现在,通过创建一个名为
Core的类并为其添加main()方法来实现示例的主类。
public class Core {
public static void main(String[] args) {
- 创建生产者和消费者将使用的两个缓冲区。
List<String> buffer1=new ArrayList<>();
List<String> buffer2=new ArrayList<>();
- 创建
Exchanger对象,用于同步生产者和消费者。
Exchanger<List<String>> exchanger=new Exchanger<>();
- 创建
Producer对象和Consumer对象。
Producer producer=new Producer(buffer1, exchanger);
Consumer consumer=new Consumer(buffer2, exchanger);
- 创建线程来执行生产者和消费者,并启动线程。
Thread threadProducer=new Thread(producer);
Thread threadConsumer=new Thread(consumer);
threadProducer.start();
threadConsumer.start();
它是如何工作的...
消费者从一个空缓冲区开始,并调用Exchanger与生产者同步。它需要数据来消费。生产者从一个空缓冲区开始执行。它创建 10 个字符串,将其存储在缓冲区中,并使用交换器与消费者同步。
此时,生产者和消费者两个线程都在Exchanger中,并且它会更改数据结构,因此当消费者从exchange()方法返回时,它将拥有一个包含 10 个字符串的缓冲区。当生产者从exchange()方法返回时,它将有一个空的缓冲区再次填充。这个操作将重复 10 次。
如果执行示例,您将看到生产者和消费者如何同时执行其工作,以及两个对象如何在每一步中交换它们的缓冲区。与其他同步工具一样,调用exchange()方法的第一个线程将被放到睡眠状态,直到其他线程到达。
还有更多...
Exchanger类有另一个版本的交换方法:exchange(V data, long time, TimeUnit unit),其中V是在Phaser声明中使用的类型(在我们的例子中是List<String>)。线程将休眠,直到被中断,另一个线程到达,或者指定的时间过去。TimeUnit类是一个枚举,具有以下常量:DAYS,HOURS,MICROSECONDS,MILLISECONDS,MINUTES,NANOSECONDS和SECONDS。
第四章:线程执行器
在本章中,我们将涵盖:
-
创建线程执行器
-
创建固定大小的线程执行器
-
在执行器中执行返回结果的任务
-
运行多个任务并处理第一个结果
-
运行多个任务并处理所有结果
-
在执行器中延迟运行任务
-
在执行器中定期运行任务
-
取消执行器中的任务
-
在执行器中控制任务的完成
-
在执行器中分离任务的启动和结果的处理
-
控制执行器的被拒绝任务
介绍
通常,在 Java 中开发简单的并发编程应用程序时,您会创建一些Runnable对象,然后创建相应的Thread对象来执行它们。如果必须开发运行大量并发任务的程序,这种方法有以下缺点:
-
您必须实现与
Thread对象管理相关的所有代码信息(创建、结束、获取结果)。 -
为每个任务创建一个
Thread对象。如果必须执行大量任务,这可能会影响应用程序的吞吐量。 -
您必须有效地控制和管理计算机的资源。如果创建了太多线程,可能会使系统饱和。
自 Java 5 以来,Java 并发 API 提供了一个旨在解决问题的机制。这个机制称为Executor 框架,围绕着Executor接口、它的子接口ExecutorService以及实现了这两个接口的ThreadPoolExecutor类。
这种机制将任务的创建和执行分开。有了执行器,您只需实现Runnable对象并将它们发送到执行器。执行器负责它们的执行、实例化和使用必要的线程运行。但它不仅如此,还使用线程池来提高性能。当您将任务发送到执行器时,它会尝试使用池化线程来执行此任务,以避免不断产生线程。
执行器框架的另一个重要优势是Callable接口。它类似于Runnable接口,但提供了两个改进,如下所示:
-
该接口的主要方法名为
call(),可能会返回一个结果。 -
当您将
Callable对象发送到执行器时,您会得到一个实现Future接口的对象。您可以使用此对象来控制Callable对象的状态和结果。
本章介绍了 11 个示例,向您展示如何使用 Executor 框架使用 Java 并发 API 提供的类和其他变体。
创建线程执行器
使用 Executor 框架的第一步是创建ThreadPoolExecutor类的对象。您可以使用该类提供的四个构造函数,或者使用一个名为Executors的工厂类来创建ThreadPoolExecutor。一旦您有了执行器,就可以发送Runnable或Callable对象进行执行。
在这个示例中,您将学习如何实现这两个操作,模拟一个从各个客户端接收请求的 Web 服务器。
准备工作
您应该阅读第一章中的创建和运行线程示例,以了解 Java 中线程创建的基本机制。您可以比较这两种机制,并根据问题选择最佳的机制。
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 首先,您必须实现将由服务器执行的任务。创建一个名为
Task的类,实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
initDate的Date属性,用于存储任务的创建日期,以及一个名为name的String属性,用于存储任务的名称。
private Date initDate;
private String name;
- 实现初始化两个属性的类的构造函数。
public Task(String name){
initDate=new Date();
this.name=name;
}
- 实现
run()方法。
@Override
public void run() {
- 首先,将
initDate属性和实际日期写入控制台,即任务的开始日期。
System.out.printf("%s: Task %s: Created on: %s\n",Thread.currentThread().getName(),name,initDate);
System.out.printf("%s: Task %s: Started on: %s\n",Thread.currentThread().getName(),name,new Date());
- 然后,让任务随机休眠一段时间。
try {
Long duration=(long)(Math.random()*10);
System.out.printf("%s: Task %s: Doing a task during %d seconds\n",Thread.currentThread().getName(),name,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 最后,将任务的完成日期写入控制台。
System.out.printf("%s: Task %s: Finished on: %s\n",Thread.currentThread().getName(),name,new Date());
- 现在,实现
Server类,它将使用执行器执行接收到的每个任务。创建一个名为Server的类。
public class Server {
- 声明一个名为
executor的ThreadPoolExecutor属性。
private ThreadPoolExecutor executor;
- 实现初始化
ThreadPoolExecutor对象的类的构造函数,使用Executors类。
public Server(){
executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
}
- 实现
executeTask()方法。它接收一个Task对象作为参数,并将其发送到执行器。首先,在控制台上写入一条消息,指示新任务已到达。
public void executeTask(Task task){
System.out.printf("Server: A new task has arrived\n");
- 然后,调用执行器的
execute()方法来发送任务。
executor.execute(task);
- 最后,将一些执行器数据写入控制台,以查看其状态。
System.out.printf("Server: Pool Size: %d\n",executor.getPoolSize());
System.out.printf("Server: Active Count: %d\n",executor.getActiveCount());
System.out.printf("Server: Completed Tasks: %d\n",executor.getCompletedTaskCount());
- 实现
endServer()方法。在这个方法中,调用执行器的shutdown()方法来结束其执行。
public void endServer() {
executor.shutdown();
}
- 最后,通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
Server server=new Server();
for (int i=0; i<100; i++){
Task task=new Task("Task "+i);
server.executeTask(task);
}
server.endServer();
}
}
工作原理...
这个例子的关键是Server类。这个类创建并使用ThreadPoolExecutor来执行任务。
第一个重要的点是在Server类的构造函数中创建ThreadPoolExecutor。ThreadPoolExecutor类有四个不同的构造函数,但是由于其复杂性,Java 并发 API 提供了Executors类来构造执行器和其他相关对象。虽然我们可以直接使用其中一个构造函数来创建ThreadPoolExecutor,但建议使用Executors类。
在这种情况下,你使用newCachedThreadPool()方法创建了一个缓存线程池。这个方法返回一个ExecutorService对象,因此被转换为ThreadPoolExecutor以便访问其所有方法。你创建的缓存线程池在需要执行新任务时创建新线程,并在现有线程完成任务执行后重用它们,这些线程现在可用。线程的重用有一个优点,就是它减少了线程创建所需的时间。然而,缓存线程池的缺点是为新任务不断保持线程,因此如果你向这个执行器发送太多任务,可能会使系统超载。
注意
只有在有合理数量的线程或者线程执行时间较短时,才使用newCachedThreadPool()方法创建的执行器。
一旦你创建了执行器,就可以使用execute()方法发送Runnable或Callable类型的任务进行执行。在这种情况下,你发送实现Runnable接口的Task类的对象。
你还打印了一些关于执行器的日志信息。具体来说,你使用了以下方法:
-
getPoolSize(): 此方法返回执行器池中实际的线程数量 -
getActiveCount(): 此方法返回执行器中正在执行任务的线程数量 -
getCompletedTaskCount(): 此方法返回执行器完成的任务数量
ThreadPoolExecutor类和执行器的一个关键方面是你必须显式地结束它。如果不这样做,执行器将继续执行,程序将无法结束。如果执行器没有要执行的任务,它将继续等待新任务,并且不会结束执行。Java 应用程序直到所有非守护线程执行完毕才会结束,因此如果不终止执行器,你的应用程序将永远不会结束。
要指示执行器您要结束它,可以使用ThreadPoolExecutor类的shutdown()方法。当执行器完成所有待处理任务的执行时,它将结束执行。在调用shutdown()方法后,如果尝试向执行器发送另一个任务,将被拒绝,并且执行器将抛出RejectedExecutionException异常。
以下屏幕截图显示了此示例的一次执行的部分:

当最后一个任务到达服务器时,执行器有一个包含 100 个任务和 97 个活动线程的池。
还有更多...
ThreadPoolExecutor类提供了许多方法来获取有关其状态的信息。我们在示例中使用了getPoolSize()、getActiveCount()和getCompletedTaskCount()方法来获取有关池大小、线程数量和执行器已完成任务数量的信息。您还可以使用getLargestPoolSize()方法,该方法返回池中曾经同时存在的最大线程数。
ThreadPoolExecutor类还提供了与执行器的完成相关的其他方法。这些方法包括:
-
shutdownNow(): 此方法立即关闭执行器。它不执行待处理的任务。它返回一个包含所有这些待处理任务的列表。当您调用此方法时正在运行的任务将继续执行,但该方法不会等待它们完成。 -
isTerminated(): 如果您调用了shutdown()或shutdownNow()方法,并且执行器完成了关闭过程,则此方法返回true。 -
isShutdown(): 如果您调用了执行器的shutdown()方法,则此方法返回true。 -
awaitTermination(long``timeout,``TimeUnit``unit): 此方法阻塞调用线程,直到执行器的任务结束或超时发生。TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
注意
如果您想等待任务完成,无论其持续时间如何,可以使用较长的超时时间,例如DAYS。
另请参阅
-
第四章 线程执行器 中的 控制执行器的拒绝任务 配方
-
第八章 测试并发应用 中的 监视执行器框架 配方
创建固定大小线程执行器
当您使用使用Executors类的newCachedThreadPool()方法创建的基本ThreadPoolExecutor时,可能会出现执行器同时运行的线程数量问题。执行器为每个接收到的任务创建一个新线程(如果没有空闲的池线程),因此,如果您发送大量任务并且它们持续时间很长,可能会过载系统并导致应用程序性能不佳。
如果要避免此问题,Executors类提供了一个创建固定大小线程执行器的方法。此执行器具有最大线程数。如果发送的任务多于线程数,执行器将不会创建额外的线程,并且剩余的任务将被阻塞,直到执行器有空闲线程。通过这种行为,您可以确保执行器不会导致应用程序性能不佳。
在本配方中,您将学习如何创建一个固定大小的线程执行器,修改本章第一个配方中实现的示例。
准备就绪
您应该阅读本章中的 创建线程执行器 配方,并实现其中解释的示例,因为您将修改此示例。
此配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 实现本章第一个示例中描述的示例。打开
Server类并修改其构造函数。使用newFixedThreadPool()方法创建执行器,并将数字5作为参数传递。
public Server(){
executor=(ThreadPoolExecutor)Executors.newFixedThreadPool(5);
}
- 修改
executeTask()方法,包括一行额外的日志消息。调用getTaskCount()方法来获取已发送到执行器的任务数量。
System.out.printf("Server: Task Count: %d\n",executor.getTaskCount());
它是如何工作的...
在这种情况下,您已经使用了Executors类的newFixedThreadPool()方法来创建执行器。此方法创建一个具有最大线程数的执行器。如果发送的任务多于线程数,剩余的任务将被阻塞,直到有空闲线程来处理它们。此方法接收最大线程数作为您希望在执行器中拥有的参数。在您的情况下,您已创建了一个具有五个线程的执行器。
以下截图显示了此示例的一次执行的部分输出:

为了编写程序的输出,您已经使用了ThreadPoolExecutor类的一些方法,包括:
-
getPoolSize(): 此方法返回执行器池中实际线程的数量 -
getActiveCount(): 此方法返回执行器中正在执行任务的线程数
您可以看到这些方法的输出是5,表示执行器有五个线程。它没有超过设定的最大线程数。
当您将最后一个任务发送到执行器时,它只有5个活动线程。剩下的 95 个任务正在等待空闲线程。我们使用getTaskCount()方法来显示您已发送到执行器的数量。
还有更多...
Executors类还提供了newSingleThreadExecutor()方法。这是一个固定大小线程执行器的极端情况。它创建一个只有一个线程的执行器,因此一次只能执行一个任务。
另请参阅
-
第四章中的创建线程执行器示例,线程执行器
-
第八章中的监视执行器框架示例,测试并发应用
在返回结果的执行器中执行任务
执行器框架的一个优点是可以运行返回结果的并发任务。Java 并发 API 通过以下两个接口实现了这一点:
-
Callable: 此接口有call()方法。在此方法中,您必须实现任务的逻辑。Callable接口是一个参数化接口,这意味着您必须指示call()方法将返回的数据类型。 -
Future: 此接口有一些方法,用于获取Callable对象生成的结果并管理其状态。
在本示例中,您将学习如何实现返回结果的任务并在执行器上运行它们。
准备就绪...
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
FactorialCalculator的类。指定它实现了带有Integer类型的Callable接口。
public class FactorialCalculator implements Callable<Integer> {
声明一个私有的Integer属性叫做number,用于存储此任务将用于计算的数字。
private Integer number;
- 实现初始化类属性的类构造函数。
public FactorialCalculator(Integer number){
this.number=number;
}
- 实现
call()方法。此方法返回FactorialCalculator的number属性的阶乘。
@Override
public Integer call() throws Exception {
- 首先,创建并初始化方法中使用的内部变量。
int result = 1;
- 如果数字是
0或1,则返回1。否则,计算数字的阶乘。在两次乘法之间,出于教育目的,让此任务休眠 20 毫秒。
if ((num==0)||(num==1)) {
result=1;
} else {
for (int i=2; i<=number; i++) {
result*=i;
TimeUnit.MILLISECONDS.sleep(20);
}
}
- 在控制台上写入一条消息,其中包含操作的结果。
System.out.printf("%s: %d\n",Thread.currentThread().getName(),result);
- 返回操作的结果。
return result;
- 通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newFixedThreadPool()方法创建ThreadPoolExecutor来运行任务。将2作为参数传递。
ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors.newFixedThreadPool(2);
- 创建一个
Future<Integer>对象列表。
List<Future<Integer>> resultList=new ArrayList<>();
- 使用
Random类创建一个随机数生成器。
Random random=new Random();
- 生成 10 个新的随机整数,介于零和 10 之间。
for (int i=0; i<10; i++){
Integer number= random.nextInt(10);
- 创建一个
FactorialCaculator对象,传递这个随机数作为参数。
FactorialCalculator calculator=new FactorialCalculator(number);
- 调用执行器的
submit()方法,将FactorialCalculator任务发送到执行器。这个方法返回一个Future<Integer>对象来管理任务,并最终获得它的结果。
Future<Integer> result=executor.submit(calculator);
- 将
Future对象添加到之前创建的列表中。
resultList.add(result);
}
- 创建一个
do循环来监视执行器的状态。
do {
- 首先,使用执行器的
getCompletedTaskNumber()方法向控制台写入一条消息,指示已完成的任务数。
System.out.printf("Main: Number of Completed Tasks: %d\n",executor.getCompletedTaskCount());
- 然后,对列表中的 10 个
Future对象,使用isDone()方法编写一条消息,指示它管理的任务是否已经完成。
for (int i=0; i<resultList.size(); i++) {
Future<Integer> result=resultList.get(i);
System.out.printf("Main: Task %d: %s\n",i,result.isDone());
}
- 让线程睡眠 50 毫秒。
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 在执行器的已完成任务数小于 10 时重复此循环。
} while (executor.getCompletedTaskCount()<resultList.size());
- 将每个任务获得的结果写入控制台。对于每个
Future对象,使用get()方法获取其任务返回的Integer对象。
System.out.printf("Main: Results\n");
for (int i=0; i<resultList.size(); i++) {
Future<Integer> result=resultList.get(i);
Integer number=null;
try {
number=result.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
- 然后,将数字打印到控制台。
System.out.printf("Main: Task %d: %d\n",i,number);
}
- 最后,调用执行器的
shutdown()方法来结束其执行。
executor.shutdown();
它是如何工作的...
在这个配方中,您已经学会了如何使用Callable接口来启动返回结果的并发任务。您已经实现了FactorialCalculator类,该类实现了Callable接口,结果类型为Integer。因此,它在call()方法的返回类型之前返回。
这个示例的另一个关键点在于Main类。您使用submit()方法将一个Callable对象发送到执行器中执行。这个方法接收一个Callable对象作为参数,并返回一个Future对象,您可以用它来实现两个主要目标:
-
您可以控制任务的状态:您可以取消任务并检查它是否已完成。为此,您已经使用了
isDone()方法来检查任务是否已完成。 -
您可以获得
call()方法返回的结果。为此,您已经使用了get()方法。该方法等待,直到Callable对象完成call()方法的执行并返回其结果。如果在get()方法等待结果时线程被中断,它会抛出InterruptedException异常。如果call()方法抛出异常,该方法会抛出ExecutionException异常。
还有更多...
当您调用Future对象的get()方法时,如果由该对象控制的任务尚未完成,该方法将阻塞直到任务完成。Future接口提供了get()方法的另一个版本。
get(long``timeout,``TimeUnit``unit): 如果任务的结果不可用,此版本的get方法会等待指定的时间。如果指定的时间段过去了,结果仍然不可用,该方法将返回null值。TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
另请参阅
-
第四章中的创建线程执行器配方,线程执行器
-
第四章中的运行多个任务并处理第一个结果配方,线程执行器
-
第四章中的运行多个任务并处理所有结果配方,线程执行器
运行多个任务并处理第一个结果
并发编程中的一个常见问题是当您有各种并发任务来解决一个问题,而您只对这些任务的第一个结果感兴趣。例如,您想对数组进行排序。您有各种排序算法。您可以启动它们所有,并获得首个对数组进行排序的结果,也就是说,对于给定数组来说,最快的排序算法。
在本示例中,您将学习如何使用ThreadPoolExecutor类实现此场景。您将实现一个示例,其中用户可以通过两种机制进行验证。如果其中一种机制对用户进行验证,则用户将通过验证。
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
UserValidator的类,它将实现用户验证的过程。
public class UserValidator {
- 声明一个名为
name的私有String属性,它将存储用户验证系统的名称。
private String name;
- 实现初始化其属性的类的构造函数。
public UserValidator(String name) {
this.name=name;
}
- 实现
validate()方法。它接收两个String参数,分别是要验证的用户的名称和密码。
public boolean validate(String name, String password) {
- 创建一个名为
random的Random对象。
Random random=new Random();
- 等待随机一段时间以模拟用户验证过程。
try {
long duration=(long)(Math.random()*10);
System.out.printf("Validator %s: Validating a user during %d seconds\n",this.name,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
return false;
}
- 返回一个随机的
Boolean值。当用户通过验证时,该方法返回true值,当用户未通过验证时,该方法返回false值。
return random.nextBoolean();
}
- 实现
getName()方法。此方法返回名称属性的值。
public String getName(){
return name;
}
- 现在,创建一个名为
TaskValidator的类,它将使用UserValidation对象作为并发任务执行验证过程。指定它实现了参数化为String类的Callable接口。
public class TaskValidator implements Callable<String> {
- 声明一个名为
validator的私有UserValidator属性。
private UserValidator validator;
- 声明两个名为
user和password的私有String属性。
private String user;
private String password;
- 实现将初始化所有属性的类的构造函数。
public TaskValidator(UserValidator validator, String user, String password){
this.validator=validator;
this.user=user;
this.password=password;
}
- 实现将返回
String对象的call()方法。
@Override
public String call() throws Exception {
- 如果用户未通过
UserValidator对象进行验证,则向控制台写入一条消息指示此情况,并抛出Exception异常。
if (!validator.validate(user, password)) {
System.out.printf("%s: The user has not been found\n",validator.getName());
throw new Exception("Error validating user");
}
- 否则,向控制台写入一条消息,指示用户已经通过验证,并返回
UserValidator对象的名称。
System.out.printf("%s: The user has been found\n",validator.getName());
return validator.getName();
- 现在,通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建两个名为
user和password的String对象,并将它们初始化为test值。
String username="test";
String password="test";
- 创建两个名为
ldapValidator和dbValidator的UserValidator对象。
UserValidator ldapValidator=new UserValidator("LDAP");
UserValidator dbValidator=new UserValidator("DataBase");
- 创建两个名为
ldapTask和dbTask的TaskValidator对象。将它们分别初始化为ldapValidator和dbValidator。
TaskValidator ldapTask=new TaskValidator(ldapValidator, username, password);
TaskValidator dbTask=new TaskValidator(dbValidator,username,password);
- 创建一个
TaskValidator对象列表,并将您创建的两个对象添加到其中。
List<TaskValidator> taskList=new ArrayList<>();
taskList.add(ldapTask);
taskList.add(dbTask);
- 使用
Executors类的newCachedThreadPool()方法创建一个新的ThreadPoolExecutor对象和一个名为result的String对象。
ExecutorService executor=(ExecutorService)Executors.newCachedThreadPool();
String result;
- 调用
executor对象的invokeAny()方法。此方法接收taskList作为参数并返回String。此外,它将由此方法返回的String对象写入控制台。
try {
result = executor.invokeAny(taskList);
System.out.printf("Main: Result: %s\n",result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
- 使用
shutdown()方法终止执行程序,并向控制台写入一条消息以指示程序已结束。
executor.shutdown();
System.out.printf("Main: End of the Execution\n");
它是如何工作的...
示例的关键在于Main类。ThreadPoolExecutor类的invokeAny()方法接收任务列表,启动它们,并返回第一个完成而不抛出异常的任务的结果。此方法返回与您启动的任务的call()方法返回的相同的数据类型。在本例中,它返回一个String值。
以下屏幕截图显示了示例执行的输出,当一个任务验证了用户时:

示例有两个UserValidator对象,返回一个随机的boolean值。每个UserValidator对象都被一个Callable对象使用,由TaskValidator类实现。如果UserValidator类的validate()方法返回false值,TaskValidator类会抛出Exception。否则,它返回true值。
因此,我们有两个任务,可以返回true值,也可以抛出Exception异常。您可以有以下四种可能性:
-
两个任务都返回
true值。invokeAny()方法的结果是第一个完成的任务的名称。 -
第一个任务返回
true值,第二个任务抛出Exception。invokeAny()方法的结果是第一个任务的名称。 -
第一个任务抛出
Exception,第二个任务返回true值。invokeAny()方法的结果是第二个任务的名称。 -
两个任务都会抛出
Exception。在该类中,invokeAny()方法会抛出ExecutionException异常。
如果多次运行示例,您可以得到四种可能的解决方案。
以下屏幕截图显示了应用程序的输出,当两个任务都抛出异常时:

还有更多...
ThreadPoolExecutor类提供了invokeAny()方法的另一个版本:
invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit): 此方法执行所有任务,并在给定的超时时间之前完成的第一个任务的结果,如果在给定的超时时间之前完成而不抛出异常,则返回。TimeUnit类是一个枚举,具有以下常量:DAYS,HOURS,MICROSECONDS,MILLISECONDS,MINUTES,NANOSECONDS和SECONDS。
另请参阅
- 第四章中的运行多个任务并处理所有结果示例,线程执行程序
运行多个任务并处理所有结果
Executor 框架允许您执行并发任务,而无需担心线程的创建和执行。它为您提供了Future类,您可以使用它来控制任何在执行程序中执行的任务的状态并获取结果。
当您想要等待任务的完成时,可以使用以下两种方法:
-
Future接口的isDone()方法在任务完成执行时返回true。 -
ThreadPoolExecutor类的awaitTermination()方法使线程休眠,直到所有任务在调用shutdown()方法后完成执行。
这两种方法都有一些缺点。使用第一种方法,您只能控制任务的完成,而使用第二种方法,您必须关闭执行程序以等待线程,否则方法的调用会立即返回。
ThreadPoolExecutor类提供了一种方法,允许您向执行程序发送任务列表,并等待列表中所有任务的完成。在这个示例中,您将学习如何通过实现一个包含三个任务的示例来使用这个特性,并在它们完成时打印出它们的结果。
准备工作
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Result的类,用于存储此示例中并发任务生成的结果。
public class Result {
- 声明两个私有属性。一个名为
name的String属性,一个名为value的int属性。
private String name;
private int value;
- 实现相应的
get()和set()方法来设置和返回名称和值属性的值。
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
- 创建一个名为
Task的类,实现带有Result类参数的Callable接口。
public class Task implements Callable<Result> {
- 声明一个名为
name的私有String属性。
private String name;
- 实现初始化其属性的类的构造函数。
public Task(String name) {
this.name=name;
}
- 实现类的
call()方法。在这种情况下,此方法将返回一个Result对象。
@Override
public Result call() throws Exception {
- 首先,向控制台写入一条消息,指示任务正在开始。
System.out.printf("%s: Staring\n",this.name);
- 然后,等待随机一段时间。
try {
long duration=(long)(Math.random()*10);
System.out.printf("%s: Waiting %d seconds for results.\n",this.name,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 为了生成要在
Result对象中返回的int值,计算五个随机数的总和。
int value=0;
for (int i=0; i<5; i++){
value+=(int)(Math.random()*100);
}
- 创建一个
Result对象,并使用此任务的名称和先前完成的操作的结果对其进行初始化。
Result result=new Result();
result.setName(this.name);
result.setValue(value);
- 向控制台写入一条消息,指示任务已经完成。
System.out.println(this.name+": Ends");
- 返回
Result对象。
return result;
}
- 最后,通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newCachedThreadPool()方法创建一个ThreadPoolExecutor对象。
ExecutorService executor=(ExecutorService)Executors.newCachedThreadPool();
- 创建一个
Task对象列表。创建三个Task对象并将它们保存在该列表中。
List<Task> taskList=new ArrayList<>();
for (int i=0; i<3; i++){
Task task=new Task(i);
taskList.add(task);
}
- 创建一个
Future对象列表。这些对象使用Result类进行参数化。
List<Future<Result>>resultList=null;
- 调用
ThreadPoolExecutor类的invokeAll()方法。此类将返回先前创建的Future对象的列表。
try {
resultList=executor.invokeAll(taskList);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
shutdown()方法终止执行程序。
executor.shutdown();
- 写入处理
Future对象列表的任务结果。
System.out.println("Main: Printing the results");
for (int i=0; i<resultList.size(); i++){
Future<Result> future=resultList.get(i);
try {
Result result=future.get();
System.out.println(result.getName()+": "+result.getValue());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
工作原理...
在本食谱中,您已经学会了如何将任务列表发送到执行程序,并使用invokeAll()方法等待它们全部完成。此方法接收Callable对象的列表并返回Future对象的列表。此列表将为列表中的每个任务具有一个Future对象。Future对象列表中的第一个对象将是控制列表中Callable对象的第一个任务的对象,依此类推。
首先要考虑的一点是,在存储结果对象的列表的声明中,用于参数化Future接口的数据类型必须与用于参数化Callable对象的数据类型兼容。在这种情况下,您使用了相同类型的数据:Result类。
关于invokeAll()方法的另一个重要点是,您将仅使用Future对象来获取任务的结果。由于该方法在所有任务完成时结束,如果调用返回的Future对象的isDone()方法,所有调用都将返回true值。
还有更多...
ExecutorService类提供了invokeAll()方法的另一个版本:
invokeAll(Collection<?``extends``Callable<T>>``tasks,``long``timeout,``TimeUnit``unit): 此方法执行所有任务,并在所有任务都完成时返回它们的执行结果,如果它们在给定的超时时间之前完成。TimeUnit类是一个枚举,具有以下常量:DAYS,HOURS,MICROSECONDS,MILLISECONDS,MINUTES,NANOSECONDS和SECONDS。
另请参阅
-
第四章中的在返回结果的执行程序中执行任务食谱,线程执行程序
-
第四章中的运行多个任务并处理第一个结果食谱,线程执行程序
在延迟后在执行程序中运行任务
执行程序框架提供了ThreadPoolExecutor类,用于使用线程池执行Callable和Runnable任务,避免了所有线程创建操作。当您将任务发送到执行程序时,它将根据执行程序的配置尽快执行。有些情况下,您可能不希望尽快执行任务。您可能希望在一段时间后执行任务,或者定期执行任务。为此,执行程序框架提供了ScheduledThreadPoolExecutor类。
在本食谱中,您将学习如何创建ScheduledThreadPoolExecutor以及如何使用它在一定时间后安排任务的执行。
准备就绪
这个食谱的例子是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE 如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类,实现参数为String类的Callable接口。
public class Task implements Callable<String> {
- 声明一个私有的
String属性,名为name,用来存储任务的名称。
private String name;
- 实现初始化
name属性的类的构造函数。
public Task(String name) {
this.name=name;
}
- 实现
call()方法。在控制台上写入一个带有实际日期的消息,并返回一个文本,例如Hello, world。
public String call() throws Exception {
System.out.printf("%s: Starting at : %s\n",name,new Date());
return "Hello, world";
}
- 通过创建一个名为
Main的类并在其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newScheduledThreadPool()方法创建一个ScheduledThreadPoolExecutor类的执行器,传递1作为参数。
ScheduledThreadPoolExecutor executor=(ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(1);
- 初始化并启动一些任务(在我们的例子中为五个),使用
ScheduledThreadPoolExecutor实例的schedule()方法。
System.out.printf("Main: Starting at: %s\n",new Date());
for (int i=0; i<5; i++) {
Task task=new Task("Task "+i);
executor.schedule(task,i+1 , TimeUnit.SECONDS);
}
- 使用
shutdown()方法请求执行器的完成。
executor.shutdown();
- 使用执行器的
awaitTermination()方法等待所有任务的完成。
try {
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写一条消息来指示程序完成的时间。
System.out.printf("Main: Ends at: %s\n",new Date());
它是如何工作的...
这个例子的关键点是Main类和ScheduledThreadPoolExecutor的管理。与ThreadPoolExecutor类一样,为了创建一个定时执行器,Java 建议使用Executors类。在这种情况下,你需要使用newScheduledThreadPool()方法。你需要将数字1作为参数传递给这个方法。这个参数是你想要在池中拥有的线程数。
要在定时执行器中在一段时间后执行任务,你需要使用schedule()方法。这个方法接收以下三个参数:
-
你想要执行的任务
-
你希望任务在执行之前等待的时间段
-
时间段的单位,指定为
TimeUnit类的常量。
在这种情况下,每个任务将等待一定数量的秒数(TimeUnit.SECONDS),等于其在任务数组中的位置加一。
注意
如果你想在特定时间执行一个任务,计算那个日期和当前日期之间的差异,并将这个差异作为任务的延迟。
以下截图显示了此示例执行的输出:

你可以看到任务如何每秒开始执行一次。所有任务都同时发送到执行器,但每个任务的延迟比前一个任务晚 1 秒。
还有更多...
你也可以使用Runnable接口来实现任务,因为ScheduledThreadPoolExecutor类的schedule()方法接受这两种类型的任务。
尽管ScheduledThreadPoolExecutor类是ThreadPoolExecutor类的子类,因此继承了所有的特性,但 Java 建议仅将ScheduledThreadPoolExecutor用于定时任务。
最后,当你调用shutdown()方法并且有待处理的任务等待其延迟时间结束时,你可以配置ScheduledThreadPoolExecutor类的行为。默认行为是,尽管执行器已经完成,这些任务仍将被执行。你可以使用ScheduledThreadPoolExecutor类的setExecuteExistingDelayedTasksAfterShutdownPolicy()方法来改变这个行为。使用false,在shutdown()时,待处理的任务将不会被执行。
另请参阅
- 第四章中的在返回结果的执行器中执行任务食谱,线程执行器
定期在执行器中运行任务
Executor 框架提供了ThreadPoolExecutor类,使用线程池执行并发任务,避免了所有线程创建操作。当您将任务发送到执行程序时,根据其配置,它会尽快执行任务。当任务结束时,任务将从执行程序中删除,如果您想再次执行它们,您必须再次将其发送到执行程序。
但是,Executor 框架提供了通过ScheduledThreadPoolExecutor类执行定期任务的可能性。在这个食谱中,您将学习如何使用该类的这个功能来安排一个定期任务。
准备工作
这个食谱的例子是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
name的私有String属性,它将存储任务的名称。
private String name;
- 实现初始化该属性的类的构造函数。
public Task(String name) {
this.name=name;
}
- 实现
run()方法。向控制台写入一个带有实际日期的消息,以验证任务是否在指定的时间内执行。
@Override
public String call() throws Exception {
System.out.printf("%s: Starting at : %s\n",name,new Date());
return "Hello, world";
}
- 通过创建一个名为
Main的类并在其中实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newScheduledThreadPool()方法创建ScheduledThreadPoolExecutor。将数字1作为该方法的参数。
ScheduledExecutorService executor=Executors.newScheduledThreadPool(1);
- 向控制台写入一个带有实际日期的消息。
System.out.printf("Main: Starting at: %s\n",new Date());
- 创建一个新的
Task对象。
Task task=new Task("Task");
- 使用
scheduledAtFixRate()方法将其发送到执行程序。将任务创建的参数、数字一、数字二和常量TimeUnit.SECONDS作为参数。该方法返回一个ScheduledFuture对象,您可以使用它来控制任务的状态。
ScheduledFuture<?> result=executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);
- 创建一个循环,有 10 个步骤来写入任务下次执行的剩余时间。在循环中,使用
ScheduledFuture对象的getDelay()方法来获取直到任务下次执行的毫秒数。
for (int i=0; i<10; i++){
System.out.printf("Main: Delay: %d\n",result.getDelay(TimeUnit.MILLISECONDS));
Sleep the thread during 500 milliseconds.
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 使用
shutdown()方法结束执行程序。
executor.shutdown();
- 将线程休眠 5 秒,以验证定期任务是否已经完成。
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写一条消息来指示程序的结束。
System.out.printf("Main: Finished at: %s\n",new Date());
它是如何工作的...
当您想要使用 Executor 框架执行定期任务时,您需要一个ScheduledExecutorService对象。要创建它(与每个执行程序一样),Java 建议使用Executors类。这个类作为执行程序对象的工厂。在这种情况下,您应该使用newScheduledThreadPool()方法来创建一个ScheduledExecutorService对象。该方法接收池中线程的数量作为参数。在这个例子中,您已经将值1作为参数传递了。
一旦您有了执行定期任务所需的执行程序,您就可以将任务发送给执行程序。您已经使用了scheduledAtFixedRate()方法。该方法接受四个参数:您想要定期执行的任务,直到任务第一次执行之间的延迟时间,两次执行之间的时间间隔,以及第二个和第三个参数的时间单位。它是TimeUnit类的一个常量。TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
一个重要的要考虑的点是两次执行之间的时间间隔是开始这两次执行之间的时间间隔。如果您有一个需要 5 秒执行的周期性任务,并且您设置了 3 秒的时间间隔,那么您将有两个任务实例同时执行。
scheduleAtFixedRate()方法返回一个ScheduledFuture对象,它扩展了Future接口,具有用于处理计划任务的方法。ScheduledFuture是一个参数化接口。在本例中,由于您的任务是一个未参数化的Runnable对象,因此您必须使用?符号对其进行参数化。
您已经使用了ScheduledFuture接口的一个方法。getDelay()方法返回任务下一次执行的时间。此方法接收一个TimeUnit常量,其中包含您希望接收结果的时间单位。
以下屏幕截图显示了示例执行的输出:

您可以看到任务每 2 秒执行一次(以Task:前缀表示),并且控制台中每 500 毫秒写入延迟。这就是主线程被挂起的时间。当您关闭执行器时,计划任务结束执行,您将不会在控制台中看到更多消息。
还有更多...
ScheduledThreadPoolExecutor提供了其他方法来安排周期性任务。它是scheduleWithFixedRate()方法。它与scheduledAtFixedRate()方法具有相同的参数,但有一个值得注意的区别。在scheduledAtFixedRate()方法中,第三个参数确定两次执行开始之间的时间间隔。在scheduledWithFixedRate()方法中,参数确定任务执行结束和下一次执行开始之间的时间间隔。
您还可以使用shutdown()方法配置ScheduledThreadPoolExecutor类的实例的行为。默认行为是在调用该方法时计划任务结束。您可以使用ScheduledThreadPoolExecutor类的setContinueExistingPeriodicTasksAfterShutdownPolicy()方法来更改此行为,并使用true值。调用shutdown()方法时,周期性任务不会结束。
另请参阅
-
第四章中的创建线程执行器食谱,线程执行器
-
第四章中的在延迟后在执行器中运行任务食谱,线程执行器
在执行器中取消任务
当您使用执行器时,无需管理线程。您只需实现Runnable或Callable任务并将其发送到执行器。执行器负责创建线程,在线程池中管理它们,并在不需要时完成它们。有时,您可能希望取消发送到执行器的任务。在这种情况下,您可以使用Future的cancel()方法来执行取消操作。在本示例中,您将学习如何使用此方法来取消发送到执行器的任务。
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类,并指定它实现了参数化为String类的Callable接口。实现call()方法。在无限循环中向控制台写入消息并将其挂起 100 毫秒。
public class Task implements Callable<String> {
@Override
public String call() throws Exception {
while (true){
System.out.printf("Task: Test\n");
Thread.sleep(100);
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newCachedThreadPool()方法创建一个ThreadPoolExecutor对象。
ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
- 创建一个新的
Task对象。
Task task=new Task();
- 使用
submit()方法将任务发送到执行器。
System.out.printf("Main: Executing the Task\n");
Future<String> result=executor.submit(task);
- 将主任务挂起 2 秒。
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
submit()方法返回的名为result的Future对象的cancel()方法取消任务的执行。将true值作为cancel()方法的参数传递。
System.out.printf("Main: Canceling the Task\n");
result.cancel(true);
- 向控制台写入调用
isCancelled()和isDone()方法的结果,以验证任务是否已被取消,因此已经完成。
System.out.printf("Main: Canceled: %s\n",result.isCanceled());
System.out.printf("Main: Done: %s\n",result.isDone());
- 使用
shutdown()方法完成执行程序,并写入指示程序完成的消息。
executor.shutdown();
System.out.printf("Main: The executor has finished\n");
它是如何工作的...
当您想要取消发送到执行程序的任务时,可以使用Future接口的cancel()方法。根据cancel()方法的参数和任务的状态,此方法的行为不同:
-
如果任务已经完成或之前已被取消,或者由于其他原因无法取消,则该方法将返回
false值,任务将不会被取消。 -
如果任务正在等待执行它的
Thread对象,则任务将被取消并且永远不会开始执行。如果任务已经在运行,则取决于方法的参数。cancel()方法接收一个Boolean值作为参数。如果该参数的值为true并且任务正在运行,则将取消任务。如果参数的值为false并且任务正在运行,则不会取消任务。
以下屏幕截图显示了此示例执行的输出:

还有更多...
如果您使用控制已取消任务的Future对象的get()方法,get()方法将抛出CancellationException异常。
另请参阅
- 第四章中的在返回结果的执行程序中执行任务食谱,线程执行程序
在执行程序中控制任务完成
FutureTask类提供了一个名为done()的方法,允许您在执行程序中执行任务完成后执行一些代码。它可以用于执行一些后处理操作,生成报告,通过电子邮件发送结果或释放一些资源。当控制此FutureTask对象的任务的执行完成时,FutureTask类在内部调用此方法。该方法在任务的结果设置并且其状态更改为isDone状态后调用,无论任务是否已被取消或正常完成。
默认情况下,此方法为空。您可以重写FutureTask类并实现此方法以更改此行为。在本示例中,您将学习如何重写此方法以在任务完成后执行代码。
准备工作
本示例的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
ExecutableTask的类,并指定它实现了参数为String类的Callable接口。
public class ExecutableTask implements Callable<String> {
- 声明一个名为
name的私有String属性。它将存储任务的名称。实现getName()方法以返回此属性的值。
private String name;
public String getName(){
return name;
}
- 实现类的构造函数以初始化任务的名称。
public ExecutableTask(String name){
this.name=name;
}
- 实现
call()方法。让任务休眠一段随机时间并返回带有任务名称的消息。
@Override
public String call() throws Exception {
try {
long duration=(long)(Math.random()*10);
System.out.printf("%s: Waiting %d seconds for results.\n",this.name,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
}
return "Hello, world. I'm "+name;
}
- 实现一个名为
ResultTask的类,它扩展了参数为String类的FutureTask类。
public class ResultTask extends FutureTask<String> {
- 声明一个名为
name的私有String属性。它将存储任务的名称。
private String name;
- 实现类的构造函数。它必须接收一个
Callable对象作为参数。调用父类的构造函数,并使用接收到的任务的属性初始化name属性。
public ResultTask(Callable<String> callable) {
super(callable);
this.name=((ExecutableTask)callable).getName();
}
- 重写
done()方法。检查isCancelled()方法的值,并根据返回的值向控制台写入不同的消息。
@Override
protected void done() {
if (isCancelled()) {
System.out.printf("%s: Has been canceled\n",name);
} else {
System.out.printf("%s: Has finished\n",name);
}
}
- 通过创建一个名为
Main的类并向其添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newCachedThreadPool()方法创建ExecutorService。
ExecutorService executor=(ExecutorService)Executors.newCachedThreadPool();
- 创建一个数组来存储五个
ResultTask对象。
ResultTask resultTasks[]=new ResultTask[5];
- 初始化
ResultTask对象。对于数组中的每个位置,首先创建ExecutorTask,然后使用该对象创建ResultTask。然后使用submit()方法将ResultTask发送到执行器。
for (int i=0; i<5; i++) {
ExecutableTask executableTask=new ExecutableTask("Task "+i);
resultTasks[i]=new ResultTask(executableTask);
executor.submit(resultTasks[i]);
}
- 让主线程休眠 5 秒。
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
- 取消所有发送到执行器的任务。
for (int i=0; i<resultTasks.length; i++) {
resultTasks[i].cancel(true);
}
- 使用
ResultTask对象的get()方法将未被取消的任务的结果写入控制台。
for (int i=0; i<resultTasks.length; i++) {
try {
if (!resultTasks[i].isCanceled()){
System.out.printf("%s\n",resultTasks[i].get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} }
- 使用
shutdown()方法结束执行器。
executor.shutdown();
}
}
工作原理...
当被控制的任务完成执行时,done()方法由FutureTask类调用。在这个示例中,您已经实现了一个Callable对象,即ExecutableTask类,然后是FutureTask类的子类,用于控制ExecutableTask对象的执行。
done()方法在FutureTask类内部调用,用于确定返回值并将任务状态更改为isDone状态。您无法更改任务的结果值或更改其状态,但可以关闭任务使用的资源,编写日志消息或发送通知。
另请参阅
- 第四章中的在返回结果的执行器中执行任务一节,线程执行器
在执行器中分离任务的启动和处理它们的结果
通常,当您使用执行器执行并发任务时,您会将Runnable或Callable任务发送到执行器,并获取Future对象来控制方法。您可能会遇到需要在一个对象中将任务发送到执行器,并在另一个对象中处理结果的情况。对于这种情况,Java 并发 API 提供了CompletionService类。
这个CompletionService类有一个方法将任务发送到执行器,并有一个方法获取下一个完成执行的任务的Future对象。在内部,它使用一个Executor对象来执行任务。这种行为的优势是可以共享CompletionService对象,并将任务发送到执行器,以便其他对象可以处理结果。限制在于第二个对象只能获取已完成执行的任务的Future对象,因此这些Future对象只能用于获取任务的结果。
在这个示例中,您将学习如何使用CompletionService类来将在执行器中启动任务与处理它们的结果分离。
准备工作
这个示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
ReportGenerator的类,并指定它实现了参数化为String类的Callable接口。
public class ReportGenerator implements Callable<String> {
- 声明两个私有的
String属性,名为sender和title,它们将代表报告的数据。
private String sender;
private String title;
- 实现类的构造函数,初始化两个属性。
public ReportGenerator(String sender, String title){
this.sender=sender;
this.title=title;
}
- 实现
call()方法。首先让线程随机休眠一段时间。
@Override
public String call() throws Exception {
try {
Long duration=(long)(Math.random()*10);
System.out.printf("%s_%s: ReportGenerator: Generating a report during %d seconds\n",this.sender,this.title,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 然后,使用发送者和标题属性生成报告字符串,并返回该字符串。
String ret=sender+": "+title;
return ret;
}
- 创建一个名为
ReportRequest的类,并指定它实现Runnable接口。这个类将模拟一些报告请求。
public class ReportRequest implements Runnable {
- 声明一个私有的
String属性,名为name,用于存储ReportRequest的名称。
private String name;
- 声明一个私有的
CompletionService属性,名为service。CompletionService接口是一个参数化的接口。使用String类。
private CompletionService<String> service;
- 实现类的构造函数,初始化两个属性。
public ReportRequest(String name, CompletionService<String> service){
this.name=name;
this.service=service;
}
- 实现
run()方法。创建三个ReportGenerator对象,并使用submit()方法将它们发送到CompletionService对象。
@Override
public void run() {
ReportGenerator reportGenerator=new ReportGenerator(name, "Report");
service.submit(reportGenerator);
}
- 创建名为
ReportProcessor的类。这个类将获取ReportGenerator任务的结果。指定它实现Runnable接口。
public class ReportProcessor implements Runnable {
- 声明一个名为
service的私有CompletionService属性。由于CompletionService接口是一个参数化接口,因此在这个CompletionService接口的参数中使用String类。
private CompletionService<String> service;
- 声明一个名为
end的私有boolean属性。
private boolean end;
- 实现类的构造函数以初始化这两个属性。
public ReportProcessor (CompletionService<String> service){
this.service=service;
end=false;
}
- 实现
run()方法。当属性end为false时,调用CompletionService接口的poll()方法,以获取完成服务执行的下一个任务的Future对象。
@Override
public void run() {
while (!end){
try {
Future<String> result=service.poll(20, TimeUnit.SECONDS);
- 然后,使用
Future对象的get()方法获取任务的结果,并将这些结果写入控制台。
if (result!=null) {
String report=result.get();
System.out.printf("ReportReceiver: Report Received: %s\n",report);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
System.out.printf("ReportSender: End\n");
}
- 实现
setEnd()方法,修改end属性的值。
public void setEnd(boolean end) {
this.end = end;
}
- 通过创建名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
Executors类的newCachedThreadPool()方法创建ThreadPoolExecutor。
ExecutorService executor=(ExecutorService)Executors.newCachedThreadPool();
- 使用先前创建的执行器作为构造函数的参数创建
CompletionService。
CompletionService<String> service=new ExecutorCompletionService<>(executor);
- 创建两个
ReportRequest对象和执行它们的线程。
ReportRequest faceRequest=new ReportRequest("Face", service);
ReportRequest onlineRequest=new ReportRequest("Online", service);
Thread faceThread=new Thread(faceRequest);
Thread onlineThread=new Thread(onlineRequest);
- 创建一个
ReportProcessor对象和执行它的线程。
ReportProcessor processor=new ReportProcessor(service);
Thread senderThread=new Thread(processor);
- 启动三个线程。
System.out.printf("Main: Starting the Threads\n");
faceThread.start();
onlineThread.start();
senderThread.start();
- 等待
ReportRequest线程的最终完成。
try {
System.out.printf("Main: Waiting for the report generators.\n");
faceThread.join();
onlineThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
shutdown()方法完成执行器,并使用awaitTermination()方法等待任务的最终完成。
System.out.printf("Main: Shutting down the executor.\n");
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 完成
ReportSender对象的执行,将其end属性的值设置为true。
processor.setEnd(true);
System.out.println("Main: Ends");
它的工作原理...
在示例的主类中,使用Executors类的newCachedThreadPool()方法创建了ThreadPoolExecutor。然后,使用该对象初始化了CompletionService对象,因为完成服务使用执行器来执行其任务。要使用完成服务执行任务,可以像在ReportRequest类中一样使用submit()方法。
当这些任务中的一个在完成服务完成其执行时执行时,完成服务将Future对象存储在队列中,用于控制其执行。poll()方法访问此队列,以查看是否有任何已完成执行的任务,并在有任务完成执行时返回该队列的第一个元素,即已完成执行的任务的Future对象。当poll()方法返回一个Future对象时,它会从队列中删除。在这种情况下,您已向该方法传递了两个属性,以指示您希望等待任务完成的时间,以防已完成任务的结果队列为空。
创建CompletionService对象后,创建两个ReportRequest对象,每个对象在CompletionService中执行三个ReportGenerator任务,并创建一个ReportSender任务,该任务将处理两个ReportRequest对象发送的任务生成的结果。
还有更多...
CompletionService类可以执行Callable或Runnable任务。在这个例子中,您已经使用了Callable,但您也可以发送Runnable对象。由于Runnable对象不产生结果,因此CompletionService类的理念在这种情况下不适用。
这个类还提供了另外两个方法来获取已完成任务的Future对象。这些方法如下:
-
poll(): 不带参数的poll()方法检查队列中是否有任何Future对象。如果队列为空,它立即返回null。否则,它返回队列的第一个元素并将其从队列中删除。 -
take(): 这个方法没有参数,它检查队列中是否有任何Future对象。如果队列为空,它会阻塞线程,直到队列有元素。当队列有元素时,它会返回并从队列中删除第一个元素。
另请参阅
- 在第四章的在返回结果的执行者中执行任务配方中,线程执行者
控制执行者的被拒绝任务
当您想要完成执行者的执行时,使用shutdown()方法指示它应该完成。执行者等待正在运行或等待执行的任务完成,然后完成其执行。
如果在shutdown()方法和执行结束之间向执行者发送任务,则任务将被拒绝,因为执行者不再接受新任务。ThreadPoolExecutor类提供了一种机制,当任务被拒绝时调用该机制。
在这个配方中,您将学习如何管理实现了RejectedExecutionHandler的执行者中的拒绝任务。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
RejectedTaskController的类,它实现RejectedExecutionHandler接口。实现该接口的rejectedExecution()方法。向控制台写入已被拒绝的任务的名称以及执行者的名称和状态。
public class RejectedTaskController implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.printf("RejectedTaskController: The task %s has been rejected\n",r.toString());
System.out.printf("RejectedTaskController: %s\n",executor.toString());
System.out.printf("RejectedTaskController: Terminating: %s\n",executor.isTerminating());
System.out.printf("RejectedTaksController: Terminated: %s\n",executor.isTerminated());
}
- 实现一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable{
- 声明一个名为
name的私有String属性。它将存储任务的名称。
private String name;
- 实现类的构造函数。它将初始化类的属性。
public Task(String name){
this.name=name;
}
- 实现
run()方法。向控制台写入消息以指示方法的开始。
@Override
public void run() {
System.out.println("Task "+name+": Starting");
- 等待一段随机时间。
try {
long duration=(long)(Math.random()*10);
System.out.printf("Task %s: ReportGenerator: Generating a report during %d seconds\n",name,duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 向控制台写入消息以指示方法的最终化。
System.out.printf("Task %s: Ending\n",name);
}
- 重写
toString()方法。返回任务的名称。
public String toString() {
return name;
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
RejectedTaskController对象来管理被拒绝的任务。
RejectecTaskController controller=new RejectecTaskController();
- 使用
Executors类的newCachedThreadPool()方法创建ThreadPoolExecutor。
ThreadPoolExecutor executor=(ThreadPoolExecutor) Executors.newCachedThreadPool();
- 建立执行者的被拒绝任务控制器。
executor.setRejectedExecutionHandler(controller);
- 创建三个任务并将它们发送到执行者。
System.out.printf("Main: Starting.\n");
for (int i=0; i<3; i++) {
Task task=new Task("Task"+i);
executor.submit(task);
}
- 使用
shutdown()方法关闭执行者。
System.out.printf("Main: Shutting down the Executor.\n");
executor.shutdown();
- 创建另一个任务并将其发送到执行者。
System.out.printf("Main: Sending another Task.\n");
Task task=new Task("RejectedTask");
executor.submit(task);
- 向控制台写入消息以指示程序的最终化。
System.out.println("Main: End");
System.out.printf("Main: End.\n");
它是如何工作的...
在下面的屏幕截图中,您可以看到示例执行的结果:

当执行被关闭并且RejectecTaskController写入控制台关于任务和执行者的信息时,可以看到任务被拒绝。
要管理执行者的被拒绝任务,您应该创建一个实现RejectedExecutionHandler接口的类。该接口有一个名为rejectedExecution()的方法,带有两个参数:
-
存储已被拒绝任务的
Runnable对象 -
存储拒绝任务的执行者对象
对于每个被执行者拒绝的任务都会调用此方法。您需要使用Executor类的setRejectedExecutionHandler()方法来建立被拒绝任务的处理程序。
还有更多...
当执行者接收到要执行的任务时,它会检查是否调用了shutdown()方法。如果是,则拒绝任务。首先,它会查找使用setRejectedExecutionHandler()建立的处理程序。如果有一个,它会调用该类的rejectedExecution()方法,否则会抛出RejectedExecutionExeption。这是一个运行时异常,所以您不需要放置catch子句来控制它。
另请参阅
- 在第四章的创建线程执行者配方中,线程执行者
第五章:Fork/Join 框架
在本章中,我们将涵盖:
-
创建 Fork/Join 池
-
合并任务的结果
-
异步运行任务
-
在任务中抛出异常
-
取消任务
介绍
通常,当您实现一个简单的并发 Java 应用程序时,您实现一些Runnable对象,然后相应的Thread对象。您在程序中控制这些线程的创建、执行和状态。Java 5 通过Executor和ExecutorService接口以及实现它们的类(例如ThreadPoolExecutor类)引入了改进。
Executor 框架将任务的创建和执行分开。您只需实现Runnable对象并使用Executor对象。您将Runnable任务发送到执行程序,它将创建、管理和完成执行这些任务所需的线程。
Java 7 更进一步,并包括了ExecutorService接口的另一个针对特定问题的实现。这就是Fork/Join 框架。
该框架旨在使用分而治之的技术解决可以分解为较小任务的问题。在任务内部,您检查要解决的问题的大小,如果大于已确定的大小,则将其分解为较小的任务,使用框架执行。如果问题的大小小于已确定的大小,则直接在任务中解决问题,然后可选择地返回结果。以下图表总结了这个概念:

没有公式可以确定问题的参考大小,以确定是否对任务进行细分,这取决于其特性。您可以使用任务中要处理的元素数量和执行时间的估计来确定参考大小。测试不同的参考大小以选择最适合您问题的大小。您可以将ForkJoinPool视为一种特殊的Executor。
该框架基于以下两个操作:
-
fork操作:当您将任务分解为较小的任务并使用框架执行它们时
-
join操作:当一个任务等待它创建的任务的完成时
Fork/Join 和 Executor 框架之间的主要区别是工作窃取算法。与 Executor 框架不同,当任务等待使用 join 操作创建的子任务的完成时,执行该任务的线程(称为工作线程)会寻找尚未执行的其他任务并开始执行。通过这种方式,线程充分利用其运行时间,从而提高应用程序的性能。
为了实现这个目标,Fork/Join 框架执行的任务有以下限制:
-
任务只能使用
fork()和join()操作作为同步机制。如果它们使用其他同步机制,当它们处于同步操作时,工作线程无法执行其他任务。例如,如果您在 Fork/Join 框架中将任务休眠,执行该任务的工作线程在休眠时间内将不会执行另一个任务。 -
任务不应执行 I/O 操作,例如在文件中读取或写入数据。
-
任务不能抛出已检查异常。它必须包含处理它们所需的代码。
Fork/Join 框架的核心由以下两个类组成:
-
ForkJoinPool:它实现了ExecutorService接口和工作窃取算法。它管理工作线程并提供有关任务状态和执行的信息。 -
ForkJoinTask:这是在ForkJoinPool中执行的任务的基类。它提供了在任务内执行fork()和join()操作的机制,以及控制任务状态的方法。通常,为了实现你的 Fork/Join 任务,你将实现这个类的两个子类的子类:RecursiveAction用于没有返回结果的任务,RecursiveTask用于返回一个结果的任务。
本章介绍了五个示例,向你展示如何有效地使用 Fork/Join 框架。
创建一个 Fork/Join 池
在这个示例中,你将学习如何使用 Fork/Join 框架的基本元素。这包括:
-
创建一个
ForkJoinPool对象来执行任务 -
创建一个
ForkJoinTask的子类在池中执行
你将在这个示例中使用 Fork/Join 框架的主要特性如下:
-
你将使用默认构造函数创建
ForkJoinPool。 -
在任务内部,你将使用 Java API 文档推荐的结构:
If (problem size > default size){
tasks=divide(task);
execute(tasks);
} else {
resolve problem using another algorithm;
}
-
你将以同步的方式执行任务。当一个任务执行两个或更多子任务时,它会等待它们的完成。这样,执行该任务的线程(称为工作线程)将寻找其他任务来执行,充分利用它们的执行时间。
-
你要实现的任务不会返回任何结果,所以你将以
RecursiveAction类作为它们实现的基类。
准备工作
这个示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
在这个示例中,你将实现一个任务来更新产品列表的价格。最初的任务将负责更新列表中的所有元素。你将使用大小为 10 作为参考大小,所以如果一个任务需要更新超过 10 个元素,它会将分配给它的列表部分分成两部分,并创建两个任务来更新各自部分的产品价格。
按照以下步骤实现示例:
- 创建一个名为
Product的类,它将存储产品的名称和价格。
public class Product {
- 声明一个名为
name的私有String属性和一个名为price的私有double属性。
private String name;
private double price;
- 实现两个方法并确定两个属性的值。
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
- 创建一个名为
ProductListGenerator的类来生成一个随机产品列表。
public class ProductListGenerator {
- 实现
generate()方法。它接收一个int参数作为列表的大小,并返回一个带有生成产品列表的List<Product>对象。
public List<Product> generate (int size) {
- 创建返回产品列表的对象。
List<Product> ret=new ArrayList<Product>();
- 生成产品列表。为所有产品分配相同的价格,例如 10,以检查程序是否正常工作。
for (int i=0; i<size; i++){
Product product=new Product();
product.setName("Product "+i);
product.setPrice(10);
ret.add(product);
}
return ret;
}
- 创建一个名为
Task的类。指定它扩展RecursiveAction类。
public class Task extends RecursiveAction {
- 声明类的序列版本 UID。这个元素是必要的,因为
RecursiveAction类的父类ForkJoinTask类实现了Serializable接口。
private static final long serialVersionUID = 1L;
- 声明一个名为
products的私有List<Product>属性。
private List<Product> products;
- 声明两个私有的
int属性,名为first和last。这些属性将确定该任务需要处理的产品块。
private int first;
private int last;
- 声明一个名为
increment的私有double属性来存储产品价格的增量。
private double increment;
- 实现类的构造函数,初始化类的所有属性。
public Task (List<Product> products, int first, int last, double increment) {
this.products=products;
this.first=first;
this.last=last;
this.increment=increment;
}
- 实现
compute()方法来实现任务的逻辑。
@Override
protected void compute() {
- 如果
last和first属性的差小于 10(任务必须更新少于 10 个产品的价格),使用updatePrices()方法增加该产品集的价格。
if (last-first<10) {
updatePrices();
- 如果
last和first属性之间的差大于或等于 10,创建两个新的Task对象,一个用于处理产品的前一半,另一个用于处理产品的后一半,并使用invokeAll()方法在ForkJoinPool中执行它们。
} else {
int middle=(last+first)/2;
System.out.printf("Task: Pending tasks: %s\n",getQueuedTaskCount());
Task t1=new Task(products, first,middle+1, increment);
Task t2=new Task(products, middle+1,last, increment);
invokeAll(t1, t2);
}
- 实现
updatePrices()方法。该方法更新产品列表中first和last属性值之间的位置上的产品。
private void updatePrices() {
for (int i=first; i<last; i++){
Product product=products.get(i);
product.setPrice(product.getPrice()*(1+increment));
}
}
- 通过创建一个名为
Main的类并在其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
ProductListGenerator类创建一个包含 10,000 个产品的列表。
ProductListGenerator generator=new ProductListGenerator();
List<Product> products=generator.generate(10000);
- 创建一个新的
Task对象来更新产品列表中所有产品的价格。参数first取值为0,last参数取值为10,000(产品列表的大小)。
Task task=new Task(products, 0, products.size(), 0.20);
- 使用无参数的构造函数创建一个
ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 使用
execute()方法在池中执行任务。
pool.execute(task);
- 实现一个代码块,每隔五毫秒显示池的演变信息,将池的一些参数值写入控制台,直到任务完成执行。
do {
System.out.printf("Main: Thread Count: %d\n",pool.getActiveThreadCount());
System.out.printf("Main: Thread Steal: %d\n",pool.getStealCount());
System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!task.isDone());
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
isCompletedNormally()方法检查任务是否已经正常完成,如果是,则向控制台写入一条消息。
if (task.isCompletedNormally()){
System.out.printf("Main: The process has completed normally.\n");
}
- 增加后所有产品的预期价格为 12 美元。写出所有价格差为 12 的产品的名称和价格,以检查它们是否都正确地增加了价格。
for (int i=0; i<products.size(); i++){
Product product=products.get(i);
if (product.getPrice()!=12) {
System.out.printf("Product %s: %f\n",product.getName(),product.getPrice());
}
}
- 写一条消息指示程序的完成。
System.out.println("Main: End of the program.\n");
工作原理...
在这个示例中,您创建了一个ForkJoinPool对象和ForkJoinTask类的一个子类,然后在池中执行它。创建ForkJoinPool对象时,您使用了无参数的构造函数,因此它将以默认配置执行。它创建了一个线程数等于计算机处理器数量的池。当ForkJoinPool对象创建时,这些线程被创建,并且它们在池中等待直到一些任务到达执行。
由于Task类不返回结果,它继承了RecursiveAction类。在这个示例中,您已经使用了推荐的结构来实现任务。如果任务需要更新超过 10 个产品,它会将这些元素分成两个块,创建两个任务,并将一个块分配给每个任务。您已经在Task类中使用了first和last属性来知道该任务在产品列表中需要更新的位置范围。您已经使用了first和last属性,以便只使用产品列表的一个副本,而不是为每个任务创建不同的列表。
要执行任务创建的子任务,调用invokeAll()方法。这是一个同步调用,任务在继续(可能完成)执行之前等待子任务的完成。当任务等待其子任务时,执行它的工作线程会取出另一个等待执行的任务并执行它。通过这种行为,Fork/Join 框架比Runnable和Callable对象本身提供了更有效的任务管理。
ForkJoinTask类的invokeAll()方法是 Executor 和 Fork/Join 框架之间的主要区别之一。在 Executor 框架中,所有任务都必须发送到执行器,而在这种情况下,任务包括在池内执行和控制任务的方法。您已经在Task类中使用了invokeAll()方法,该类扩展了RecursiveAction类,后者又扩展了ForkJoinTask类。
您已经向池中发送了一个唯一的任务来更新所有产品列表,使用execute()方法。在这种情况下,这是一个异步调用,主线程继续执行。
你已经使用了ForkJoinPool类的一些方法来检查正在运行的任务的状态和进展。该类包括更多的方法,可以用于此目的。请参阅监视 Fork/Join 池中的完整方法列表。
最后,就像使用 Executor 框架一样,你应该使用shutdown()方法来结束ForkJoinPool。
以下截图显示了此示例的部分执行:

你可以看到任务完成它们的工作,产品价格更新。
还有更多...
ForkJoinPool类提供了其他方法来执行任务。这些方法如下:
-
execute (Runnable task): 这是在示例中使用的execute()方法的另一个版本。在这种情况下,你将一个Runnable任务发送给ForkJoinPool类。请注意,ForkJoinPool类不会使用工作窃取算法处理Runnable对象。它只用于ForkJoinTask对象。 -
invoke(ForkJoinTask<T> task): 虽然execute()方法在示例中对ForkJoinPool类进行了异步调用,但invoke()方法对ForkJoinPool类进行了同步调用。这个调用直到作为参数传递的任务完成执行才返回。 -
你还可以使用
ExecutorService接口中声明的invokeAll()和invokeAny()方法。这些方法接收Callable对象作为参数。ForkJoinPool类不会使用工作窃取算法处理Callable对象,因此最好使用执行器来执行它们。
ForkJoinTask类还包括在示例中使用的invokeAll()方法的其他版本。这些版本如下:
-
invokeAll(ForkJoinTask<?>... tasks): 这个方法的版本使用可变参数列表。你可以传递任意数量的ForkJoinTask对象作为参数。 -
invokeAll(Collection<T> tasks): 这个方法的版本接受一个泛型类型T的对象集合(例如ArrayList对象、LinkedList对象或TreeSet对象)。这个泛型类型T必须是ForkJoinTask类或它的子类。
虽然ForkJoinPool类设计用于执行ForkJoinTask对象,但你也可以直接执行Runnable和Callable对象。你还可以使用ForkJoinTask类的adapt()方法,该方法接受一个Callable对象或Runnable对象,并返回一个ForkJoinTask对象来执行该任务。
另请参阅
- 在第八章的监视 Fork/Join 池中
合并任务的结果
Fork/Join 框架提供了执行返回结果的任务的能力。这些任务由RecursiveTask类实现。这个类扩展了ForkJoinTask类,并实现了 Executor 框架提供的Future接口。
在任务内部,你必须使用 Java API 文档推荐的结构:
If (problem size > size){
tasks=Divide(task);
execute(tasks);
groupResults()
return result;
} else {
resolve problem;
return result;
}
如果任务需要解决的问题比预定义的大小更大,你可以将问题分解为更多的子任务,并使用 Fork/Join 框架执行这些子任务。当它们完成执行时,发起任务获取所有子任务生成的结果,对它们进行分组,并返回最终结果。最终,当池中执行的发起任务完成执行时,你获得它的结果,这实际上是整个问题的最终结果。
在这个示例中,你将学习如何使用 Fork/Join 框架解决问题,开发一个在文档中查找单词的应用程序。你将实现以下两种任务:
-
一个文档任务,用于在文档的一组行中搜索单词
-
一行任务,用于在文档的一部分中搜索单词
所有任务将返回它们处理的文档或行中单词出现的次数。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Document的类。它将生成一个模拟文档的字符串矩阵。
public class Document {
- 创建一个包含一些单词的字符串数组。这个数组将在生成字符串矩阵时使用。
private String words[]={"the","hello","goodbye","packt", "java","thread","pool","random","class","main"};
- 实现
generateDocument()方法。它接收行数、每行单词数和示例将要查找的单词作为参数。它返回一个字符串矩阵。
public String[][] generateDocument(int numLines, int numWords, String word){
- 首先,创建必要的对象来生成文档:
String矩阵和一个Random对象来生成随机数。
int counter=0;
String document[][]=new String[numLines][numWords];
Random random=new Random();
- 用字符串填充数组。在每个位置存储数组中随机位置的字符串,并计算程序将在生成的数组中查找的单词出现的次数。您可以使用这个值来检查程序是否正确执行其任务。
for (int i=0; i<numLines; i++){
for (int j=0; j<numWords; j++) {
int index=random.nextInt(words.length);
document[i][j]=words[index];
if (document[i][j].equals(word)){
counter++;
}
}
}
- 编写一条消息,其中包含单词出现的次数,并返回生成的矩阵。
System.out.println("DocumentMock: The word appears "+ counter+" times in the document");
return document;
- 创建一个名为
DocumentTask的类,并指定它扩展了参数为Integer类的RecursiveTask类。这个类将实现计算一组行中单词出现次数的任务。
public class DocumentTask extends RecursiveTask<Integer> {
- 声明一个私有的
String矩阵,名为document,和两个私有的int属性,名为start和end。还声明一个私有的String属性,名为word。
private String document[][];
private int start, end;
private String word;
- 实现类的构造函数以初始化所有属性。
public DocumentTask (String document[][], int start, int end, String word){
this.document=document;
this.start=start;
this.end=end;
this.word=word;
}
- 实现
compute()方法。如果end和start属性之间的差小于 10,任务将调用processLines()方法计算这些位置之间行中单词出现的次数。
@Override
protected Integer compute() {
int result;
if (end-start<10){
result=processLines(document, start, end, word);
- 否则,将行组分成两个对象,创建两个新的
DocumentTask对象来处理这两组,并使用invokeAll()方法在池中执行它们。
} else {
int mid=(start+end)/2;
DocumentTask task1=new DocumentTask(document,start,mid,word);
DocumentTask task2=new DocumentTask(document,mid,end,word);
invokeAll(task1,task2);
- 然后,使用
groupResults()方法添加两个任务返回的值。最后,返回任务计算的结果。
try {
result=groupResults(task1.get(),task2.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
- 实现
processLines()方法。它接收字符串矩阵、start属性、end属性和word属性作为参数,任务是搜索的单词。
private Integer processLines(String[][] document, int start, int end,String word) {
- 对于任务需要处理的每一行,创建一个
LineTask对象来处理整行,并将它们存储在任务列表中。
List<LineTask> tasks=new ArrayList<LineTask>();
for (int i=start; i<end; i++){
LineTask task=new LineTask(document[i], 0, document[i].length, word);
tasks.add(task);
}
- 使用
invokeAll()方法执行列表中的所有任务。
invokeAll(tasks);
- 将所有这些任务返回的值相加,并返回结果。
int result=0;
for (int i=0; i<tasks.size(); i++) {
LineTask task=tasks.get(i);
try {
result=result+task.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return new Integer(result);
- 实现
groupResults()方法。它将两个数字相加并返回结果。
private Integer groupResults(Integer number1, Integer number2) {
Integer result;
result=number1+number2;
return result;
}
- 创建一个名为
LineTask的类,并指定它扩展了参数为Integer类的RecursiveTask类。这个类将实现计算一行中单词出现次数的任务。
public class LineTask extends RecursiveTask<Integer>{
- 声明类的序列化版本 UID。这个元素是必要的,因为
RecursiveTask类的父类ForkJoinTask类实现了Serializable接口。声明一个私有的String数组属性,名为line,和两个私有的int属性,名为start和end。最后,声明一个私有的String属性,名为word。
private static final long serialVersionUID = 1L;
private String line[];
private int start, end;
private String word;
- 实现类的构造函数以初始化所有属性。
public LineTask(String line[], int start, int end, String word) {
this.line=line;
this.start=start;
this.end=end;
this.word=word;
}
- 实现类的
compute()方法。如果end和start属性之间的差小于 100,任务将使用count()方法在由start和end属性确定的行片段中搜索单词。
@Override
protected Integer compute() {
Integer result=null;
if (end-start<100) {
result=count(line, start, end, word);
- 否则,将行中的单词组分成两部分,创建两个新的
LineTask对象来处理这两组,并使用invokeAll()方法在池中执行它们。
} else {
int mid=(start+end)/2;
LineTask task1=new LineTask(line, start, mid, word);
LineTask task2=new LineTask(line, mid, end, word);
invokeAll(task1, task2);
- 然后,使用
groupResults()方法添加两个任务返回的值。最后,返回任务计算的结果。
try {
result=groupResults(task1.get(),task2.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return result;
- 实现
count()方法。它接收完整行的字符串数组,star属性,end属性和作为参数搜索任务的word属性。
private Integer count(String[] line, int start, int end, String word) {
- 将存储在
start和end属性之间位置的单词与任务正在搜索的word属性进行比较,如果它们相等,则增加一个counter变量。
int counter;
counter=0;
for (int i=start; i<end; i++){
if (line[i].equals(word)){
counter++;
}
}
- 为了减慢示例的执行,让任务休眠 10 毫秒。
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 返回
counter变量的值。
return counter;
- 实现
groupResults()方法。它将两个数字相加并返回结果。
private Integer groupResults(Integer number1, Integer number2) {
Integer result;
result=number1+number2;
return result;
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main{
public static void main(String[] args) {
- 使用
DocumentMock类创建一个包含 100 行和每行 1,000 个单词的Document。
DocumentMock mock=new DocumentMock();
String[][] document=mock.generateDocument(100, 1000, "the");
- 创建一个新的
DocumentTask对象来更新整个文档的产品。参数start取值0,end参数取值100。
DocumentTask task=new DocumentTask(document, 0, 100, "the");
- 使用不带参数的构造函数创建一个
ForkJoinPool对象,并使用execute()方法在池中执行任务。
ForkJoinPool pool=new ForkJoinPool();
pool.execute(task);
- 实现一段代码块,每秒向控制台写入池的一些参数值,直到任务完成执行为止,显示有关池进度的信息。
do {
System.out.printf("******************************************\n");
System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
System.out.printf("Main: Active Threads: %d\n",pool.getActiveThreadCount());
System.out.printf("Main: Task Count: %d\n",pool.getQueuedTaskCount());
System.out.printf("Main: Steal Count: %d\n",pool.getStealCount());
System.out.printf("******************************************\n");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!task.isDone());
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
awaitTermination()方法等待任务的完成。
try {
pool.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写下单词在文档中出现的次数。检查这个数字是否与
DocumentMock类写的数字相同。
try {
System.out.printf("Main: The word appears %d in the document",task.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
它是如何工作的...
在这个例子中,您实现了两个不同的任务:
-
DocumentTask类:这个类的任务是处理由start和end属性确定的文档行集合。如果这组行的大小小于 10,它为每行创建一个LineTask,当它们完成执行时,它将这些任务的结果相加并返回总和的结果。如果任务必须处理的行集合大小为 10 或更大,它将这个集合分成两部分,并创建两个DocumentTask对象来处理这些新集合。当这些任务完成执行时,任务将它们的结果相加并返回该总和作为结果。 -
LineTask类:这个类的任务是处理文档行的一组单词。如果这组单词小于 100,任务直接在这组单词中搜索单词并返回单词出现的次数。否则,它将这组单词分成两部分,并创建两个LineTask对象来处理这些集合。当这些任务完成执行时,任务将两个任务的结果相加,并将该总和作为结果返回。
在Main类中,您使用默认构造函数创建了一个ForkJoinPool对象,并在其中执行了一个DocumentTask类,该类必须处理 100 行文档,每行 1,000 个单词。这个任务将使用其他DocumentTask对象和LineTask对象来分解问题,当所有任务完成执行时,您可以使用原始任务来获取整个文档中单词出现的总次数。由于任务返回一个结果,它们扩展了RecursiveTask类。
为了获得Task返回的结果,您使用了get()方法。这个方法在RecursiveTask类中声明。
当您执行程序时,您可以比较控制台中写的第一行和最后一行。第一行是在生成文档时计算单词出现次数,最后一行是由 Fork/Join 任务计算的相同数字。
还有更多...
ForkJoinTask类提供了另一个方法来完成任务的执行并返回结果,即complete()方法。此方法接受RecursiveTask类参数化中使用的类型的对象,并在调用join()方法时将该对象作为任务的结果返回。建议使用它来为异步任务提供结果。
由于RecursiveTask类实现了Future接口,因此get()方法还有另一个版本:
get(long timeout, TimeUnit unit): 此版本的get()方法,如果任务的结果不可用,将等待指定的时间。如果指定的时间段过去,结果尚不可用,则该方法返回null值。TimeUnit类是一个枚举,具有以下常量:DAYS,HOURS,MICROSECONDS,MILLISECONDS,MINUTES,NANOSECONDS和SECONDS。
另请参阅
-
在第五章的创建 Fork/Join 池配方中,Fork/Join Framework
-
在第八章的监视 Fork/Join 池配方中,测试并发应用程序
异步运行任务
当您在ForkJoinPool中执行ForkJoinTask时,可以以同步或异步方式执行。当以同步方式执行时,将任务发送到池的方法直到任务完成执行才返回。当以异步方式执行时,将任务发送到执行程序的方法立即返回,因此任务可以继续执行。
您应该注意两种方法之间的重大区别。当您使用同步方法时,调用其中一个方法的任务(例如invokeAll()方法)将被挂起,直到它发送到池的任务完成执行。这允许ForkJoinPool类使用工作窃取算法将新任务分配给执行休眠任务的工作线程。相反,当您使用异步方法(例如fork()方法)时,任务将继续执行,因此ForkJoinPool类无法使用工作窃取算法来提高应用程序的性能。在这种情况下,只有当您调用join()或get()方法等待任务的完成时,ForkJoinPool类才能使用该算法。
在本配方中,您将学习如何使用ForkJoinPool和ForkJoinTask类提供的异步方法来管理任务。您将实现一个程序,该程序将在文件夹及其子文件夹中搜索具有确定扩展名的文件。您要实现的ForkJoinTask类将处理文件夹的内容。对于该文件夹中的每个子文件夹,它将以异步方式向ForkJoinPool类发送一个新任务。对于该文件夹中的每个文件,任务将检查文件的扩展名,并将其添加到结果列表中(如果适用)。
如何做...
按照以下步骤实现示例:
- 创建一个名为
FolderProcessor的类,并指定它扩展了使用List<String>类型参数化的RecursiveTask类。
public class FolderProcessor extends RecursiveTask<List<String>> {
- 声明类的序列版本 UID。这个元素是必需的,因为
RecursiveTask类的父类ForkJoinTask类实现了Serializable接口。
private static final long serialVersionUID = 1L;
- 声明一个名为
path的私有String属性。此属性将存储此任务要处理的文件夹的完整路径。
private String path;
- 声明一个名为
extension的私有String属性。此属性将存储此任务要查找的文件的扩展名。
private String extension;
- 实现类的构造函数以初始化其属性。
public FolderProcessor (String path, String extension) {
this.path=path;
this.extension=extension;
}
- 实现
compute()方法。由于您使用List<String>类型参数化了RecursiveTask类,因此此方法必须返回该类型的对象。
@Override
protected List<String> compute() {
- 声明一个
String对象列表,用于存储存储在文件夹中的文件的名称。
List<String> list=new ArrayList<>();
- 声明一个
FolderProcessor任务列表,用于存储将处理存储在文件夹中的子文件夹的子任务。
List<FolderProcessor> tasks=new ArrayList<>();
- 获取文件夹的内容。
File file=new File(path);
File content[] = file.listFiles();
- 对于文件夹中的每个元素,如果有子文件夹,则创建一个新的
FolderProcessor对象,并使用fork()方法异步执行它。
if (content != null) {
for (int i = 0; i < content.length; i++) {
if (content[i].isDirectory()) {
FolderProcessor task=new FolderProcessor(content[i].getAbsolutePath(), extension);
task.fork();
tasks.add(task);
- 否则,使用
checkFile()方法比较文件的扩展名与您要查找的扩展名,如果它们相等,则将文件的完整路径存储在先前声明的字符串列表中。
} else {
if (checkFile(content[i].getName())){
list.add(content[i].getAbsolutePath());
}
}
}
- 如果
FolderProcessor子任务列表的元素超过 50 个,向控制台写入消息以指示此情况。
if (tasks.size()>50) {
System.out.printf("%s: %d tasks ran.\n",file.getAbsolutePath(),tasks.size());
}
- 调用辅助方法
addResultsFromTask(),该方法将由此任务启动的子任务返回的结果添加到文件列表中。将字符串列表和FolderProcessor子任务列表作为参数传递给它。
addResultsFromTasks(list,tasks);
- 返回字符串列表。
return list;
- 实现
addResultsFromTasks()方法。对于存储在任务列表中的每个任务,调用join()方法等待其完成,然后将任务的结果使用addAll()方法添加到字符串列表中。
private void addResultsFromTasks(List<String> list,
List<FolderProcessor> tasks) {
for (FolderProcessor item: tasks) {
list.addAll(item.join());
}
}
- 实现
checkFile()方法。该方法比较传递的文件名是否以你要查找的扩展名结尾。如果是,则该方法返回true值,否则返回false值。
private boolean checkFile(String name) {
return name.endsWith(extension);
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用默认构造函数创建
ForkJoinPool。
ForkJoinPool pool=new ForkJoinPool();
- 创建三个
FolderProcessor任务。使用不同的文件夹路径初始化每个任务。
FolderProcessor system=new FolderProcessor("C:\\Windows", "log");
FolderProcessor apps=new
FolderProcessor("C:\\Program Files","log");
FolderProcessor documents=new FolderProcessor("C:\\Documents And Settings","log");
- 使用
execute()方法在池中执行三个任务。
pool.execute(system);
pool.execute(apps);
pool.execute(documents);
- 每秒向控制台写入有关池状态的信息,直到三个任务完成执行。
do {
System.out.printf("******************************************\n");
System.out.printf("Main: Parallelism: %d\n",pool.getParallelism());
System.out.printf("Main: Active Threads: %d\n",pool.getActiveThreadCount());
System.out.printf("Main: Task Count: %d\n",pool.getQueuedTaskCount());
System.out.printf("Main: Steal Count: %d\n",pool.getStealCount());
System.out.printf("******************************************\n");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while ((!system.isDone())||(!apps.isDone())||(!documents.isDone()));
- 使用
shutdown()方法关闭ForkJoinPool。
pool.shutdown();
- 将每个任务生成的结果数量写入控制台。
List<String> results;
results=system.join();
System.out.printf("System: %d files found.\n",results.size());
results=apps.join();
System.out.printf("Apps: %d files found.\n",results.size());
results=documents.join();
System.out.printf("Documents: %d files found.\n",results.size());
工作原理...
以下屏幕截图显示了此示例的部分执行:

这个示例的关键在于FolderProcessor类。每个任务处理文件夹的内容。正如您所知,此内容具有以下两种元素:
-
文件
-
其他文件夹
如果任务找到一个文件夹,它会创建另一个Task对象来处理该文件夹,并使用fork()方法将其发送到池中。该方法将任务发送到池中,如果有空闲的工作线程,它将执行该任务,或者它可以创建一个新的工作线程。该方法立即返回,因此任务可以继续处理文件夹的内容。对于每个文件,任务将其扩展名与要查找的扩展名进行比较,如果它们相等,则将文件名添加到结果列表中。
一旦任务处理了分配的文件夹的所有内容,它将等待通过join()方法发送到池中的所有任务的完成。在任务中调用的此方法等待其执行的完成,并返回compute()方法返回的值。任务将其自己的结果与其发送的所有任务的结果分组,并将该列表作为compute()方法的返回值返回。
ForkJoinPool类还允许以异步方式执行任务。您已经使用execute()方法将三个初始任务发送到池中。在Main类中,您还使用shutdown()方法完成了池,并编写了有关正在其中运行的任务的状态和进展的信息。ForkJoinPool类包括更多对此有用的方法。请参阅监视 Fork/Join 池配方,以查看这些方法的完整列表。
还有更多...
在这个例子中,您已经使用join()方法等待任务的完成并获取它们的结果。您还可以使用get()方法的两个版本之一来实现这个目的:
-
get():如果ForkJoinTask已经完成执行,此版本的get()方法将返回compute()方法返回的值,或者等待直到其完成。 -
get(long timeout, TimeUnit unit):如果任务的结果不可用,此版本的get()方法将等待指定的时间。如果经过指定的时间段,结果仍然不可用,该方法将返回一个null值。TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
get()和join()方法之间有两个主要区别:
-
join()方法无法被中断。如果中断调用join()方法的线程,该方法将抛出InterruptedException异常。 -
get()方法将在任务抛出任何未检查的异常时返回ExecutionException异常,而join()方法将返回RuntimeException异常。
另请参阅
-
在第五章的创建 Fork/Join 池配方中,Fork/Join Framework
-
在第八章的监视 Fork/Join 池配方中,测试并发应用
在任务中抛出异常
Java 中有两种异常:
-
已检查的异常:这些异常必须在方法的
throws子句中指定,或者在其中捕获。例如,IOException或ClassNotFoundException。 -
未检查的异常:这些异常不需要被指定或捕获。例如,
NumberFormatException。
您不能在ForkJoinTask类的compute()方法中抛出任何已检查的异常,因为该方法在其实现中不包括任何 throws 声明。您必须包含必要的代码来处理异常。另一方面,您可以抛出(或者可以由方法或方法内部使用的对象抛出)未检查的异常。ForkJoinTask和ForkJoinPool类的行为与您可能期望的不同。程序不会完成执行,您也不会在控制台上看到有关异常的任何信息。它会被简单地吞没,就好像它没有被抛出一样。但是,您可以使用ForkJoinTask类的一些方法来了解任务是否抛出了异常以及异常的类型。在本配方中,您将学习如何获取这些信息。
准备工作
这个示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类。指定它实现了使用Integer类参数化的RecursiveTask类。
public class Task extends RecursiveTask<Integer> {
- 声明一个名为
array的私有int数组。它将模拟您将在本示例中处理的数据数组。
private int array[];
- 声明两个私有
int属性,名为start和end。这些属性将确定该任务必须处理的数组元素。
private int start, end;
- 实现初始化其属性的类的构造函数。
public Task(int array[], int start, int end){
this.array=array;
this.start=start;
this.end=end;
}
- 实现任务的
compute()方法。由于您使用Integer类对RecursiveTask类进行了参数化,因此该方法必须返回一个Integer对象。首先,在控制台上写入start和end属性的值。
@Override
protected Integer compute() {
System.out.printf("Task: Start from %d to %d\n",start,end);
- 如果该任务必须处理的元素块(由
start和end属性确定)的大小小于 10,请检查数组中第四个位置(索引号为 3)的元素是否在该块中。如果是这样,抛出RuntimeException异常。然后,让任务休眠一秒钟。
if (end-start<10) {
if ((3>start)&&(3<end)){
throw new RuntimeException("This task throws an"+ "Exception: Task from "+start+" to "+end);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 否则(此任务需要处理的元素块的大小为 10 或更大),将元素块分成两部分,创建两个
Task对象来处理这些块,并使用invokeAll()方法在池中执行它们。
} else {
int mid=(end+start)/2;
Task task1=new Task(array,start,mid);
Task task2=new Task(array,mid,end);
invokeAll(task1, task2);
}
- 向控制台写入一条消息,指示任务结束,并写入
start和end属性的值。
System.out.printf("Task: End form %d to %d\n",start,end);
- 将数字
0作为任务的结果返回。
return 0;
- 通过创建一个名为
Main的类并创建一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个包含 100 个整数的数组。
int array[]=new int[100];
- 创建一个
Task对象来处理该数组。
Task task=new Task(array,0,100);
- 使用默认构造函数创建一个
ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 使用
execute()方法在池中执行任务。
pool.execute(task);
- 使用
shutdown()方法关闭ForkJoinPool类。
pool.shutdown();
- 使用
awaitTermination()方法等待任务的完成。由于您希望等待任务的完成时间长达多久,因此将值1和TimeUnit.DAYS作为参数传递给此方法。
try {
pool.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
isCompletedAbnormally()方法检查任务或其子任务是否抛出异常。在这种情况下,使用ForkJoinTask类的getException()方法向控制台写入带有抛出的异常的消息。获取该异常。
if (task.isCompletedAbnormally()) {
System.out.printf("Main: An exception has ocurred\n");
System.out.printf("Main: %s\n",task.getException());
}
System.out.printf("Main: Result: %d",task.join());
它是如何工作的...
在这个示例中,您实现的Task类处理一个数字数组。它检查它需要处理的数字块是否有 10 个或更多元素。在这种情况下,它将块分成两部分,并创建两个新的Task对象来处理这些块。否则,它会查找数组的第四个位置(索引号为 3)的元素。如果该元素在任务需要处理的块中,则会抛出RuntimeException异常。
当执行程序时,会抛出异常,但程序不会停止。在Main类中,您已经包含了对ForkJoinTask类的isCompletedAbnormally()方法的调用,使用原始任务。如果该任务或其子任务中的一个抛出异常,则此方法返回true。您还使用了相同对象的getException()方法来获取它抛出的Exception对象。
当您在任务中抛出一个未经检查的异常时,它也会影响其父任务(将其发送到ForkJoinPool类的任务)以及其父任务的父任务,依此类推。如果您审查程序的所有输出,您会发现某些任务的完成没有输出消息。这些任务的开始消息如下:
Task: Starting form 0 to 100
Task: Starting form 0 to 50
Task: Starting form 0 to 25
Task: Starting form 0 to 12
Task: Starting form 0 to 6
这些任务是抛出异常的任务及其父任务。它们全部都以异常方式完成。在开发使用ForkJoinPool和ForkJoinTask对象的程序时,如果不希望出现这种行为,应考虑这一点。
以下屏幕截图显示了此示例的部分执行:

还有更多...
如果您使用ForkJoinTask类的completeExceptionally()方法而不是抛出异常,则可以获得与示例中相同的结果。代码将如下所示:
Exception e=new Exception("This task throws an Exception: "+ "Task from "+start+" to "+end);
completeExceptionally(e);
另请参阅
- 在第五章的创建 Fork/Join 池示例中,Fork/Join Framework
取消任务
当您在ForkJoinPool类中执行ForkJoinTask对象时,可以在它们开始执行之前取消它们。ForkJoinTask类提供了cancel()方法来实现此目的。当您想要取消一个任务时,有一些要考虑的要点,如下所示:
-
ForkJoinPool类没有提供任何方法来取消它正在运行或等待在池中的所有任务 -
当您取消一个任务时,不会取消该任务执行的任务
在本示例中,您将实现取消ForkJoinTask对象的示例。您将在数组中查找一个数字的位置。找到数字的第一个任务将取消其余任务。由于 Fork/Join 框架没有提供此功能,您将实现一个辅助类来执行此取消操作。
准备就绪...
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE NetBeans,请打开它并创建一个新的 Java 项目
如何做...
按照以下步骤实现示例:
- 创建一个名为
ArrayGenerator的类。该类将生成指定大小的随机整数数组。实现一个名为generateArray()的方法。它将生成数字数组。它接收数组的大小作为参数。
public class ArrayGenerator {
public int[] generateArray(int size) {
int array[]=new int[size];
Random random=new Random();
for (int i=0; i<size; i++){
array[i]=random.nextInt(10);
}
return array;
}
- 创建一个名为
TaskManager的类。我们将使用这个类来存储在示例中使用的ForkJoinPool中执行的所有任务。由于ForkJoinPool和ForkJoinTask类的限制,您将使用此类来取消ForkJoinPool类的所有任务。
public class TaskManager {
- 声明一个参数化为
ForkJoinTask类参数化为Integer类的对象列表,命名为List。
private List<ForkJoinTask<Integer>> tasks;
- 实现类的构造函数。它初始化任务列表。
public TaskManager(){
tasks=new ArrayList<>();
}
- 实现
addTask()方法。它将一个ForkJoinTask对象添加到任务列表中。
public void addTask(ForkJoinTask<Integer> task){
tasks.add(task);
}
- 实现
cancelTasks()方法。它将使用cancel()方法取消列表中存储的所有ForkJoinTask对象。它接收一个要取消其余任务的ForkJoinTask对象作为参数。该方法取消所有任务。
public void cancelTasks(ForkJoinTask<Integer> cancelTask){
for (ForkJoinTask<Integer> task :tasks) {
if (task!=cancelTask) {
task.cancel(true);
((SearchNumberTask)task).writeCancelMessage();
}
}
}
- 实现
SearchNumberTask类。指定它扩展了参数化为Integer类的RecursiveTask类。该类将在整数数组的元素块中查找一个数字。
public class SearchNumberTask extends RecursiveTask<Integer> {
- 声明一个名为
array的私有int数字数组。
private int numbers[];
- 声明两个私有的
int属性,命名为start和end。这些属性将确定该任务需要处理的数组元素。
private int start, end;
- 声明一个名为
number的私有int属性,用于存储要查找的数字。
private int number;
- 声明一个名为
manager的私有TaskManager属性。您将使用此对象来取消所有任务。
private TaskManager manager;
- 声明一个私有的
int常量,并将其初始化为-1值。当任务找不到数字时,它将是任务的返回值。
private final static int NOT_FOUND=-1;
- 实现类的构造函数以初始化其属性。
public Task(int numbers[], int start, int end, int number, TaskManager manager){
this.numbers=numbers;
this.start=start;
this.end=end;
this.number=number;
this.manager=manager;
}
- 实现
compute()方法。开始方法时,向控制台写入一条消息,指示start和end属性的值。
@Override
protected Integer compute() {
System.out.println("Task: "+start+":"+end);
- 如果
start和end属性之间的差异大于 10(任务需要处理数组的元素超过 10 个),则调用launchTasks()方法将该任务的工作分成两个子任务。
int ret;
if (end-start>10) {
ret=launchTasks();
- 否则,在调用
lookForNumber()方法的任务所处理的数组块中查找数字。
} else {
ret=lookForNumber();
}
- 返回任务的结果。
return ret;
- 实现
lookForNumber()方法。
private int lookForNumber() {
- 对于该任务需要处理的元素块中的所有元素,将存储在该元素中的值与要查找的数字进行比较。如果它们相等,向控制台写入一条消息,指示在这种情况下使用
TaskManager对象的cancelTasks()方法来取消所有任务,并返回找到数字的元素位置。
for (int i=start; i<end; i++){
if (array[i]==number) {
System.out.printf("Task: Number %d found in position %d\n",number,i);
manager.cancelTasks(this);
return i;
}
- 在循环内,使任务休眠一秒钟。
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 最后,返回
-1值。
return NOT_FOUND;
}
- 实现
launchTasks()方法。首先,将这些任务需要处理的数字块分成两部分,然后创建两个Task对象来处理它们。
private int launchTasks() {
int mid=(start+end)/2;
Task task1=new Task(array,start,mid,number,manager);
Task task2=new Task(array,mid,end,number,manager);
- 将任务添加到
TaskManager对象。
manager.addTask(task1);
manager.addTask(task2);
- 使用
fork()方法异步执行这两个任务。
task1.fork();
task2.fork();
- 等待任务完成并返回第一个任务的结果(如果不同,则返回
-1),或第二个任务的结果。
int returnValue;
returnValue=task1.join();
if (returnValue!=-1) {
return returnValue;
}
returnValue=task2.join();
return returnValue;
- 实现
writeCancelMessage()方法,在任务被取消时写一条消息。
public void writeCancelMessage(){
System.out.printf("Task: Canceled task from %d to %d",start,end);
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 使用
ArrayGenerator类创建一个包含 1,000 个数字的数组。
ArrayGenerator generator=new ArrayGenerator();
int array[]=generator.generateArray(1000);
- 创建一个
TaskManager对象。
TaskManager manager=new TaskManager();
- 使用默认构造函数创建一个
ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 创建一个
Task对象来处理之前生成的数组。
Task task=new Task(array,0,1000,5,manager);
- 使用
execute()方法在池中异步执行任务。
pool.execute(task);
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
ForkJoinPool类的awaitTermination()方法等待任务的完成。
try {
pool.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 在控制台上写一条消息,指示程序的结束。
System.out.printf("Main: The program has finished\n");
它是如何工作的...
ForkJoinTask类提供了cancel()方法,允许您在任务尚未执行时取消任务。这是一个非常重要的点。如果任务已经开始执行,调用cancel()方法将没有效果。该方法接收一个名为mayInterruptIfRunning的Boolean值作为参数。这个名字可能会让你觉得,如果你向方法传递true值,即使任务正在运行,任务也会被取消。Java API 文档指定,在ForkJoinTask类的默认实现中,这个属性没有效果。任务只有在尚未开始执行时才会被取消。取消任务对该任务发送到池中的任务没有影响。它们会继续执行。
Fork/Join 框架的一个限制是它不允许取消ForkJoinPool中的所有任务。为了克服这个限制,您已经实现了TaskManager类。它存储了所有发送到池中的任务。它有一个方法可以取消它存储的所有任务。如果一个任务无法取消,因为它正在运行或已经完成,cancel()方法会返回false值,因此您可以尝试取消所有任务而不必担心可能的副作用。
在示例中,您已经实现了一个任务,该任务在数字数组中查找一个数字。您按照 Fork/Join 框架的建议将问题分解为更小的子问题。您只对数字的一个出现感兴趣,所以当您找到它时,取消其他任务。
以下截图显示了此示例的部分执行:

另请参阅
- 在第五章的创建 Fork/Join 池配方中,Fork/Join Framework
第六章:并发集合
在本章中,我们将涵盖:
-
使用非阻塞线程安全列表
-
使用阻塞线程安全列表
-
使用按优先级排序的阻塞线程安全列表
-
使用带延迟元素的线程安全列表
-
使用线程安全的可导航映射
-
生成并发随机数
-
使用原子变量
-
使用原子数组
介绍
数据结构是编程中的基本元素。几乎每个程序都使用一种或多种类型的数据结构来存储和管理它们的数据。Java API 提供了Java 集合框架,其中包含接口、类和算法,实现了许多不同的数据结构,您可以在程序中使用。
当您需要在并发程序中处理数据集合时,必须非常小心地选择实现。大多数集合类都不准备与并发应用程序一起工作,因为它们无法控制对其数据的并发访问。如果一些并发任务共享一个不准备与并发任务一起工作的数据结构,您可能会遇到数据不一致的错误,这将影响程序的正确运行。这种数据结构的一个例子是ArrayList类。
Java 提供了可以在并发程序中使用的数据集合,而不会出现任何问题或不一致。基本上,Java 提供了两种在并发应用程序中使用的集合:
-
阻塞集合:这种类型的集合包括添加和删除数据的操作。如果操作无法立即完成,因为集合已满或为空,进行调用的线程将被阻塞,直到操作可以完成。
-
非阻塞集合:这种类型的集合还包括添加和删除数据的操作。如果操作无法立即完成,操作将返回
null值或抛出异常,但进行调用的线程不会被阻塞。
通过本章的示例,您将学习如何在并发应用程序中使用一些 Java 集合。这包括:
-
非阻塞列表,使用
ConcurrentLinkedDeque类 -
使用
LinkedBlockingDeque类的阻塞列表 -
使用
LinkedTransferQueue类的阻塞列表与数据的生产者和消费者一起使用 -
通过
PriorityBlockingQueue对其元素按优先级排序的阻塞列表 -
使用
DelayQueue类的带延迟元素的阻塞列表 -
使用
ConcurrentSkipListMap类的非阻塞可导航映射 -
随机数,使用
ThreadLocalRandom类 -
原子变量,使用
AtomicLong和AtomicIntegerArray类
使用非阻塞线程安全列表
最基本的集合是列表。列表具有不确定数量的元素,您可以在任何位置添加、读取或删除元素。并发列表允许各个线程同时在列表中添加或删除元素,而不会产生任何数据不一致。
在本示例中,您将学习如何在并发程序中使用非阻塞列表。非阻塞列表提供操作,如果操作无法立即完成(例如,您想获取列表的元素,而列表为空),它们会抛出异常或返回null值,具体取决于操作。Java 7 引入了实现非阻塞并发列表的ConcurrentLinkedDeque类。
我们将实现一个示例,其中包括以下两个不同的任务:
-
一个大量向列表中添加数据的任务
-
一个大量从同一列表中删除数据的任务
准备工作
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
AddTask的类,并指定它实现Runnable接口。
public class AddTask implements Runnable {
- 声明一个参数为
String类的私有ConcurrentLinkedDeque属性,命名为list。
private ConcurrentLinkedDeque<String> list;
- 实现类的构造函数以初始化其属性。
public AddTask(ConcurrentLinkedDeque<String> list) {
this.list=list;
}
- 实现类的
run()方法。它将在列表中存储 10,000 个带有执行任务的线程名称和数字的字符串。
@Override
public void run() {
String name=Thread.currentThread().getName();
for (int i=0; i<10000; i++){
list.add(name+": Element "+i);
}
}
- 创建一个名为
PollTask的类,并指定它实现Runnable接口。
public class PollTask implements Runnable {
- 声明一个参数为
String类的私有ConcurrentLinkedDeque属性,命名为list。
private ConcurrentLinkedDeque<String> list;
- 实现类的构造函数以初始化其属性。
public PollTask(ConcurrentLinkedDeque<String> list) {
this.list=list;
}
- 实现类的
run()方法。它以 5,000 步的循环方式从列表中取出 10,000 个元素,每步取出两个元素。
@Override
public void run() {
for (int i=0; i<5000; i++) {
list.pollFirst();
list.pollLast();
}
}
- 通过创建一个名为
Main的类并添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个参数为
String类的ConcurrentLinkedDeque对象,命名为list。
ConcurrentLinkedDeque<String> list=new ConcurrentLinkedDeque<>();
- 创建一个包含 100 个
Thread对象的数组,命名为threads。
Thread threads[]=new Thread[100];
- 创建 100 个
AddTask对象和一个线程来运行每个对象。将每个线程存储在之前创建的数组中,并启动这些线程。
for (int i=0; i<threads.length ; i++){
AddTask task=new AddTask(list);
threads[i]=new Thread(task);
threads[i].start();
}
System.out.printf("Main: %d AddTask threads have been launched\n",threads.length);
- 使用
join()方法等待线程的完成。
for (int i=0; i<threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 在控制台中写入列表的大小。
System.out.printf("Main: Size of the List: %d\n",list.size());
- 创建 100 个
PollTask对象和一个线程来运行每个对象。将每个线程存储在之前创建的数组中,并启动这些线程。
for (int i=0; i< threads.length; i++){
PollTask task=new PollTask(list);
threads[i]=new Thread(task);
threads[i].start();
}
System.out.printf("Main: %d PollTask threads have been launched\n",threads.length);
- 使用
join()方法等待线程的完成。
for (int i=0; i<threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 在控制台中写入列表的大小。
System.out.printf("Main: Size of the List: %d\n",list.size());
它是如何工作的...
在本示例中,我们使用了参数为String类的ConcurrentLinkedDeque对象来处理非阻塞并发数据列表。以下屏幕截图显示了此示例执行的输出:

首先,您已经执行了 100 个AddTask任务,向列表中添加元素。这些任务中的每一个都使用add()方法向列表中插入 10,000 个元素。此方法将新元素添加到列表的末尾。当所有这些任务都完成时,您已经在控制台中写入了列表的元素数量。此时,列表中有 1,000,000 个元素。
然后,您已经执行了 100 个PollTask任务来从列表中移除元素。这些任务中的每一个都使用pollFirst()和pollLast()方法从列表中移除 10,000 个元素。pollFirst()方法返回并移除列表的第一个元素,pollLast()方法返回并移除列表的最后一个元素。如果列表为空,这些方法返回一个null值。当所有这些任务都完成时,您已经在控制台中写入了列表的元素数量。此时,列表中没有元素。
要写入列表的元素数量,您已经使用了size()方法。您必须考虑到,这个方法可能会返回一个不真实的值,特别是在有线程向列表中添加或删除数据时。该方法必须遍历整个列表来计算元素的数量,列表的内容可能会因此操作而发生变化。只有在没有任何线程修改列表时使用它们,您才能保证返回的结果是正确的。
还有更多...
ConcurrentLinkedDeque类提供了更多的方法来从列表中获取元素:
-
getFirst()和getLast():这些方法分别返回列表的第一个和最后一个元素。它们不会从列表中移除返回的元素。如果列表为空,这些方法会抛出一个NoSuchElementExcpetion异常。 -
peek(),peekFirst()和peekLast():这些方法分别返回列表的第一个和最后一个元素。它们不会从列表中移除返回的元素。如果列表为空,这些方法返回一个null值。 -
remove(),removeFirst(),removeLast(): 这些方法分别返回列表的第一个和最后一个元素。它们会从列表中删除返回的元素。如果列表为空,这些方法会抛出NoSuchElementException异常。
使用阻塞线程安全列表
最基本的集合是列表。列表有不确定数量的元素,您可以从任何位置添加、读取或删除元素。并发列表允许多个线程同时添加或删除列表中的元素,而不会产生任何数据不一致性。
在本示例中,您将学习如何在并发程序中使用阻塞列表。阻塞列表和非阻塞列表之间的主要区别在于,阻塞列表具有用于插入和删除元素的方法,如果无法立即执行操作,因为列表已满或为空,它们将阻塞进行调用的线程,直到可以执行操作。Java 包括实现阻塞列表的LinkedBlockingDeque类。
您将实现一个示例,其中包括以下两个任务:
-
一个大规模地向列表中添加数据
-
一个大规模地从同一列表中删除数据
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照下面描述的步骤来实现示例:
- 创建一个名为
Client的类,并指定它实现Runnable接口。
public class Client implements Runnable{
- 声明一个私有的
LinkedBlockingDeque属性,命名为requestList,参数化为String类。
private LinkedBlockingDeque<String> requestList;
- 实现类的构造函数以初始化其属性。
public Client (LinkedBlockingDeque<String> requestList) {
this.requestList=requestList;
}
- 实现
run()方法。使用requestList对象的put()方法每秒向列表中插入五个String对象。重复该循环三次。
@Override
public void run() {
for (int i=0; i<3; i++) {
for (int j=0; j<5; j++) {
StringBuilder request=new StringBuilder();
request.append(i);
request.append(":");
request.append(j);
try {
requestList.put(request.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Client: %s at %s.\n",request,new Date());
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("Client: End.\n");
}
- 通过创建一个名为
Main的类并向其中添加main()方法,创建示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 声明并创建
String类命名为list的LinkedBlockingDeque。
LinkedBlockingDeque<String> list=new LinkedBlockingDeque<>(3);
- 创建并启动一个
Thread对象来执行客户端任务。
Client client=new Client(list);
Thread thread=new Thread(client);
thread.start();
- 使用列表对象的
take()方法每 300 毫秒获取列表的三个String对象。重复该循环五次。在控制台中写入字符串。
for (int i=0; i<5 ; i++) {
for (int j=0; j<3; j++) {
String request=list.take();
System.out.printf("Main: Request: %s at %s. Size: %d\n",request,new Date(),list.size());
}
TimeUnit.MILLISECONDS.sleep(300);
}
- 编写一条消息以指示程序的结束。
System.out.printf("Main: End of the program.\n");
工作原理...
在本示例中,您已经使用了参数化为String类的LinkedBlockingDeque来处理非阻塞并发数据列表。
Client类使用put()方法向列表中插入字符串。如果列表已满(因为您使用固定容量创建了它),该方法将阻塞其线程的执行,直到列表中有空间。
Main类使用take()方法从列表中获取字符串。如果列表为空,该方法将阻塞其线程的执行,直到列表中有元素为止。
在本示例中使用的LinkedBlockingDeque类的两种方法,如果它们在被阻塞时被中断,可以抛出InterruptedException异常,因此您必须包含必要的代码来捕获该异常。
还有更多...
LinkedBlockingDeque类还提供了用于向列表中放置和获取元素的方法,而不是阻塞,它们会抛出异常或返回null值。这些方法包括:
-
takeFirst()和takeLast(): 这些方法分别返回列表的第一个和最后一个元素。它们会从列表中删除返回的元素。如果列表为空,这些方法会阻塞线程,直到列表中有元素。 -
getFirst()和getLast(): 这些方法分别返回列表中的第一个和最后一个元素。它们不会从列表中删除返回的元素。如果列表为空,这些方法会抛出NoSuchElementExcpetion异常。 -
peek()、peekFirst()和peekLast():这些方法分别返回列表的第一个和最后一个元素。它们不会从列表中删除返回的元素。如果列表为空,这些方法返回一个null值。 -
poll()、pollFirst()和pollLast():这些方法分别返回列表的第一个和最后一个元素。它们从列表中删除返回的元素。如果列表为空,这些方法返回一个null值。 -
add()、addFirst()、addLast():这些方法分别在第一个和最后一个位置添加一个元素。如果列表已满(你使用固定容量创建了它),这些方法会抛出IllegalStateException异常。
另请参阅
- 第六章中的使用非阻塞线程安全列表配方,并发集合
使用按优先级排序的阻塞线程安全列表
在使用数据结构时,通常需要有一个有序列表。Java 提供了具有这种功能的PriorityBlockingQueue。
你想要添加到PriorityBlockingQueue中的所有元素都必须实现Comparable接口。这个接口有一个方法compareTo(),它接收一个相同类型的对象,所以你有两个对象可以比较:执行该方法的对象和作为参数接收的对象。如果本地对象小于参数,则该方法必须返回小于零的数字,如果本地对象大于参数,则返回大于零的数字,如果两个对象相等,则返回零。
当你向PriorityBlockingQueue中插入一个元素时,它会使用compareTo()方法来确定插入元素的位置。较大的元素将成为队列的尾部。
PriorityBlockingQueue的另一个重要特性是它是一个阻塞数据结构。它有一些方法,如果它们不能立即执行操作,就会阻塞线程,直到它们可以执行为止。
在这个示例中,你将学习如何使用PriorityBlockingQueue类来实现一个示例,其中你将在同一个列表中存储许多具有不同优先级的事件,以检查队列是否按照你的要求排序。
准备工作
这个示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Event的类,并指定它实现了参数化为Event类的Comparable接口。
public class Event implements Comparable<Event> {
- 声明一个私有的
int属性,命名为thread,用于存储创建事件的线程号。
private int thread;
- 声明一个私有的
int属性,命名为priority,用于存储事件的优先级。
private int priority;
- 实现类的构造函数以初始化其属性。
public Event(int thread, int priority){
this.thread=thread;
this.priority=priority;
}
- 实现
getThread()方法以返回线程属性的值。
public int getThread() {
return thread;
}
- 实现
getPriority()方法以返回优先级属性的值。
public int getPriority() {
return priority;
}
- 实现
compareTo()方法。它接收Event作为参数,并比较当前事件和接收的参数的优先级。如果当前事件的优先级较大,则返回-1,如果两个优先级相等,则返回0,如果当前事件的优先级较小,则返回1。请注意,这与大多数Comparator.compareTo()实现相反。
@Override
public int compareTo(Event e) {
if (this.priority>e.getPriority()) {
return -1;
} else if (this.priority<e.getPriority()) {
return 1;
} else {
return 0;
}
}
- 创建一个名为
Task的类,并指定它实现了Runnable接口。
public class Task implements Runnable {
- 声明一个私有的
int属性,命名为id,用于存储标识任务的编号。
private int id;
- 声明一个私有的参数化为
Event类的PriorityBlockingQueue属性,命名为queue,用于存储任务生成的事件。
private PriorityBlockingQueue<Event> queue;
- 实现类的构造函数以初始化其属性。
public Task(int id, PriorityBlockingQueue<Event> queue) {
this.id=id;
this.queue=queue;
}
- 实现
run()方法。它使用其 ID 将 1000 个事件存储在队列中,以标识创建事件的任务,并为它们分配一个递增的优先级数字。使用add()方法将事件存储在队列中。
@Override
public void run() {
for (int i=0; i<1000; i++){
Event event=new Event(id,i);
queue.add(event);
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main{
public static void main(String[] args) {
- 创建一个使用
Event类参数化的PriorityBlockingQueue对象,命名为queue。
PriorityBlockingQueue<Event> queue=new PriorityBlockingQueue<>();
- 创建一个包含五个
Thread对象的数组,用于存储将执行五个任务的线程。
Thread taskThreads[]=new Thread[5];
- 创建五个
Task对象。将线程存储在先前创建的数组中。
for (int i=0; i<taskThreads.length; i++){
Task task=new Task(i,queue);
taskThreads[i]=new Thread(task);
}
- 启动先前创建的五个线程。
for (int i=0; i<taskThreads.length ; i++) {
taskThreads[i].start();
}
- 使用
join()方法等待五个线程的完成。
for (int i=0; i<taskThreads.length ; i++) {
try {
taskThreads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 向控制台写入队列的实际大小和其中存储的事件。使用
poll()方法从队列中取出事件。
System.out.printf("Main: Queue Size: %d\n",queue.size());
for (int i=0; i<taskThreads.length*1000; i++){
Event event=queue.poll();
System.out.printf("Thread %s: Priority %d\n",event.getThread(),event.getPriority());
}
- 向控制台写入队列的最终大小的消息。
System.out.printf("Main: Queue Size: %d\n",queue.size());
System.out.printf("Main: End of the program\n");
它是如何工作的...
在这个例子中,您已经使用PriorityBlockingQueue实现了一个Event对象的优先级队列。正如我们在介绍中提到的,存储在PriorityBlockingQueue中的所有元素都必须实现Comparable接口,因此您已经在 Event 类中实现了compareTo()方法。
所有事件都有一个优先级属性。具有更高优先级值的元素将成为队列中的第一个元素。当您实现了compareTo()方法时,如果执行该方法的事件具有比作为参数传递的事件的优先级更高的优先级,则返回-1作为结果。在另一种情况下,如果执行该方法的事件具有比作为参数传递的事件的优先级更低的优先级,则返回1作为结果。如果两个对象具有相同的优先级,则compareTo()方法返回0值。在这种情况下,PriorityBlockingQueue类不能保证元素的顺序。
我们已经实现了Task类,以将Event对象添加到优先级队列中。每个任务对象向队列中添加 1000 个事件,优先级在 0 到 999 之间,使用add()方法。
Main类的main()方法创建了五个Task对象,并在相应的线程中执行它们。当所有线程都完成执行时,您已经将所有元素写入控制台。为了从队列中获取元素,我们使用了poll()方法。该方法返回并删除队列中的第一个元素。
以下屏幕截图显示了程序执行的部分输出:

您可以看到队列有 5000 个元素,并且前几个元素具有最大的优先级值。
还有更多...
PriorityBlockingQueue类还有其他有趣的方法。以下是其中一些的描述:
-
clear(): 此方法删除队列的所有元素。 -
take(): 此方法返回并删除队列的第一个元素。如果队列为空,该方法将阻塞其线程,直到队列有元素。 -
put(E``e):E是用于参数化PriorityBlockingQueue类的类。此方法将传递的元素插入队列。 -
peek(): 此方法返回队列的第一个元素,但不删除它。
另请参阅
- 第六章中的使用阻塞线程安全列表配方,并发集合
使用具有延迟元素的线程安全列表
Java API 提供的一个有趣的数据结构,您可以在并发应用程序中使用,是在DelayedQueue类中实现的。在这个类中,您可以存储具有激活日期的元素。返回或提取队列元素的方法将忽略那些数据在未来的元素。它们对这些方法是不可见的。
为了获得这种行为,您想要存储在DelayedQueue类中的元素必须实现Delayed接口。此接口允许您处理延迟对象,因此您将实现存储在DelayedQueue类中的对象的激活日期作为激活日期之间的剩余时间。此接口强制实现以下两种方法:
-
compareTo(Delayed o):Delayed接口扩展了Comparable接口。如果执行该方法的对象的延迟小于作为参数传递的对象,则此方法将返回小于零的值;如果执行该方法的对象的延迟大于作为参数传递的对象,则返回大于零的值;如果两个对象的延迟相同,则返回零值。 -
getDelay(TimeUnit unit):此方法必须返回直到指定单位的激活日期剩余的时间。TimeUnit类是一个枚举,具有以下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
在此示例中,您将学习如何使用DelayedQueue类,其中存储了具有不同激活日期的一些事件。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
Event的类,并指定它实现Delayed接口。
public class Event implements Delayed {
- 声明一个名为
startDate的私有Date属性。
private Date startDate;
- 实现类的构造函数以初始化其属性。
public Event (Date startDate) {
this.startDate=startDate;
}
- 实现
compareTo()方法。它接收一个Delayed对象作为参数。返回当前对象的延迟与传递的参数之间的差异。
@Override
public int compareTo(Delayed o) {
long result=this.getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS);
if (result<0) {
return -1;
} else if (result>0) {
return 1;
}
return 0;
}
- 实现
getDelay()方法。以作为参数接收的TimeUnit返回对象的startDate和实际Date之间的差异。
public long getDelay(TimeUnit unit) {
Date now=new Date();
long diff=startDate.getTime()-now.getTime();
return unit.convert(diff,TimeUnit.MILLISECONDS);
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
id的私有int属性,用于存储标识此任务的数字。
private int id;
- 声明一个名为
queue的私有参数化为Event类的DelayQueue属性。
private DelayQueue<Event> queue;
- 实现类的构造函数以初始化其属性。
public Task(int id, DelayQueue<Event> queue) {
this.id=id;
this.queue=queue;
}
- 实现
run()方法。首先,计算此任务将创建的事件的激活日期。将实际日期增加等于对象 ID 的秒数。
@Override
public void run() {
Date now=new Date();
Date delay=new Date();
delay.setTime(now.getTime()+(id*1000));
System.out.printf("Thread %s: %s\n",id,delay);
- 使用
add()方法将 100 个事件存储在队列中。
for (int i=0; i<100; i++) {
Event event=new Event(delay);
queue.add(event);
}
}
- 通过创建名为
Main的类并向其添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个参数化为
Event类的DelayedQueue对象。
DelayQueue<Event> queue=new DelayQueue<>();
- 创建一个包含五个
Thread对象的数组,用于存储要执行的任务。
Thread threads[]=new Thread[5];
- 创建五个具有不同 ID 的
Task对象。
for (int i=0; i<threads.length; i++){
Task task=new Task(i+1, queue);
threads[i]=new Thread(task);
}
- 启动先前创建的所有五个任务。
for (int i=0; i<threads.length; i++) {
threads[i].start();
}
- 使用
join()方法等待线程的完成。
for (int i=0; i<threads.length; i++) {
threads[i].join();
}
- 将存储在队列中的事件写入控制台。当队列的大小大于零时,使用
poll()方法获取一个Event类。如果返回null,则将主线程等待 500 毫秒以等待更多事件的激活。
do {
int counter=0;
Event event;
do {
event=queue.poll();
if (event!=null) counter++;
} while (event!=null);
System.out.printf("At %s you have read %d events\n",new Date(),counter);
TimeUnit.MILLISECONDS.sleep(500);
} while (queue.size()>0);
}
}
它是如何工作的...
在此示例中,我们已经实现了Event类。该类具有一个唯一的属性,即事件的激活日期,并实现了Delayed接口,因此您可以将Event对象存储在DelayedQueue类中。
getDelay()方法返回激活日期和实际日期之间的纳秒数。这两个日期都是Date类的对象。您已经使用了getTime()方法,该方法返回转换为毫秒的日期,然后将该值转换为作为参数接收的TimeUnit。DelayedQueue类以纳秒为单位工作,但在这一点上,对您来说是透明的。
如果执行方法的对象的延迟小于作为参数传递的对象的延迟,则compareTo()方法返回小于零的值,如果执行方法的对象的延迟大于作为参数传递的对象的延迟,则返回大于零的值,并且如果两个延迟相等,则返回0值。
您还实现了Task类。此类具有名为id的integer属性。执行Task对象时,它将与任务的 ID 相等的秒数添加到实际日期,并且这是由此任务在DelayedQueue类中存储的事件的激活日期。每个Task对象使用add()方法在队列中存储 100 个事件。
最后,在Main类的main()方法中,您创建了五个Task对象并在相应的线程中执行它们。当这些线程完成执行时,您使用poll()方法将所有事件写入控制台。该方法检索并删除队列的第一个元素。如果队列没有任何活动元素,则该方法返回null值。您调用poll()方法,如果它返回一个Event类,则增加一个计数器。当poll()方法返回null值时,您将计数器的值写入控制台,并使线程休眠半秒钟以等待更多活动事件。当您获得队列中存储的 500 个事件时,程序的执行结束。
以下屏幕截图显示了程序执行的部分输出:

您可以看到程序在激活时仅获取 100 个事件。
注意
您必须非常小心使用size()方法。它返回包括活动和非活动元素的列表中的元素总数。
还有更多...
DelayQueue类还有其他有趣的方法,如下所示:
-
clear(): 此方法删除队列的所有元素。 -
offer(E``e):E表示用于参数化DelayQueue类的类。此方法将作为参数传递的元素插入队列。 -
peek(): 此方法检索但不删除队列的第一个元素。 -
take(): 此方法检索并删除队列的第一个元素。如果队列中没有任何活动元素,则执行该方法的线程将被阻塞,直到线程有一些活动元素为止。
另请参阅
- 第六章中的使用阻塞线程安全列表食谱,并发集合
使用线程安全的可导航映射
Java API 提供的一个有趣的数据结构,您可以在并发程序中使用,由ConcurrentNavigableMap接口定义。实现ConcurrentNavigableMap接口的类在两个部分中存储元素:
-
唯一标识元素的键
-
定义元素的其余数据
每个部分必须在不同的类中实现。
Java API 还提供了一个实现该接口的类,即实现具有ConcurrentNavigableMap接口行为的非阻塞列表的ConcurrentSkipListMap接口。在内部,它使用Skip List来存储数据。跳表是一种基于并行列表的数据结构,允许我们获得类似于二叉树的效率。使用它,您可以获得一个排序的数据结构,其插入、搜索或删除元素的访问时间比排序列表更好。
注意
Skip List 由 William Pugh 于 1990 年引入。
当您在映射中插入元素时,它使用键对它们进行排序,因此所有元素都将被排序。该类还提供了一些方法来获取映射的子映射,以及返回具体元素的方法。
在本食谱中,您将学习如何使用ConcurrentSkipListMap类来实现联系人映射。
准备就绪
这个示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE 如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Contact的类。
public class Contact {
- 声明两个私有的
String属性,命名为name和phone。
private String name;
private String phone;
- 实现类的构造函数以初始化其属性。
public Contact(String name, String phone) {
this.name=name;
this.phone=phone;
}
- 实现方法来返回
name和phone属性的值。
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 声明一个私有的
ConcurrentSkipListMap属性,参数化为String和Contact类,命名为map。
private ConcurrentSkipListMap<String, Contact> map;
- 声明一个私有的
String属性,命名为id,用于存储当前任务的 ID。
private String id;
- 实现类的构造函数以存储其属性。
public Task (ConcurrentSkipListMap<String, Contact> map, String id) {
this.id=id;
this.map=map;
}
- 实现
run()方法。它使用任务的 ID 和递增数字来创建 1,000 个不同的联系人,并使用put()方法将联系人存储在地图中。
@Override
public void run() {
for (int i=0; i<1000; i++) {
Contact contact=new Contact(id, String.valueOf(i+1000));
map.put(id+contact.getPhone(), contact);
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个参数为
String和Conctact类的ConcurrentSkipListMap对象,命名为map。
ConcurrentSkipListMap<String, Contact> map;
map=new ConcurrentSkipListMap<>();
- 创建一个包含 25 个
Thread对象的数组,用于存储所有要执行的Task对象。
Thread threads[]=new Thread[25];
int counter=0;
- 创建并启动 25 个任务对象,为每个任务分配一个大写字母作为 ID。
for (char i='A'; i<'Z'; i++) {
Task task=new Task(map, String.valueOf(i));
threads[counter]=new Thread(task);
threads[counter].start();
counter++;
}
- 使用
join()方法等待线程的完成。
for (int i=0; i<25; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 使用
firstEntry()方法获取地图的第一个条目。将其数据写入控制台。
System.out.printf("Main: Size of the map: %d\n",map.size());
Map.Entry<String, Contact> element;
Contact contact;
element=map.firstEntry();
contact=element.getValue();
System.out.printf("Main: First Entry: %s: %s\n",contact.getName(),contact.getPhone());
- 使用
lastEntry()方法获取地图的最后一个条目。将其数据写入控制台。
element=map.lastEntry();
contact=element.getValue();
System.out.printf("Main: Last Entry: %s: %s\n",contact.getName(),contact.getPhone());
- 使用
subMap()方法获取地图的子地图。将它们的数据写入控制台。
System.out.printf("Main: Submap from A1996 to B1002: \n");
ConcurrentNavigableMap<String, Contact> submap=map.subMap("A1996", "B1002");
do {
element=submap.pollFirstEntry();
if (element!=null) {
contact=element.getValue();
System.out.printf("%s: %s\n",contact.getName(),contact.getPhone());
}
} while (element!=null);
}
它是如何工作的...
在这个示例中,我们实现了一个Task类来存储可导航地图中的Contact对象。每个联系人都有一个名称,即创建它的任务的 ID,以及一个电话号码,即 1,000 到 2,000 之间的数字。我们使用这些值的连接作为联系人的键。每个Task对象创建 1,000 个联系人,这些联系人使用put()方法存储在可导航地图中。
注意
如果你插入一个具有在地图中存在的键的元素,那么与该键关联的元素将被新元素替换。
Main类的main()方法创建了 25 个Task对象,使用字母 A 到 Z 作为 ID。然后,你使用了一些方法来从地图中获取数据。firstEntry()方法返回一个带有地图第一个元素的Map.Entry对象。这个方法不会从地图中移除元素。该对象包含键和元素。要获取元素,你调用了getValue()方法。你可以使用getKey()方法来获取该元素的键。
lastEntry()方法返回一个带有地图最后一个元素的Map.Entry对象,而subMap()方法返回一个ConcurrentNavigableMap对象,其中包含地图部分元素,即具有键在A1996和B1002之间的元素。在这种情况下,你使用了pollFirst()方法来处理subMap()方法的元素。该方法返回并移除子地图的第一个Map.Entry对象。
以下截图显示了程序执行的输出:

还有更多...
ConcurrentSkipListMap类还有其他有趣的方法。以下是其中一些:
-
headMap(K``toKey):K是在ConcurrentSkipListMap对象的参数化中使用的键值的类。这个方法返回地图的第一个元素的子地图,其中包含具有小于传递的键的元素。 -
tailMap(K``fromKey):K是用于ConcurrentSkipListMap对象参数化的键值的类。此方法返回具有大于传递的键的元素的子映射。 -
putIfAbsent(K``key,``V``Value): 如果键在映射中不存在,则此方法将使用指定的键作为参数插入指定的值作为参数。 -
pollLastEntry(): 此方法返回并删除映射的最后一个元素的Map.Entry对象。 -
replace(K``key,``V``Value): 如果指定的键存在于映射中,此方法将替换与参数指定的键关联的值。
参见
- 第六章中的使用非阻塞线程安全列表食谱,并发集合
生成并发随机数
Java 并发 API 提供了一个特定的类来在并发应用程序中生成伪随机数。它是ThreadLocalRandom类,它是 Java 7 版本中的新功能。它的工作方式类似于线程本地变量。想要生成随机数的每个线程都有一个不同的生成器,但所有这些生成器都是从同一个类中管理的,对程序员来说是透明的。通过这种机制,您将获得比使用共享的Random对象来生成所有线程的随机数更好的性能。
在这个示例中,您将学习如何使用ThreadLocalRandom类在并发应用程序中生成随机数。
准备就绪
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
TaskLocalRandom的类,并指定它实现Runnable接口。
public class TaskLocalRandom implements Runnable {
- 实现类的构造函数。使用它来使用
current()方法将随机数生成器初始化为实际线程。
public TaskLocalRandom() {
ThreadLocalRandom.current();
}
- 实现
run()方法。获取执行此任务的线程的名称,并使用nextInt()方法将 10 个随机整数写入控制台。
@Override
public void run() {
String name=Thread.currentThread().getName();
for (int i=0; i<10; i++){
System.out.printf("%s: %d\n",name,ThreadLocalRandom.current().nextInt(10));
}
}
- 通过创建名为
Main的类并向其添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 为三个
Thread对象创建一个数组。
Thread threads[]=new Thread[3];
- 创建并启动三个
TaskLocalRandom任务。将线程存储在之前创建的数组中。
for (int i=0; i<3; i++) {
TaskLocalRandom task=new TaskLocalRandom();
threads[i]=new Thread(task);
threads[i].start();
}
它是如何工作的...
这个示例的关键在于TaskLocalRandom类。在类的构造函数中,我们调用了ThreadLocalRandom类的current()方法。这是一个返回与当前线程关联的ThreadLocalRandom对象的静态方法,因此您可以使用该对象生成随机数。如果调用该方法的线程尚未关联任何对象,则该类将创建一个新对象。在这种情况下,您可以使用此方法初始化与此任务关联的随机生成器,因此它将在下一次调用该方法时创建。
在TaskLocalRandom类的run()方法中,调用current()方法以获取与此线程关联的随机生成器,还调用nextInt()方法并传递数字 10 作为参数。此方法将返回 0 到 10 之间的伪随机数。每个任务生成 10 个随机数。
还有更多...
ThreadLocalRandom类还提供了生成long、float和double数字以及Boolean值的方法。有一些方法允许您提供一个数字作为参数,以在零和该数字之间生成随机数。其他方法允许您提供两个参数,以在这些数字之间生成随机数。
参见
- 第一章中的使用本地线程变量食谱,线程管理
使用原子变量
原子变量是在 Java 版本 5 中引入的,用于对单个变量进行原子操作。当您使用普通变量时,您在 Java 中实现的每个操作都会被转换为多个指令,这些指令在编译程序时可以被机器理解。例如,当您给变量赋值时,在 Java 中只使用一条指令,但在编译此程序时,此指令会在 JVM 语言中转换为各种指令。当您使用多个共享变量的线程时,这个事实可能会导致数据不一致的错误。
为了避免这些问题,Java 引入了原子变量。当一个线程对原子变量进行操作时,如果其他线程想要对同一个变量进行操作,类的实现会包括一个机制来检查该操作是否一步完成。基本上,该操作获取变量的值,将值更改为本地变量,然后尝试将旧值更改为新值。如果旧值仍然相同,则进行更改。如果不是,则方法重新开始操作。这个操作被称为比较和设置。
原子变量不使用锁或其他同步机制来保护对其值的访问。它们所有的操作都基于比较和设置操作。保证多个线程可以同时使用原子变量而不会产生数据不一致的错误,并且其性能比使用由同步机制保护的普通变量更好。
在这个示例中,您将学习如何使用原子变量来实现一个银行账户和两个不同的任务,一个是向账户添加金额,另一个是从中减去金额。您将在示例的实现中使用AtomicLong类。
准备就绪
这个示例的实现已经使用了 Eclipse IDE。如果您正在使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Account的类来模拟银行账户。
public class Account {
- 声明一个私有的
AtomicLong属性,名为balance,用于存储账户的余额。
private AtomicLong balance;
- 实现类的构造函数以初始化其属性。
public Account(){
balance=new AtomicLong();
}
- 实现一个名为“getBalance()”的方法来返回余额属性的值。
public long getBalance() {
return balance.get();
}
- 实现一个名为“setBalance()”的方法来建立余额属性的值。
public void setBalance(long balance) {
this.balance.set(balance);
}
- 实现一个名为“addAmount()”的方法来增加
balance属性的值。
public void addAmount(long amount) {
this.balance.getAndAdd(amount);
}
- 实现一个名为“substractAmount()”的方法来减少
balance属性的值。
public void subtractAmount(long amount) {
this.balance.getAndAdd(-amount);
}
- 创建一个名为
Company的类,并指定它实现Runnable接口。这个类将模拟公司的付款。
public class Company implements Runnable {
- 声明一个私有的
Account属性,名为account。
private Account account;
- 实现类的构造函数以初始化其属性。
public Company(Account account) {
this.account=account;
}
- 实现任务的“run()”方法。使用账户的“addAmount()”方法使其余额增加 1,000 的 10 次。
@Override
public void run() {
for (int i=0; i<10; i++){
account.addAmount(1000);
}
}
- 创建一个名为
Bank的类,并指定它实现Runnable接口。这个类将模拟从账户中取钱。
public class Bank implements Runnable {
- 声明一个私有的
Account属性,名为account。
private Account account;
- 实现类的构造函数以初始化其属性。
public Bank(Account account) {
this.account=account;
}
- 实现任务的“run()”方法。使用账户的“subtractAmount()”方法使其余额减少 1,000 的 10 次。
@Override
public void run() {
for (int i=0; i<10; i++){
account.subtractAmount(1000);
}
}
- 通过创建一个名为
Main的类并向其添加“main()”方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个
Account对象并将其余额设置为1000。
Account account=new Account();
account.setBalance(1000);
- 创建一个新的
Company任务和一个线程来执行它。
Company company=new Company(account);
Thread companyThread=new Thread(company);
Create a new Bank task and a thread to execute it.
Bank bank=new Bank(account);
Thread bankThread=new Thread(bank);
- 在控制台中写入账户的初始余额。
System.out.printf("Account : Initial Balance: %d\n",account.getBalance());
- 启动线程。
companyThread.start();
bankThread.start();
- 使用“join()”方法等待线程的完成,并在控制台中写入账户的最终余额。
try {
companyThread.join();
bankThread.join();
System.out.printf("Account : Final Balance: %d\n",account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
它是如何工作的...
这个例子的关键在于Account类。在这个类中,我们声明了一个AtomicLong变量,名为balance,用于存储账户的余额,然后我们使用AtomicLong类提供的方法来实现处理这个余额的方法。为了实现getBalance()方法,返回balance属性的值,你使用了AtomicLong类的get()方法。为了实现setBalance()方法,用于设定余额属性的值,你使用了AtomicLong类的set()方法。为了实现addAmount()方法,用于向账户余额添加金额,你使用了AtomicLong类的getAndAdd()方法,该方法返回指定参数的值并将其增加到余额中。最后,为了实现subtractAmount()方法,用于减少balance属性的值,你也使用了getAndAdd()方法。
然后,你实现了两个不同的任务:
-
Company类模拟了一个增加账户余额的公司。该类的每个任务都会增加 1,000 的余额。 -
Bank类模拟了一个银行,银行账户的所有者取出了他的钱。该类的每个任务都会减少 1,000 的余额。
在Main类中,你创建了一个余额为 1,000 的Account对象。然后,你执行了一个银行任务和一个公司任务,所以账户的最终余额必须与初始余额相同。
当你执行程序时,你会看到最终余额与初始余额相同。以下截图显示了此示例的执行输出:

还有更多...
正如我们在介绍中提到的,Java 中还有其他原子类。AtomicBoolean、AtomicInteger和AtomicReference是原子类的其他示例。
另请参阅
- 在第二章的Synchronizing a method示例中,Basic thread synchronization
使用原子数组
当你实现一个并发应用程序,其中有一个或多个对象被多个线程共享时,你必须使用同步机制来保护对其属性的访问,如锁或synchronized关键字,以避免数据不一致错误。
这些机制存在以下问题:
-
死锁:当一个线程被阻塞等待被其他线程锁定的锁,并且永远不会释放它时,就会发生这种情况。这种情况会阻塞程序,因此它永远不会结束。
-
如果只有一个线程访问共享对象,它必须执行必要的代码来获取和释放锁。
为了提供更好的性能,开发了对比交换操作。这个操作实现了对变量值的修改,分为以下三个步骤:
-
你获取了变量的值,这是变量的旧值。
-
你将变量的值更改为临时变量,这是变量的新值。
-
如果旧值等于变量的实际值,你用新值替换旧值。如果另一个线程已更改了变量的值,那么旧值可能与实际值不同。
通过这种机制,你不需要使用任何同步机制,因此可以避免死锁,并获得更好的性能。
Java 在原子变量中实现了这种机制。这些变量提供了compareAndSet()方法,这是对比交换操作的实现以及基于它的其他方法。
Java 还引入了原子数组,为integer或long数字的数组提供原子操作。在这个示例中,你将学习如何使用AtomicIntegerArray类来处理原子数组。
准备就绪
这个配方的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤来实现示例:
- 创建一个名为
Incrementer的类,并指定它实现Runnable接口。
public class Incrementer implements Runnable {
- 声明一个私有的
AtomicIntegerArray属性,名为vector,用于存储一个整数数组。
private AtomicIntegerArray vector;
- 实现类的构造函数以初始化其属性。
public Incrementer(AtomicIntegerArray vector) {
this.vector=vector;
}
- 实现
run()方法。使用getAndIncrement()方法递增数组的所有元素。
@Override
public void run() {
for (int i=0; i<vector.length(); i++){
vector.getAndIncrement(i);
}
}
- 创建一个名为
Decrementer的类,并指定它实现Runnable接口。
public class Decrementer implements Runnable {
- 声明一个私有的
AtomicIntegerArray属性,名为vector,用于存储一个整数数组。
private AtomicIntegerArray vector;
- 实现类的构造函数以初始化其属性。
public Decrementer(AtomicIntegerArray vector) {
this.vector=vector;
}
- 实现
run()方法。使用getAndDecrement()方法递减数组的所有元素。
@Override
public void run() {
for (int i=0; i<vector.length(); i++) {
vector.getAndDecrement(i);
}
}
- 通过创建一个名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 声明一个名为
THREADS的常量,并将其赋值为100。创建一个包含 1,000 个元素的AtomicIntegerArray对象。
final int THREADS=100;
AtomicIntegerArray vector=new AtomicIntegerArray(1000);
- 创建一个
Incrementer任务来处理之前创建的原子数组。
Incrementer incrementer=new Incrementer(vector);
- 创建一个
Decrementer任务来处理之前创建的原子数组。
Decrementer decrementer=new Decrementer(vector);
- 创建两个数组来存储 100 个线程对象。
Thread threadIncrementer[]=new Thread[THREADS];
Thread threadDecrementer[]=new Thread[THREADS];
- 创建并启动 100 个线程来执行
Incrementer任务,另外启动 100 个线程来执行Decrementer任务。将线程存储在之前创建的数组中。
for (int i=0; i<THREADS; i++) {
threadIncrementer[i]=new Thread(incrementer);
threadDecrementer[i]=new Thread(decrementer);
threadIncrementer[i].start();
threadDecrementer[i].start();
}
- 等待线程的完成,使用
join()方法。
for (int i=0; i<100; i++) {
try {
threadIncrementer[i].join();
threadDecrementer[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 在控制台中打印出原子数组中不为零的元素。使用
get()方法来获取原子数组的元素。
for (int i=0; i<vector.length(); i++) {
if (vector.get(i)!=0) {
System.out.println("Vector["+i+"] : "+vector.get(i));
}
}
- 在控制台中写入一条消息,指示示例的完成。
System.out.println("Main: End of the example");
它是如何工作的...
在这个示例中,你已经实现了两个不同的任务来处理AtomicIntegerArray对象:
-
Incrementer任务:这个类使用getAndIncrement()方法递增数组的所有元素 -
Decrementer任务:这个类使用getAndDecrement()方法递减数组的所有元素
在Main类中,你已经创建了一个包含 1,000 个元素的AtomicIntegerArray,然后执行了 100 个增量器和 100 个减量器任务。在这些任务结束时,如果没有不一致的错误,数组的所有元素必须具有值0。如果你执行程序,你会看到程序只会在控制台中写入最终消息,因为所有元素都是零。
还有更多...
现在,Java 只提供了另一个原子数组类。它是AtomicLongArray类,提供了与IntegerAtomicArray类相同的方法。
这些类提供的其他有趣的方法是:
-
get(int``i): 返回由参数指定的数组位置的值 -
set(int``I,``int``newValue): 建立由参数指定的数组位置的值。
另请参阅
- 使用原子变量配方在第六章, 并发集合
第七章:自定义并发类
在本章中,我们将涵盖:
-
自定义
ThreadPoolExecutor类 -
实现基于优先级的
Executor类 -
实现
ThreadFactory接口以生成自定义线程 -
在
Executor对象中使用我们的ThreadFactory -
自定义在计划线程池中运行的任务
-
实现
ThreadFactory接口以为 Fork/Join 框架生成自定义线程 -
自定义在 Fork/Join 框架中运行的任务
-
实现自定义
Lock类 -
基于优先级实现传输队列
-
实现自己的原子对象
介绍
Java 并发 API 提供了许多接口和类来实现并发应用程序。它们提供低级机制,如Thread类、Runnable或Callable接口或synchronized关键字,以及高级机制,如 Executor 框架和 Java 7 版本中添加的 Fork/Join 框架。尽管如此,您可能会发现自己正在开发一个程序,其中没有任何 java 类满足您的需求。
在这种情况下,您可能需要基于 Java 提供的工具来实现自己的自定义并发工具。基本上,您可以:
-
实现一个接口以提供该接口定义的功能。例如,
ThreadFactory接口。 -
重写类的一些方法以使其行为适应您的需求。例如,重写
Thread类的run()方法,默认情况下不执行任何有用的操作,应该重写以提供一些功能。
通过本章的示例,您将学习如何更改一些 Java 并发 API 类的行为,而无需从头设计并发框架。您可以将这些示例作为实现自定义的初始点。
自定义 ThreadPoolExecutor 类
Executor 框架是一种允许您将线程创建与其执行分离的机制。它基于Executor和ExecutorService接口,使用实现了这两个接口的ThreadPoolExecutor类。它具有内部线程池,并提供方法,允许您发送两种类型的任务以在池化线程中执行。这些任务是:
-
Runnable接口以实现不返回结果的任务 -
Callable接口以实现返回结果的任务
在这两种情况下,您只需将任务发送到执行器。执行器使用其池化线程之一或创建一个新线程来执行这些任务。执行器还决定任务执行的时机。
在本示例中,您将学习如何重写ThreadPoolExecutor类的一些方法,以计算在执行器中执行的任务的执行时间,并在执行器完成执行时在控制台中写入有关执行器的统计信息。
准备工作
本示例的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做…
按照下面描述的步骤实现示例:
- 创建一个名为
MyExecutor的类,它扩展了ThreadPoolExecutor类。
public class MyExecutor extends ThreadPoolExecutor {
- 声明一个私有的
ConcurrentHashMap属性,参数化为String和Date类,命名为startTimes。
private ConcurrentHashMap<String, Date> startTimes;
- 实现该类的构造函数。使用
super关键字调用父类的构造函数并初始化startTime属性。
public MyExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
startTimes=new ConcurrentHashMap<>();
}
- 重写
shutdown()方法。在控制台中写入有关已执行任务、正在运行任务和待处理任务的信息。然后,使用super关键字调用父类的shutdown()方法。
@Override
public void shutdown() {
System.out.printf("MyExecutor: Going to shutdown.\n");
System.out.printf("MyExecutor: Executed tasks: %d\n",getCompletedTaskCount());
System.out.printf("MyExecutor: Running tasks: %d\n",getActiveCount());
System.out.printf("MyExecutor: Pending tasks: %d\n",getQueue().size());
super.shutdown();
}
- 重写
shutdownNow()方法。在控制台中写入有关已执行任务、正在运行任务和待处理任务的信息。然后,使用super关键字调用父类的shutdownNow()方法。
@Override
public List<Runnable> shutdownNow() {
System.out.printf("MyExecutor: Going to immediately shutdown.\n");
System.out.printf("MyExecutor: Executed tasks: %d\n",getCompletedTaskCount());
System.out.printf("MyExecutor: Running tasks: %d\n",getActiveCount());
System.out.printf("MyExecutor: Pending tasks: %d\n",getQueue().size());
return super.shutdownNow();
}
- 覆盖
beforeExecute()方法。在控制台中写入将执行任务的线程的名称和任务的哈希码的消息。使用任务的哈希码作为键,将开始日期存储在HashMap中。
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.printf("MyExecutor: A task is beginning: %s : %s\n",t.getName(),r.hashCode());
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
- 覆盖
afterExecute()方法。在控制台中写入任务的结果,并计算任务的运行时间,减去存储在HashMap中的任务的开始日期。
@Override
protected void afterExecute(Runnable r, Throwable t) {
Future<?> result=(Future<?>)r;
try {
System.out.printf("*********************************\n");
System.out.printf("MyExecutor: A task is finishing.\n");
System.out.printf("MyExecutor: Result: %s\n",result.get());
Date startDate=startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate=new Date();
long diff=finishDate.getTime()-startDate.getTime();
System.out.printf("MyExecutor: Duration: %d\n",diff);
System.out.printf("*********************************\n");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
- 创建一个名为
SleepTwoSecondsTask的类,实现带有String类参数的Callable接口。实现call()方法。将当前线程休眠 2 秒,并返回转换为String类型的当前日期。
public class SleepTwoSecondsTask implements Callable<String> {
public String call() throws Exception {
TimeUnit.SECONDS.sleep(2);
return new Date().toString();
}
}
- 通过创建一个名为
Main的主类来实现示例的主要类,其中包含一个main()方法。
public class Main {
public static void main(String[] args) {
- 创建一个名为
myExecutor的MyExecutor对象。
MyExecutor myExecutor=new MyExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());
- 创建一个带有
String类参数的Future对象列表,用于存储您要发送到执行器的任务的结果对象。
List<Future<String>> results=new ArrayList<>();¡;
- 提交 10 个
Task对象。
for (int i=0; i<10; i++) {
SleepTwoSecondsTask task=new SleepTwoSecondsTask();
Future<String> result=myExecutor.submit(task);
results.add(result);
}
- 使用
get()方法获取前五个任务的执行结果。在控制台中写入它们。
for (int i=0; i<5; i++){
try {
String result=results.get(i).get();
System.out.printf("Main: Result for Task %d : %s\n",i,result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- 使用
shutdown()方法结束执行器的执行。
myExecutor.shutdown();
- 使用
get()方法获取最后五个任务的执行结果。在控制台中写入它们。
for (int i=5; i<10; i++){
try {
String result=results.get(i).get();
System.out.printf("Main: Result for Task %d : %s\n",i,result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- 使用
awaitTermination()方法等待执行器的完成。
try {
myExecutor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写一条消息指示程序执行结束。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
在这个配方中,我们扩展了ThreadPoolExecutor类来实现我们的自定义执行器,并覆盖了它的四个方法。beforeExecute()和afterExecute()方法用于计算任务的执行时间。beforeExecute()方法在任务执行之前执行。在这种情况下,我们使用HashMap来存储任务的开始日期。afterExecute()方法在任务执行后执行。您可以从HashMap中获取已完成任务的startTime,然后计算实际日期与该日期之间的差异,以获取任务的执行时间。您还覆盖了shutdown()和shutdownNow()方法,以将执行器中执行的任务的统计信息写入控制台:
-
使用
getCompletedTaskCount()方法执行的任务 -
使用
getActiveCount()方法获取当前正在运行的任务
使用阻塞队列的size()方法来存储待处理任务的执行器。实现Callable接口的SleepTwoSecondsTask类将其执行线程休眠 2 秒,Main类中,您向执行器发送 10 个任务,使用它和其他类来演示它们的特性。
执行程序,您将看到程序显示每个正在运行的任务的时间跨度以及在调用shutdown()方法时执行器的统计信息。
另请参阅
-
第四章中的创建线程执行器配方,线程执行器
-
第七章中的在执行器中使用我们的 ThreadFactory对象配方,自定义并发类
实现基于优先级的执行器类
在 Java 并发 API 的早期版本中,您必须创建和运行应用程序的所有线程。在 Java 版本 5 中,随着执行器框架的出现,引入了一种新的机制来执行并发任务。
使用执行器框架,您只需实现您的任务并将其发送到执行器。执行器负责创建和执行执行您的任务的线程。
在内部,执行程序使用阻塞队列来存储待处理任务。这些任务按照它们到达执行程序的顺序进行存储。一种可能的替代方案是使用优先级队列来存储新任务。这样,如果具有高优先级的新任务到达执行程序,它将在已经等待线程执行的其他线程之前执行,但具有较低优先级。
在本示例中,您将学习如何实现一个执行程序,该执行程序将使用优先级队列来存储您发送的任务以供执行。
准备就绪
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyPriorityTask的类,该类实现了Runnable和Comparable接口,参数化为MyPriorityTask类接口。
public class MyPriorityTask implements Runnable, Comparable<MyPriorityTask> {
- 声明一个名为
priority的私有int属性。
private int priority;
- 声明一个名为
name的私有String属性。
private String name;
- 通过实现类的构造函数来初始化其属性。
public MyPriorityTask(String name, int priority) {
this.name=name;
this.priority=priority;
}
- 实现一个方法来返回优先级属性的值。
public int getPriority(){
return priority;
}
- 实现
Comparable接口中声明的compareTo()方法。它接收一个MyPriorityTask对象作为参数,并比较两个对象的优先级,当前对象和参数对象。您让具有更高优先级的任务在具有较低优先级的任务之前执行。
@Override
public int compareTo(MyPriorityTask o) {
if (this.getPriority() < o.getPriority()) {
return 1;
}
if (this.getPriority() > o.getPriority()) {
return -1;
}
return 0;
}
- 实现
run()方法。将当前线程休眠 2 秒。
@Override
public void run() {
System.out.printf("MyPriorityTask: %s Priority : %d\n",name,priority);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建名为
executor的ThreadPoolExecutor对象。使用PriorityBlockingQueue参数化为Runnable接口作为此执行程序将用于存储其待处理任务的队列。
ThreadPoolExecutor executor=new ThreadPoolExecutor(2,2,1,TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());
- 使用循环的计数器作为任务的优先级,向执行程序发送四个任务。使用
execute()方法将任务发送到执行程序。
for (int i=0; i<4; i++){
MyPriorityTask task=new MyPriorityTask ("Task "+i,i);
executor.execute(task);
}
- 将当前线程休眠 1 秒。
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用循环的计数器作为任务的优先级,向执行程序发送四个额外的任务。使用
execute()方法将任务发送到执行程序。
for (int i=4; i<8; i++) {
MyPriorityTask task=new MyPriorityTask ("Task "+i,i);
executor.execute(task);
}
- 使用
shutdown()方法关闭执行程序。
executor.shutdown();
- 使用
awaitTermination()方法等待执行程序的完成。
try {
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 在控制台中写入一条消息,指示程序的完成。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
将执行程序转换为基于优先级的执行程序很简单。您只需传递一个使用Runnable接口参数化的PriorityBlockingQueue对象作为参数。但是对于执行程序,您应该知道存储在优先级队列中的所有对象都必须实现Comparable接口。
您已经实现了MyPriorityTask类,该类实现了Runnable接口,用作任务,并实现了Comparable接口,用于存储在优先级队列中。该类具有一个Priority属性,用于存储任务的优先级。如果任务的这个属性具有更高的值,它将更早执行。compareTo()方法确定了优先级队列中任务的顺序。在Main类中,您向执行程序发送了八个具有不同优先级的任务。您发送给执行程序的第一个任务是最先执行的任务。当执行程序空闲等待要执行的任务时,随着第一个任务到达执行程序,它立即执行它们。您使用两个执行线程创建了执行程序,因此前两个任务将是最先执行的任务。然后,其余的任务将根据它们的优先级执行。
以下屏幕截图显示了此示例的一个执行:

还有更多...
您可以配置Executor以使用BlockingQueue接口的任何实现。一个有趣的实现是DelayQueue。这个类用于存储延迟激活的元素。它提供了只返回活动对象的方法。您可以使用这个类来实现自己版本的ScheduledThreadPoolExecutor类。
另请参阅
-
第四章中的创建线程执行器配方,线程执行器
-
第七章中的自定义 ThreadPoolExecutor 类配方,自定义并发类
-
第六章中的使用按优先级排序的阻塞线程安全列表配方,并发集合
实现 ThreadFactory 接口以生成自定义线程
工厂模式是面向对象编程世界中广泛使用的设计模式。它是一个创建模式,其目标是开发一个类,其任务是创建一个或多个类的对象。然后,当我们想要创建这些类中的一个对象时,我们使用工厂而不是使用new运算符。
- 使用这个工厂,我们集中了对象的创建,从而方便地改变创建的对象的类或创建这些对象的方式,从而轻松限制了有限资源的对象创建。例如,我们可以只有N个对象,这些对象很容易生成有关对象创建的统计数据。
Java 提供了ThreadFactory接口来实现Thread对象工厂。Java 并发 API 的一些高级工具,如 Executor 框架或 Fork/Join 框架,使用线程工厂来创建线程。
Java 并发 API 中工厂模式的另一个例子是Executors类。它提供了许多方法来创建不同类型的Executor对象。
在这个配方中,您将通过添加新功能来扩展Thread类,并实现一个线程工厂类来生成该新类的线程。
准备工作
这个配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyThread的类,它继承Thread类。
public class MyThread extends Thread {
- 声明三个私有的
Date属性,分别命名为creationDate、startDate和finishDate。
private Date creationDate;
private Date startDate;
private Date finishDate;
- 实现类的构造函数。它接收名称和
Runnable对象作为参数。存储线程的创建日期。
public MyThread(Runnable target, String name ){
super(target,name);
setCreationDate();
}
- 实现
run()方法。存储线程的开始日期,调用父类的run()方法,并存储执行的完成日期。
@Override
public void run() {
setStartDate();
super.run();
setFinishDate();
}
- 实现一个方法来建立
creationDate属性的值。
public void setCreationDate() {
creationDate=new Date();
}
- 实现一个方法来建立
startDate属性的值。
public void setStartDate() {
startDate=new Date();
}
- 实现一个方法来建立
finishDate属性的值。
public void setFinishDate() {
finishDate=new Date();
}
- 实现一个名为
getExecutionTime()的方法,它计算线程的执行时间,即开始日期和完成日期之间的差异。
public long getExecutionTime() {
return finishDate.getTime()-startDate.getTime();
}
- 重写
toString()方法以返回线程的创建日期和执行时间。
@Override
public String toString(){
StringBuilder buffer=new StringBuilder();
buffer.append(getName());
buffer.append(": ");
buffer.append(" Creation Date: ");
buffer.append(creationDate);
buffer.append(" : Running time: ");
buffer.append(getExecutionTime());
buffer.append(" Milliseconds.");
return buffer.toString();
}
- 创建一个名为
MyThreadFactory的类,它实现ThreadFactory接口。
public class MyThreadFactory implements ThreadFactory {
- 声明一个私有的
int属性,命名为counter。
private int counter;
- 声明一个私有的
String属性,命名为prefix。
private String prefix;
- 实现类的构造函数以初始化其属性。
public MyThreadFactory (String prefix) {
this.prefix=prefix;
counter=1;
}
- 实现
newThread()方法。创建一个MyThread对象并增加counter属性。
@Override
public Thread newThread(Runnable r) {
MyThread myThread=new MyThread(r,prefix+"-"+counter);
counter++;
return myThread;
}
- 创建一个名为
MyTask的类,它实现Runnable接口。实现run()方法。让当前线程休眠 2 秒。
public class MyTask implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类并添加一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个
MyThreadFactory对象。
MyThreadFactory myFactory=new MyThreadFactory("MyThreadFactory");
- 创建一个
Task对象。
MyTask task=new MyTask();
- 创建一个
MyThread对象,使用工厂的newThread()方法来执行任务。
Thread thread=myFactory.newThread(task);
- 启动线程并等待其完成。
thread.start();
thread.join();
- 使用
toString()方法编写有关线程的信息。
System.out.printf("Main: Thread information.\n");
System.out.printf("%s\n",thread);
System.out.printf("Main: End of the example.\n");
它是如何工作的...
在本篇文章中,您已经实现了一个自定义的MyThread类,该类扩展了Thread类。该类有三个属性,用于存储创建日期、执行开始日期和执行结束日期。使用开始日期和结束日期属性,您已经实现了getExecutionTime()方法,该方法返回线程执行任务的时间。最后,您已重写了toString()方法以生成有关线程的信息。
一旦您拥有自己的线程类,您就实现了一个工厂来创建实现ThreadFactory接口的该类的对象。如果您要将工厂用作独立对象,则不一定要使用接口,但是如果您想要将此工厂与 Java 并发 API 的其他类一起使用,则必须通过实现该接口来构建您的工厂。ThreadFactory接口只有一个方法,即newThread()方法,该方法接收一个Runnable对象作为参数,并返回一个Thread对象来执行该Runnable对象。在您的情况下,您返回一个MyThread对象。
要检查这两个类,您已经实现了实现Runnable对象的MyTask类。这是由MyThread对象管理的线程要执行的任务。MyTask实例将其执行线程休眠 2 秒。
在示例的主方法中,您使用MyThreadFactory工厂创建了一个MyThread对象来执行一个Task对象。执行程序,您将看到一个带有线程开始日期和执行时间的消息。
以下屏幕截图显示了此示例生成的输出:

还有更多...
Java 并发 API 提供了Executors类来生成线程执行器,通常是ThreadPoolExecutor类的对象。您还可以使用此类来获取ThreadFactory接口的最基本实现,使用defaultThreadFactory()方法。此方法生成的工厂生成基本的Thread对象,它们都属于同一个ThreadGroup对象。
您可以在程序中使用ThreadFactory接口进行任何目的,不一定与 Executor 框架相关。
在 Executor 对象中使用我们的 ThreadFactory
在前一篇文章中,实现 ThreadFactory 接口以生成自定义线程,我们介绍了工厂模式,并提供了如何实现实现ThreadFactory接口的线程工厂的示例。
Executor 框架是一种允许您分离线程创建和执行的机制。它基于Executor和ExecutorService接口以及实现这两个接口的ThreadPoolExecutor类。它具有内部线程池,并提供方法,允许您将两种类型的任务发送到池化线程中进行执行。这两种类型的任务是:
-
实现
Runnable接口的类,以实现不返回结果的任务 -
实现
Callable接口的类,以实现返回结果的任务
在内部,Executor 框架使用ThreadFactory接口来创建它用于生成新线程的线程。在本篇文章中,您将学习如何实现自己的线程类、线程工厂来创建该类的线程,以及如何在执行器中使用该工厂,以便执行器将执行您的线程。
准备就绪...
阅读前一篇文章,实现 ThreadFactory 接口以生成自定义线程,并实现其示例。
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤来实现示例:
-
将在实现 ThreadFactory 接口以生成自定义线程中实现的
MyThread、MyThreadFactory和MyTask类复制到项目中,以便在此示例中使用它们。 -
通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
threadFactory的新MyThreadFactory对象。
MyThreadFactory threadFactory=new MyThreadFactory("MyThreadFactory");
- 使用
Executors类的newCachedThreadPool()方法创建一个新的Executor对象。将之前创建的工厂对象作为参数传递。新的Executor对象将使用该工厂来创建必要的线程,因此它将执行MyThread线程。
ExecutorService executor=Executors.newCachedThreadPool(threadFactory);
- 创建一个新的
Task对象,并使用submit()方法将其发送到执行器。
MyTask task=new MyTask();
executor.submit(task);
- 使用
shutdown()方法关闭执行器。
executor.shutdown();
- 使用
awaitTermination()方法等待执行器的完成。
executor.awaitTermination(1, TimeUnit.DAYS);
- 写一条消息来指示程序的结束。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
在前一个示例的How it works...部分,实现 ThreadFactory 接口以生成自定义线程中,您可以阅读有关MyThread、MyThreadFactory和MyTask类如何工作的详细解释。
在示例的main()方法中,使用Executors类的newCachedThreadPool()方法创建了一个Executor对象。您已将之前创建的工厂对象作为参数传递,因此创建的Executor对象将使用该工厂来创建所需的线程,并执行MyThread类的线程。
执行程序,您将看到一个关于线程启动日期和执行时间的信息。以下截图显示了此示例生成的输出:

另请参阅
- 在第七章的自定义并发类中的实现 ThreadFactory 接口以生成自定义线程食谱中
自定义在定时线程池中运行的任务
定时线程池是 Executor 框架的基本线程池的扩展,允许您安排任务在一段时间后执行。它由ScheduledThreadPoolExecutor类实现,并允许执行以下两种类型的任务:
-
延迟任务:这种类型的任务在一段时间后只执行一次
-
周期性任务:这种类型的任务在延迟后定期执行
延迟任务可以执行Callable和Runnable对象,但周期性任务只能执行Runnable对象。定时池执行的所有任务都是RunnableScheduledFuture接口的实现。在此示例中,您将学习如何实现自己的RunnableScheduledFuture接口的实现来执行延迟和周期性任务。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照下面描述的步骤来实现示例:
- 创建一个名为
MyScheduledTask的类,参数化为一个名为V的泛型类型。它扩展了FutureTask类并实现了RunnableScheduledFuture接口。
public class MyScheduledTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
- 声明一个名为
task的私有RunnableScheduledFuture属性。
private RunnableScheduledFuture<V> task;
- 声明一个名为
executor的私有ScheduledThreadPoolExecutor。
private ScheduledThreadPoolExecutor executor;
- 声明一个名为
period的私有long属性。
private long period;
- 声明一个名为
startDate的私有long属性。
private long startDate;
- 实现一个类的构造函数。它接收一个将由任务执行的
Runnable对象,将由此任务返回的结果,将用于创建MyScheduledTask对象的RunnableScheduledFuture任务,以及将执行任务的ScheduledThreadPoolExecutor对象。调用其父类的构造函数并存储任务和executor属性。
public MyScheduledTask(Runnable runnable, V result, RunnableScheduledFuture<V> task, ScheduledThreadPoolExecutor executor) {
super(runnable, result);
this.task=task;
this.executor=executor;
}
- 实现
getDelay()方法。如果任务是一个周期性任务,并且startDate属性的值不为零,则计算返回值为startDate属性和实际日期之间的差值。否则,返回存储在task属性中的原始任务的延迟。不要忘记以参数传递的时间单位返回结果。
@Override
public long getDelay(TimeUnit unit) {
if (!isPeriodic()) {
return task.getDelay(unit);
} else {
if (startDate==0){
return task.getDelay(unit);
} else {
Date now=new Date();
long delay=startDate-now.getTime();
return unit.convert(delay, TimeUnit.MILLISECONDS);
}
}
}
- 实现
compareTo()方法。调用原始任务的compareTo()方法。
@Override
public int compareTo(Delayed o) {
return task.compareTo(o);
}
- 实现
isPeriodic()方法。调用原始任务的isPeriodic()方法。
@Override
public boolean isPeriodic() {
return task.isPeriodic();
}
- 实现
run()方法。如果是一个周期性任务,你必须更新它的startDate属性,以便将来执行任务的开始日期。计算方法是将实际日期和周期相加。然后,再次将任务添加到ScheduledThreadPoolExecutor对象的队列中。
@Override
public void run() {
if (isPeriodic() && (!executor.isShutdown())) {
Date now=new Date();
startDate=now.getTime()+period;
executor.getQueue().add(this);
}
- 在控制台中打印一条带有实际日期的消息,调用
runAndReset()方法执行任务,然后再次在控制台中打印一条带有实际日期的消息。
System.out.printf("Pre-MyScheduledTask: %s\n",new Date());
System.out.printf("MyScheduledTask: Is Periodic: %s\n",isPeriodic());
super.runAndReset();
System.out.printf("Post-MyScheduledTask: %s\n",new Date());
}
- 实现
setPeriod()方法来设定该任务的周期。
public void setPeriod(long period) {
this.period=period;
}
- 创建一个名为
MyScheduledThreadPoolExecutor的类,以实现执行MyScheduledTask任务的ScheduledThreadPoolExecutor对象。指定该类扩展ScheduledThreadPoolExecutor类。
public class MyScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
- 实现一个类的构造函数,它仅调用其父类的构造函数。
public MyScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize);
}
- 实现
decorateTask()方法。它接收一个将要执行的Runnable对象和将执行该Runnable对象的RunnableScheduledFuture任务作为参数。使用这些对象创建并返回一个MyScheduledTask任务。
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable,
RunnableScheduledFuture<V> task) {
MyScheduledTask<V> myTask=new MyScheduledTask<V>(runnable, null, task,this);
return myTask;
}
- 重写
scheduledAtFixedRate()方法。调用其父类的方法,将返回的对象转换为MyScheduledTask对象,并使用setPeriod()方法设定该任务的周期。
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
ScheduledFuture<?> task= super.scheduleAtFixedRate(command, initialDelay, period, unit);
MyScheduledTask<?> myTask=(MyScheduledTask<?>)task;
myTask.setPeriod(TimeUnit.MILLISECONDS.convert(period,unit));
return task;
}
- 创建一个名为
Task的类,实现Runnable接口。
public class Task implements Runnable {
- 实现
run()方法。在任务开始时打印一条消息,让当前线程休眠 2 秒,然后在任务结束时再打印一条消息。
@Override
public void run() {
System.out.printf("Task: Begin.\n");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Task: End.\n");
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception{
- 创建一个名为
executor的MyScheduledThreadPoolExecutor对象。使用2作为参数,在池中有两个线程。
MyScheduledThreadPoolExecutor executor=new MyScheduledThreadPoolExecutor(2);
- 创建一个名为
task的Task对象。在控制台中写下实际日期。
Task task=new Task();
System.out.printf("Main: %s\n",new Date());
- 使用
schedule()方法向执行器发送一个延迟任务。该任务将在 1 秒延迟后执行。
executor.schedule(task, 1, TimeUnit.SECONDS);
- 让主线程休眠 3 秒。
TimeUnit.SECONDS.sleep(3);
- 创建另一个
Task对象。再次在控制台中打印实际日期。
task=new Task();
System.out.printf("Main: %s\n",new Date());
- 使用
scheduleAtFixedRate()方法向执行器发送一个周期性任务。该任务将在 1 秒延迟后执行,然后每 3 秒执行一次。
executor.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
- 让主线程休眠 10 秒。
TimeUnit.SECONDS.sleep(10);
- 使用
shutdown()方法关闭执行器。使用awaitTermination()方法等待执行器的完成。
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
- 在控制台中写一条消息,指示程序结束。
System.out.printf("Main: End of the program.\n");
工作原理...
在这个示例中,您已经实现了MyScheduledTask类,以实现可以在ScheduledThreadPoolExecutor执行器上执行的自定义任务。该类扩展了FutureTask类并实现了RunnableScheduledFuture接口。它实现了RunnableScheduledFuture接口,因为在计划的执行器中执行的所有任务都必须实现该接口并扩展FutureTask类,因为该类提供了在RunnableScheduledFuture接口中声明的方法的有效实现。之前提到的所有接口和类都是参数化类,具有将由任务返回的数据类型。
要在计划的执行器中使用MyScheduledTask任务,您已经覆盖了MyScheduledThreadPoolExecutor类中的decorateTask()方法。该类扩展了ScheduledThreadPoolExecutor执行器,该方法提供了一种将ScheduledThreadPoolExecutor执行器实现的默认计划任务转换为MyScheduledTask任务的机制。因此,当您实现自己的计划任务版本时,必须实现自己的计划执行器的版本。
decorateTask()方法只是使用参数创建一个新的MyScheduledTask对象;一个将在任务中执行的Runnable对象。该任务将返回一个结果。在这种情况下,任务不会返回结果,因此使用了null值。原始任务用于执行Runnable对象。这是新对象将在池中替换的任务;将执行任务的执行器。在这种情况下,您使用this关键字引用创建任务的执行器。
MyScheduledTask类可以执行延迟和周期性任务。您已经实现了两种任务的所有必要逻辑的两种方法,它们是getDelay()和run()方法。
scheduled executor调用getDelay()方法来确定是否执行任务。此方法在延迟和周期性任务中的行为不同。正如我们之前提到的,MyScheduledClass类的构造函数接收原始的ScheduledRunnableFuture对象,该对象将执行Runnable对象,并将其存储为类的属性,以便访问其方法和数据。当要执行延迟任务时,getDelay()方法返回原始任务的延迟,但是对于周期性任务,getDelay()方法返回startDate属性与实际日期之间的差异。
run()方法是执行任务的方法。周期性任务的一个特点是,如果要再次执行任务,您必须将任务的下一次执行放入执行器的队列中作为新任务。因此,如果要执行周期性任务,您要确定startDate属性的值,将其添加到实际日期和任务执行的周期,并将任务再次存储在执行器的队列中。startDate属性存储了任务的下一次执行将开始的日期。然后,您使用FutureTask类提供的runAndReset()方法执行任务。对于延迟任务,您不必将它们放入执行器的队列中,因为它们只执行一次。
注意
您还必须考虑执行器是否已关闭。在这种情况下,您不必再将周期性任务存储到执行器的队列中。
最后,您已经覆盖了MyScheduledThreadPoolExecutor类中的scheduleAtFixedRate()方法。我们之前提到,对于周期性任务,您要使用任务的周期来确定startDate属性的值,但是您还没有初始化该周期。您必须覆盖此方法,该方法接收该周期作为参数,然后将其传递给MyScheduledTask类,以便它可以使用它。
该示例包括实现了Runnable接口的Task类,并且是在计划执行程序中执行的任务。示例的主类创建了一个MyScheduledThreadPoolExecutor执行程序,并将以下两个任务发送给它们:
-
一个延迟任务,1 秒后执行。
-
一个周期性任务,首次在实际日期后 1 秒执行,然后每 3 秒执行一次
以下屏幕截图显示了此示例的部分执行。您可以检查两种类型的任务是否被正确执行:

还有更多...
ScheduledThreadPoolExecutor类提供了decorateTask()方法的另一个版本,该方法接收Callable对象作为参数,而不是Runnable对象。
另请参阅
-
在延迟后在执行者中运行任务配方第四章, 线程执行者
-
在执行者中定期运行任务配方第四章, 线程执行者
实现 ThreadFactory 接口以为 Fork/Join 框架生成自定义线程
Java 7 最有趣的特性之一是 Fork/Join 框架。它是Executor和ExecutorService接口的实现,允许您执行Callable和Runnable任务,而无需管理执行它们的线程。
这个执行器旨在执行可以分成更小部分的任务。其主要组件如下:
-
ForkJoinTask类实现的一种特殊类型的任务。 -
将任务分成子任务的两个操作(
fork操作)和等待这些子任务完成(join操作)。 -
一种算法,称为工作窃取算法,它优化了线程池中线程的使用。当一个任务正在等待其子任务时,执行它的线程被用来执行另一个线程。
Fork/Join 框架的主类是ForkJoinPool类。在内部,它有以下两个元素:
-
一个等待执行的任务队列
-
执行任务的线程池
在本示例中,您将学习如何实现一个自定义的工作线程,用于ForkJoinPool类,并使用工厂来使用它。
准备工作
这个示例的实现是使用 Eclipse IDE 完成的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyWorkerThread的类,它扩展了ForkJoinWorkerThread类。
public class MyWorkerThread extends ForkJoinWorkerThread {
- 声明并创建一个私有的
ThreadLocal属性,参数为Integer类,命名为taskCounter。
private static ThreadLocal<Integer> taskCounter=new ThreadLocal<Integer>();
- 实现一个类的构造函数。
protected MyWorkerThread(ForkJoinPool pool) {
super(pool);
}
- 重写
onStart()方法。调用其父类的方法,在控制台中打印一条消息,并将此线程的taskCounter属性的值设置为零。
@Override
protected void onStart() {
super.onStart();
System.out.printf("MyWorkerThread %d: Initializing task counter.\n",getId());
taskCounter.set(0);
}
- 重写
onTermination()方法。在控制台中写入此线程的taskCounter属性的值。
@Override
protected void onTermination(Throwable exception) {
System.out.printf("MyWorkerThread %d: %d\n",getId(),taskCounter.get());
super.onTermination(exception);
}
- 实现
addTask()方法。增加taskCounter属性的值。
public void addTask(){
int counter=taskCounter.get().intValue();
counter++;
taskCounter.set(counter);
}
- 创建一个名为
MyWorkerThreadFactory的类,它实现了ForkJoinWorkerThreadFactory接口。实现newThread()方法。创建并返回一个MyWorkerThread对象。
public class MyWorkerThreadFactory implements ForkJoinWorkerThreadFactory {
@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return new MyWorkerThread(pool);
}
}
- 创建一个名为
MyRecursiveTask的类,它扩展了Integer类参数化的RecursiveTask类。
public class MyRecursiveTask extends RecursiveTask<Integer> {
- 声明一个名为
array的私有int数组。
private int array[];
- 声明两个私有的
int属性,命名为start和end。
private int start, end;
- 实现初始化其属性的类的构造函数。
public Task(int array[],int start, int end) {
this.array=array;
this.start=start;
this.end=end;
}
- 实现
compute()方法,对数组在开始和结束位置之间的所有元素求和。首先,将执行任务的线程转换为MyWorkerThread对象,并使用addTask()方法增加该线程的任务计数器。
@Override
protected Integer compute() {
Integer ret;
MyWorkerThread thread=(MyWorkerThread)Thread.currentThread();
thread.addTask();
}
- 实现
addResults()方法。计算并返回作为参数接收的两个任务结果的总和。
private Integer addResults(Task task1, Task task2) {
int value;
try {
value = task1.get().intValue()+task2.get().intValue();
} catch (InterruptedException e) {
e.printStackTrace();
value=0;
} catch (ExecutionException e) {
e.printStackTrace();
value=0;
}
- 让线程休眠 10 毫秒,并返回任务的结果。
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return value;
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
factory的MyWorkerThreadFactory对象。
MyWorkerThreadFactory factory=new MyWorkerThreadFactory();
- 创建一个名为
pool的ForkJoinPool对象。将之前创建的工厂对象传递给构造函数。
ForkJoinPool pool=new ForkJoinPool(4, factory, null, false);
- 创建一个包含 100,000 个整数的数组。将所有元素初始化为
1。
int array[]=new int[100000];
for (int i=0; i<array.length; i++){
array[i]=1;
}
- 创建一个新的
Task对象来对数组的所有元素求和。
MyRecursiveTask task=new MyRecursiveTask(array,0,array.length);
- 使用
execute()方法将任务发送到池中。
pool.execute(task);
- 使用
join()方法等待任务结束。
task.join();
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
awaitTermination()方法等待执行器的完成。
pool.awaitTermination(1, TimeUnit.DAYS);
- 使用
get()方法在控制台中写入任务的结果。
System.out.printf("Main: Result: %d\n",task.get());
- 在控制台中写入一条消息,指示示例的结束。
System.out.printf("Main: End of the program\n");
它是如何工作的...
Fork/Join 框架使用的线程称为工作线程。Java 包括ForkJoinWorkerThread类,该类扩展了Thread类并实现了 Fork/Join 框架使用的工作线程。
在这个配方中,您已经实现了MyWorkerThread类,该类扩展了ForkJoinWorkerThread类并重写了该类的两个方法。您的目标是在每个工作线程中实现一个任务计数器,以便您知道每个工作线程执行了多少任务。您使用ThreadLocal属性实现了计数器。这样,每个线程都将以对程序员透明的方式拥有自己的计数器。
您已经重写了ForkJoinWorkerThread类的onStart()方法,以初始化任务计数器。当工作线程开始执行时,将调用此方法。您还重写了onTermination()方法,以将任务计数器的值打印到控制台。当工作线程完成执行时,将调用此方法。您还在MyWorkerThread类中实现了一个方法。addTask()方法增加每个线程的任务计数器。
ForkJoinPool类,与 Java 并发 API 中的所有执行程序一样,使用工厂创建其线程,因此如果要在ForkJoinPool类中使用MyWorkerThread线程,必须实现自己的线程工厂。对于 Fork/Join 框架,此工厂必须实现ForkJoinPool.ForkJoinWorkerThreadFactory类。您已经为此目的实现了MyWorkerThreadFactory类。这个类只有一个方法,用于创建一个新的MyWorkerThread对象。
最后,您只需使用您创建的工厂初始化一个ForkJoinPool类。您已经在Main类中使用ForkJoinPool类的构造函数完成了这一点。
以下屏幕截图显示了程序输出的一部分:

您可以看到ForkJoinPool对象已经执行了四个工作线程,以及每个工作线程执行了多少任务。
还有更多...
请注意,ForkJoinWorkerThread类提供的onTermination()方法在线程正常完成或抛出Exception异常时调用。该方法接收一个Throwable对象作为参数。如果参数取null值,则工作线程正常完成,但如果参数取值,则线程抛出异常。您必须包含必要的代码来处理这种情况。
另请参阅
-
在第五章的创建 Fork/Join 池配方中,Fork/Join 框架
-
在第一章的通过工厂创建线程配方中,线程管理
自定义在 Fork/Join 框架中运行的任务
执行器框架将任务的创建和执行分开。您只需实现Runnable对象并使用Executor对象。将Runnable任务发送到执行器,它将创建、管理和完成执行这些任务所需的线程。
Java 7 提供了 Fork/Join 框架中的一种特殊的执行器。该框架旨在使用分而治之的技术解决可以分解为较小任务的问题。在任务内部,您必须检查要解决的问题的大小,如果大于设定的大小,则将问题分成两个或更多任务,并使用框架执行这些任务。如果问题的大小小于设定的大小,则直接在任务中解决问题,然后可选择地返回结果。Fork/Join 框架实现了改进这类问题整体性能的工作窃取算法。
Fork/Join 框架的主要类是ForkJoinPool类。在内部,它具有以下两个元素:
-
等待执行的任务队列
-
执行任务的线程池
默认情况下,由ForkJoinPool类执行的任务是ForkJoinTask类的对象。您还可以将Runnable和Callable对象发送到ForkJoinPool类,但它们无法充分利用 Fork/Join 框架的所有优势。通常,您将向ForkJoinPool对象发送ForkJoinTask类的两个子类之一的对象:
-
RecursiveAction:如果您的任务不返回结果 -
RecursiveTask:如果您的任务返回结果
在本示例中,您将学习如何为 Fork/Join 框架实现自己的任务,实现一个扩展了ForkJoinTask类的任务,该任务测量并在控制台中写入其执行时间,以便您可以控制其演变。您还可以实现自己的 Fork/Join 任务来写入日志信息,获取任务中使用的资源,或者对任务的结果进行后处理。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyWorkerTask的类,并指定它扩展了参数为Void类型的ForkJoinTask类。
public abstract class MyWorkerTask extends ForkJoinTask<Void> {
- 声明一个名为
name的私有String属性,用于存储任务的名称。
private String name;
- 实现类的构造函数以初始化其属性。
public MyWorkerTask(String name) {
this.name=name;
}
- 实现
getRawResult()方法。这是ForkJoinTask类的抽象方法之一。由于MyWorkerTask任务不会返回任何结果,因此此方法必须返回null值。
@Override
public Void getRawResult() {
return null;
}
- 实现
setRawResult()方法。这是ForkJoinTask类的另一个抽象方法。由于MyWorkerTask任务不会返回任何结果,因此将此方法的主体留空。
@Override
protected void setRawResult(Void value) {
}
- 实现
exec()方法。这是任务的主要方法。在这种情况下,将任务的逻辑委托给compute()方法。计算该方法的执行时间并将其写入控制台。
@Override
protected boolean exec() {
Date startDate=new Date();
compute();
Date finishDate=new Date();
long diff=finishDate.getTime()-startDate.getTime();
System.out.printf("MyWorkerTask: %s : %d Milliseconds to complete.\n",name,diff);
return true;
}
- 实现
getName()方法以返回任务的名称。
public String getName(){
return name;
}
- 声明抽象方法
compute()。如前所述,此方法将实现任务的逻辑,并且必须由MyWorkerTask类的子类实现。
protected abstract void compute();
- 创建一个名为
Task的类,该类扩展了MyWorkerTask类。
public class Task extends MyWorkerTask {
- 声明一个名为
array的私有int值数组。
private int array[];
- 实现一个初始化其属性的类的构造函数。
public Task(String name, int array[], int start, int end){
super(name);
this.array=array;
this.start=start;
this.end=end;
}
- 实现
compute()方法。此方法增加由开始和结束属性确定的数组元素块。如果此元素块的元素超过 100 个,则将该块分成两部分,并创建两个Task对象来处理每个部分。使用invokeAll()方法将这些任务发送到池中。
protected void compute() {
if (end-start>100){
int mid=(end+start)/2;
Task task1=new Task(this.getName()+"1",array,start,mid);
Task task2=new Task(this.getName()+"2",array,mid,end);
invokeAll(task1,task2);
- 如果元素块少于 100 个,则使用
for循环增加所有元素。
} else {
for (int i=start; i<end; i++) {
array[i]++;
}
- 最后,让执行任务的线程休眠 50 毫秒。
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个包含 10,000 个元素的
int数组。
int array[]=new int[10000];
- 创建一个名为
pool的ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 创建一个
Task对象来增加数组的所有元素。构造函数的参数是Task作为任务的名称,数组对象和值0和10000,以指示该任务必须处理整个数组。
Task task=new Task("Task",array,0,array.length);
- 使用
execute()方法将任务发送到池中。
pool.invoke(task);
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 在控制台中写入一条消息,指示程序的结束。
System.out.printf("Main: End of the program.\n");
工作原理...
在此配方中,您已实现了MyWorkerTask类,该类扩展了ForkJoinTask类。这是您自己的基类,用于实现可以在ForkJoinPool执行程序中执行并且可以利用该执行程序的所有优势的任务,如工作窃取算法。该类相当于RecursiveAction和RecursiveTask类。
当您扩展ForkJoinTask类时,您必须实现以下三种方法:
-
setRawResult(): 此方法用于设置任务的结果。由于您的任务不返回任何结果,因此您将此方法留空。 -
getRawResult(): 此方法用于返回任务的结果。由于您的任务不返回任何结果,因此此方法返回null值。 -
exec(): 此方法实现任务的逻辑。在您的情况下,您已将逻辑委托给抽象方法compute()(如RecursiveAction和RecursiveTask类),并且在exec()方法中测量该方法的执行时间,并将其写入控制台。
最后,在示例的主类中,您已创建了一个包含 10,000 个元素的数组,一个ForkJoinPool执行程序和一个Task对象来处理整个数组。执行程序,您将看到执行的不同任务如何在控制台中写入它们的执行时间。
另请参阅
-
第五章中的创建 Fork/Join 池配方,Fork/Join Framework
-
第七章中的实现 ThreadFactory 接口以为 Fork/Join 框架生成自定义线程配方,自定义并发类
实现自定义锁类
锁是 Java 并发 API 提供的基本同步机制之一。它允许程序员保护代码的临界区,因此只有一个线程可以一次执行该代码块。它提供以下两个操作:
-
lock(): 当您想要访问临界区时,调用此操作。如果有另一个线程正在运行该临界区,则其他线程将被阻塞,直到它们被锁唤醒以访问临界区。 -
unlock(): 在临界区的末尾调用此操作,以允许其他线程访问临界区。
在 Java 并发 API 中,锁在Lock接口中声明,并在一些类中实现,例如ReentrantLock类。
在此配方中,您将学习如何实现自己的Lock对象,该对象实现了实现Lock接口的类,可用于保护临界区。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyQueuedSynchronizer的类,它继承AbstractQueuedSynchronizer类。
public class MyAbstractQueuedSynchronizer extends AbstractQueuedSynchronizer {
- 声明一个名为
state的私有AtomicInteger属性。
private AtomicInteger state;
- 实现类的构造函数以初始化其属性。
public MyAbstractQueuedSynchronizer() {
state=new AtomicInteger(0);
}
- 实现
tryAcquire()方法。此方法尝试将状态变量的值从零更改为一。如果可以,它返回true值,否则返回false。
@Override
protected boolean tryAcquire(int arg) {
return state.compareAndSet(0, 1);
}
- 实现
tryRelease()方法。此方法尝试将状态变量的值从一更改为零。如果可以,则返回true值,否则返回false值。
@Override
protected boolean tryRelease(int arg) {
return state.compareAndSet(1, 0);
}
- 创建一个名为
MyLock的类,并指定它实现Lock接口。
public class MyLock implements Lock{
- 声明一个名为
sync的私有AbstractQueuedSynchronizer属性。
private AbstractQueuedSynchronizer sync;
- 通过使用新的
MyAbstractQueueSynchronizer对象初始化sync属性来实现类的构造函数。
public MyLock() {
sync=new MyAbstractQueuedSynchronizer();
}
- 实现
lock()方法。调用sync对象的acquire()方法。
@Override
public void lock() {
sync.acquire(1);
}
- 实现
lockInterruptibly()方法。调用sync对象的acquireInterruptibly()方法。
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- 实现
tryLock()方法。调用sync对象的tryAcquireNanos()方法。
@Override
public boolean tryLock() {
try {
return sync.tryAcquireNanos(1, 1000);
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
- 使用两个参数实现
tryLock()方法的另一个版本。一个名为time的长参数和一个名为unit的TimeUnit参数。调用sync对象的tryAcquireNanos()方法。
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, TimeUnit.NANOSECONDS.convert(time, unit));
}
- 实现
unlock()方法。调用sync对象的release()方法。
@Override
public void unlock() {
sync.release(1);
}
- 实现
newCondition()方法。创建sync对象的内部类ConditionObject的新对象。
@Override
public Condition newCondition() {
return sync.new ConditionObject();
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
lock的私有MyLock属性。
private MyLock lock;
- 声明一个名为
name的私有String属性。
private String name;
- 实现类的构造函数以初始化其属性。
public Task(String name, MyLock lock){
this.lock=lock;
this.name=name;
}
- 实现类的
run()方法。获取锁,使线程休眠 2 秒,然后释放lock对象。
@Override
public void run() {
lock.lock();
System.out.printf("Task: %s: Take the lock\n",name);
try {
TimeUnit.SECONDS.sleep(2);
System.out.printf("Task: %s: Free the lock\n",name);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个名为
lock的MyLock对象。
MyLock lock=new MyLock();
- 创建并执行 10 个
Task任务。
for (int i=0; i<10; i++){
Task task=new Task("Task-"+i,lock);
Thread thread=new Thread(task);
thread.start();
}
- 尝试使用
tryLock()方法获取锁。等待一秒钟,如果没有获得锁,则写一条消息并重试。
boolean value;
do {
try {
value=lock.tryLock(1,TimeUnit.SECONDS);
if (!value) {
System.out.printf("Main: Trying to get the Lock\n");
}
} catch (InterruptedException e) {
e.printStackTrace();
value=false;
}
} while (!value);
- 写一条消息指示您已获得锁并释放它。
System.out.printf("Main: Got the lock\n");
lock.unlock();
- 写一条消息指示程序结束。
System.out.printf("Main: End of the program\n");
它是如何工作的...
Java 并发 API 提供了一个类,可用于实现具有锁或信号量特性的同步机制。它是AbstractQueuedSynchronizer,正如其名称所示,它是一个抽象类。它提供了控制对临界区的访问以及管理被阻塞等待对临界区的访问的线程队列的操作。这些操作基于两个抽象方法:
-
tryAcquire():调用此方法尝试访问临界区。如果调用它的线程可以访问临界区,则该方法返回true值。否则,该方法返回false值。 -
tryRelease():调用此方法尝试释放对临界区的访问。如果调用它的线程可以释放访问,则该方法返回true值。否则,该方法返回false值。
在这些方法中,您必须实现用于控制对临界区的访问的机制。在您的情况下,您已经实现了MyQueuedSynchonizer类,该类扩展了AbstractQueuedSyncrhonizer类,并使用AtomicInteger变量实现了抽象方法,以控制对临界区的访问。如果锁是空闲的,该变量将存储0值,因此线程可以访问临界区,如果锁被阻塞,该变量将存储1值,因此线程无法访问临界区。
您已经使用了AtomicInteger类提供的compareAndSet()方法,该方法尝试将您指定为第一个参数的值更改为您指定为第二个参数的值。要实现tryAcquire()方法,您尝试将原子变量的值从零更改为一。同样,要实现tryRelease()方法,您尝试将原子变量的值从一更改为零。
你必须实现这个类,因为AbstractQueuedSynchronizer类的其他实现(例如ReentrantLock类使用的实现)是作为私有类在使用它的类内部实现的,所以你无法访问它。
然后,你已经实现了MyLock类。这个类实现了Lock接口,并有一个MyQueuedSynchronizer对象作为属性。为了实现Lock接口的所有方法,你使用了MyQueuedSynchronizer对象的方法。
最后,你已经实现了Task类,它实现了Runnable接口,并使用MyLock对象来访问临界区。该临界区使线程休眠 2 秒。主类创建了一个MyLock对象,并运行了 10 个共享该锁的Task对象。主类还尝试使用tryLock()方法来访问锁。
当你执行示例时,你会看到只有一个线程可以访问临界区,当该线程完成时,另一个线程将获得访问权限。
你可以使用自己的Lock来编写关于其使用情况的日志消息,控制锁定的时间,或实现高级的同步机制,例如控制对资源的访问,使其只在特定时间可用。
还有更多...
AbstractQueuedSynchronizer类提供了两个方法来管理锁的状态。它们是getState()和setState()方法。这些方法接收并返回一个整数值,表示锁的状态。你可以使用这些方法来代替AtomicInteger属性来存储锁的状态。
Java 并发 API 提供了另一个类来实现同步机制。它是AbstractQueuedLongSynchronizer类,它相当于AbstractQueuedSynchronizer类,但使用long属性来存储线程的状态。
另请参阅
- 在第二章的Synchronizing a block of code with locks食谱中,基本线程同步
基于优先级实现传输队列
Java 7 API 提供了几种数据结构来处理并发应用程序。其中,我们想要强调以下两种数据结构:
-
LinkedTransferQueue:这种数据结构应该在具有生产者/消费者结构的程序中使用。在这些应用程序中,你有一个或多个数据的生产者和一个或多个数据的消费者,一个数据结构被所有人共享。生产者将数据放入数据结构,消费者从数据结构中取数据。如果数据结构为空,消费者将被阻塞,直到有数据可供消费。如果数据结构已满,生产者将被阻塞,直到有空间放置他们的数据。 -
PriorityBlockingQueue:在这种数据结构中,元素以有序方式存储。元素必须实现带有compareTo()方法的Comparable接口。当你将一个元素插入结构中时,它会与结构中的元素进行比较,直到找到它的位置。
LinkedTransferQueue的元素按照它们到达的顺序存储,所以先到达的元素先被消耗。当你想要开发一个生产者/消费者程序时,数据根据某种优先级而不是到达时间进行消耗时,可能会出现这种情况。在这个示例中,你将学习如何实现一个数据结构,用于解决生产者/消费者问题,其元素将按照它们的优先级进行排序。具有更高优先级的元素将首先被消耗。
准备工作
这个示例已经在 Eclipse IDE 中实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyPriorityTransferQueue的类,该类扩展了PriorityBlockingQueue类并实现了TransferQueue接口。
public class MyPriorityTransferQueue<E> extends PriorityBlockingQueue<E> implements
TransferQueue<E> {
- 声明一个私有的
AtomicInteger属性,名为counter,用于存储等待消费元素的消费者数量。
private AtomicInteger counter;
- 声明一个私有的
LinkedBlockingQueue属性,名为transferred。
private LinkedBlockingQueue<E> transfered;
- 声明一个私有的
ReentrantLock属性,名为lock。
private ReentrantLock lock;
- 实现类的构造函数以初始化其属性。
public MyPriorityTransferQueue() {
counter=new AtomicInteger(0);
lock=new ReentrantLock();
transfered=new LinkedBlockingQueue<E>();
}
- 实现
tryTransfer()方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有等待的消费者,该方法返回false值。
@Override
public boolean tryTransfer(E e) {
lock.lock();
boolean value;
if (counter.get()==0) {
value=false;
} else {
put(e);
value=true;
}
lock.unlock();
return value;
}
- 实现
transfer()方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有等待的消费者,该方法将元素存储在一个特殊的队列中,以便发送给尝试获取元素并阻塞线程直到元素被消费的第一个消费者。
@Override
public void transfer(E e) throws InterruptedException {
lock.lock();
if (counter.get()!=0) {
put(e);
lock.unlock();
} else {
transfered.add(e);
lock.unlock();
synchronized (e) {
e.wait();
}
}
}
- 实现
tryTransfer()方法,该方法接收三个参数:元素、等待消费者的时间(如果没有)和用于指定时间的时间单位。如果有等待的消费者,它立即发送元素。否则,将指定的时间转换为毫秒,并使用wait()方法使线程进入休眠状态。当消费者取走元素时,如果线程正在wait()方法中休眠,你将使用notify()方法唤醒它,稍后会看到。
@Override
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
lock.lock();
if (counter.get()!=0) {
put(e);
lock.unlock();
return true;
} else {
transfered.add(e);
long newTimeout= TimeUnit.MILLISECONDS.convert(timeout, unit);
lock.unlock();
e.wait(newTimeout);
lock.lock();
if (transfered.contains(e)) {
transfered.remove(e);
lock.unlock();
return false;
} else {
lock.unlock();
return true;
}
}
}
- 实现
hasWaitingConsumer()方法。使用计数属性的值来计算该方法的返回值。如果计数大于零,则返回true。否则,返回false。
@Override
public boolean hasWaitingConsumer() {
return (counter.get()!=0);
}
- 实现
getWaitingConsumerCount()方法。返回counter属性的值。
@Override
public int getWaitingConsumerCount() {
return counter.get();
}
- 实现
take()方法。消费者想要消费元素时调用此方法。首先获取之前定义的锁,并增加等待消费者的数量。
@Override
public E take() throws InterruptedException {
lock.lock();
counter.incrementAndGet();
- 如果在转移队列中没有任何元素,则释放锁并尝试从队列中获取元素,使用
take()方法,并再次获取锁。如果队列中没有任何元素,该方法将使线程进入休眠状态,直到有元素可供消费。
E value=transfered.poll();
if (value==null) {
lock.unlock();
value=super.take();
lock.lock();
- 否则,从转移队列中取出元素,并唤醒正在等待消费该元素的线程(如果有的话)。
} else {
synchronized (value) {
value.notify();
}
}
- 最后,减少等待消费者的计数并释放锁。
counter.decrementAndGet();
lock.unlock();
return value;
}
- 实现一个名为
Event的类,该类实现了参数化为Event类的Comparable接口。
public class Event implements Comparable<Event> {
- 声明一个私有的
String属性,名为thread,用于存储创建事件的线程的名称。
private String thread;
- 声明一个私有的
int属性,名为priority,用于存储事件的优先级。
private int priority;
- 实现类的构造函数以初始化其属性。
public Event(String thread, int priority){
this.thread=thread;
this.priority=priority;
}
- 实现一个方法来返回
thread属性的值。
public String getThread() {
return thread;
}
- 实现一个方法来返回
priority属性的值。
public int getPriority() {
return priority;
}
- 实现
compareTo()方法。该方法将实际事件与作为参数接收的事件进行比较。如果实际事件的优先级高于参数,则返回-1,如果实际事件的优先级低于参数,则返回1,如果两个事件具有相同的优先级,则返回0。你将按优先级降序获得列表。优先级较高的事件将首先存储在队列中。
public int compareTo(Event e) {
if (this.priority>e.getPriority()) {
return -1;
} else if (this.priority<e.getPriority()) {
return 1;
} else {
return 0;
}
}
- 实现一个名为
Producer的类,该类实现了Runnable接口。
public class Producer implements Runnable {
- 声明一个私有的
MyPriorityTransferQueue属性,参数化为Event类,名为buffer,用于存储生产者生成的事件。
private MyPriorityTransferQueue<Event> buffer;
- 实现类的构造函数以初始化其属性。
public Producer(MyPriorityTransferQueue<Event> buffer) {
this.buffer=buffer;
}
- 实现类的
run()方法。使用创建顺序作为优先级创建 100 个Event对象(最新的事件将具有最高优先级),并使用put()方法将它们插入队列。
@Override
public void run() {
for (int i=0; i<100; i++) {
Event event=new Event(Thread.currentThread().getName(),i);
buffer.put(event);
}
}
- 实现一个名为
Consumer的类,该类实现了Runnable接口。
public class Consumer implements Runnable {
- 声明一个私有的
MyPriorityTransferQueue属性,参数化为Event类,命名为 buffer,以获取此类消耗的事件。
private MyPriorityTransferQueue<Event> buffer;
- 实现类的构造函数以初始化其属性。
public Consumer(MyPriorityTransferQueue<Event> buffer) {
this.buffer=buffer;
}
- 实现
run()方法。它使用take()方法消耗 1002 个Events(在示例中生成的所有事件),并在控制台中写入生成事件的线程编号和其优先级。
@Override
public void run() {
for (int i=0; i<1002; i++) {
try {
Event value=buffer.take();
System.out.printf("Consumer: %s: %d\n",value.getThread(),value.getPriority());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
buffer的MyPriorityTransferQueue对象。
MyPriorityTransferQueue<Event> buffer=new MyPriorityTransferQueue<Event>();
- 创建一个
Producer任务并启动 10 个线程来执行该任务。
Producer producer=new Producer(buffer);
Thread producerThreads[]=new Thread[10];
for (int i=0; i<producerThreads.length; i++) {
producerThreads[i]=new Thread(producer);
producerThreads[i].start();
}
- 创建并启动一个
Consumer任务。
Consumer consumer=new Consumer(buffer);
Thread consumerThread=new Thread(consumer);
consumerThread.start();
- 在控制台中写入实际消费者计数。
System.out.printf("Main: Buffer: Consumer count: %d\n",buffer.getWaitingConsumerCount());
- 使用
transfer()方法向消费者传输事件。
Event myEvent=new Event("Core Event",0);
buffer.transfer(myEvent);
System.out.printf("Main: My Event has ben transfered.\n");
- 使用
join()方法等待生产者的完成。
for (int i=0; i<producerThreads.length; i++) {
try {
producerThreads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 使线程休眠 1 秒。
TimeUnit.SECONDS.sleep(1);
- 写入实际消费者计数。
System.out.printf("Main: Buffer: Consumer count: %d\n",buffer.getWaitingConsumerCount());
- 使用
transfer()方法传输另一个事件。
myEvent=new Event("Core Event 2",0);
buffer.transfer(myEvent);
- 使用
join()方法等待消费者的完成。
consumerThread.join();
- 写一条消息指示程序的结束。
System.out.printf("Main: End of the program\n");
工作原理...
在这个示例中,您已经实现了MyPriorityTransferQueue数据结构。这是一个用于生产者/消费者问题的数据结构,但其元素按优先级而不是到达顺序排序。由于 Java 不允许多重继承,您的第一个决定是MyPriorityTransferQueue类的基类。您扩展了PriorityBlockingQueue类,以实现按优先级将元素插入结构的操作。您还实现了TransferQueue接口以添加与生产者/消费者相关的方法。
MyPriortyTransferQueue类具有以下三个属性:
-
一个名为
counter的AtomicInteger属性:此属性存储等待从数据结构中获取元素的消费者数量。当消费者调用take()操作从数据结构中获取元素时,计数器会递增。当消费者完成take()操作的执行时,计数器再次递减。此计数器用于实现hasWaitingConsumer()和getWaitingConsumerCount()方法。 -
名为
lock的ReentrantLock属性:此属性用于控制对实现的操作的访问。只有一个线程可以使用数据结构。 -
最后,创建一个
LinkedBlockingQueue列表来存储传输的元素。
您已经在MyPriorityTransferQueue中实现了一些方法。所有这些方法都在TransferQueue接口中声明,并且take()方法在PriorityBlockingQueue接口中实现。其中两个已在前面描述过。以下是其余方法的描述:
-
tryTransfer(E``e):此方法尝试直接将一个元素发送给消费者。如果有消费者在等待,该方法将元素存储在优先级队列中,以便立即被消费者消费,然后返回true值。如果没有消费者在等待,该方法返回false值。 -
transfer(E``e):此方法直接将一个元素传输给消费者。如果有消费者在等待,该方法将元素存储在优先级队列中,以便立即被消费者消费。否则,该元素将存储在传输元素的列表中,并且线程将被阻塞,直到元素被消费。当线程处于休眠状态时,您必须释放锁,否则会阻塞队列。 -
tryTransfer(E``e,``long``timeout,``TimeUnit``unit):此方法类似于transfer()方法,但线程会阻塞由其参数确定的时间段。当线程处于休眠状态时,您必须释放锁,否则会阻塞队列。 -
take(): 该方法返回要消耗的下一个元素。如果传输元素列表中有元素,则要消耗的元素将从该列表中取出。否则,它将从优先级队列中取出。
实现数据结构后,您已经实现了Event类。这是您在数据结构中存储的元素的类。Event类有两个属性,用于存储生产者的 ID 和事件的优先级,并实现了Comparable接口,因为这是数据结构的要求。
然后,您已经实现了Producer和Consumer类。在示例中,您有 10 个生产者和一个消费者,它们共享相同的缓冲区。每个生产者生成 100 个具有增量优先级的事件,因此具有更高优先级的事件是最后生成的。
示例的主类创建了一个MyPriorityTransferQueue对象,10 个生产者和一个消费者,并使用MyPriorityTransferQueue缓冲区的transfer()方法将两个事件传输到缓冲区。
以下屏幕截图显示了程序执行的部分输出:

您可以看到,具有更高优先级的事件首先被消耗,并且消费者消耗了传输的事件。
另请参阅
-
第六章中的使用按优先级排序的阻塞线程安全列表配方,并发集合
-
第六章中的使用阻塞线程安全列表配方,并发集合
实现自己的原子对象
原子变量是在 Java 版本 5 中引入的,它们对单个变量提供原子操作。当线程对原子变量进行操作时,类的实现包括一个机制来检查操作是否在一步中完成。基本上,该操作获取变量的值,将值更改为本地变量,然后尝试将旧值更改为新值。如果旧值仍然相同,则进行更改。如果不是,则该方法重新开始操作。
在本配方中,您将学习如何扩展原子对象以及如何实现遵循原子对象机制的两个操作,以确保所有操作都在一步中完成。
准备就绪
本配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
ParkingCounter的类,并指定它扩展AtomicInteger类。
public class ParkingCounter extends AtomicInteger {
- 声明一个名为
maxNumber的私有int属性,以存储停车场中允许的最大汽车数量。
private int maxNumber;
- 实现类的构造函数以初始化其属性。
public ParkingCounter(int maxNumber){
set(0);
this.maxNumber=maxNumber;
}
- 实现
carIn()方法。如果计数器的值小于设定的最大值,则该方法递增汽车的计数器。构建一个无限循环,并使用get()方法获取内部计数器的值。
public boolean carIn() {
for (;;) {
int value=get();
- 如果该值等于
maxNumber属性,则无法递增计数器(停车场已满,汽车无法进入)。该方法返回false值。
if (value==maxNumber) {
System.out.printf("ParkingCounter: The parking lot is full.\n");
return false;
- 否则,增加该值并使用
compareAndSet()方法将旧值更改为新值。该方法返回false值;计数器未递增,因此必须重新开始循环。如果返回true值,则表示已进行更改,然后返回true值。
} else {
int newValue=value+1;
boolean changed=compareAndSet(value,newValue);
if (changed) {
System.out.printf("ParkingCounter: A car has entered.\n");
return true;
}
}
}
}
- 实现
carOut()方法。如果计数器的值大于0,则该方法递减汽车的计数器。构建一个无限循环,并使用get()方法获取内部计数器的值。
public boolean carOut() {
for (;;) {
int value=get();
if (value==0) {
System.out.printf("ParkingCounter: The parking lot is empty.\n");
return false;
} else {
int newValue=value-1;
boolean changed=compareAndSet(value,newValue);
if (changed) {
System.out.printf("ParkingCounter: A car has gone out.\n");
return true;
}
}
}
}
- 创建一个名为
Sensor1的类,该类实现Runnable接口。
public class Sensor1 implements Runnable {
- 声明一个名为
counter的私有ParkingCounter属性。
private ParkingCounter counter;
- 实现类的构造函数以初始化其属性。
public Sensor1(ParkingCounter counter) {
this.counter=counter;
}
- 实现
run()方法。多次调用carIn()和carOut()操作。
@Override
public void run() {
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carOut();
counter.carOut();
counter.carOut();
counter.carIn();
counter.carIn();
counter.carIn();
}
- 创建一个名为
Sensor2的类,实现Runnable接口。
public class Sensor2 implements Runnable {
- 声明一个名为
counter的私有ParkingCounter属性。
private ParkingCounter counter;
- 实现类的构造函数以初始化其属性。
public Sensor2(ParkingCounter counter) {
this.counter=counter;
}
- 实现
run()方法。多次调用carIn()和carOut()操作。
@Override
public void run() {
counter.carIn();
counter.carOut();
counter.carOut();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建名为
counter的ParkingCounter对象。
ParkingCounter counter=new ParkingCounter(5);
- 创建并启动一个
Sensor1任务和一个Sensor2任务。
Sensor1 sensor1=new Sensor1(counter);
Sensor2 sensor2=new Sensor2(counter);
Thread thread1=new Thread(sensor1);
Thread thread2=new Thread(sensor2);
thread1.start();
thread2.start();
- 等待两个任务的最终确定。
thread1.join();
thread2.join();
- 在控制台中写入计数器的实际值。
System.out.printf("Main: Number of cars: %d\n",counter.get());
- 在控制台中写入指示程序结束的消息。
System.out.printf("Main: End of the program.\n");
工作原理...
ParkingCounter类扩展了AtomicInteger类,具有两个原子操作carIn()和carOut()。该示例模拟了一个控制停车场内汽车数量的系统。停车场可以容纳一定数量的汽车,由maxNumber属性表示。
carIn()操作将停车场内的实际汽车数量与最大值进行比较。如果它们相等,则汽车无法进入停车场,该方法返回false值。否则,它使用原子操作的以下结构:
-
将原子对象的值存储在本地变量中。
-
将新值存储在不同的变量中。
-
使用
compareAndSet()方法尝试用新值替换旧值。如果此方法返回true值,则您发送的旧值作为参数是变量的值,因此它会更改值。该操作以原子方式执行,因为carIn()方法返回true值。如果compareAndSet()方法返回false值,则您发送的旧值不是变量的值(其他线程对其进行了修改),因此该操作无法以原子方式执行。操作将重新开始,直到可以以原子方式执行为止。
carOut()方法类似于carIn()方法。您还实现了两个使用carIn()和carOut()方法来模拟停车活动的Runnable对象。当您执行程序时,您会发现停车场从未超过停车场内汽车的最大值。
另请参阅
- 在第六章中使用原子变量中的配方,并发集合
第八章:测试并发应用程序
在本章中,我们将涵盖:
-
监视
Lock接口 -
监视
Phaser类 -
监视 Executor 框架
-
监视 Fork/Join 池
-
编写有效的日志消息
-
使用 FindBugs 分析并发代码
-
配置 Eclipse 以调试并发代码
-
配置 NetBeans 以调试并发代码
-
使用 MultithreadedTC 测试并发代码
介绍
测试应用程序是一项关键任务。在应用程序准备交付给最终用户之前,您必须证明其正确性。您使用测试过程来证明已经实现了正确性并修复了错误。测试阶段是任何软件开发和质量保证流程中的常见任务。您可以找到大量关于测试过程和您可以应用于开发的不同方法的文献。还有许多库,如JUnit,以及应用程序,如 Apache JMetter,您可以使用它们以自动化的方式测试您的 Java 应用程序。这在并发应用程序开发中更加关键。
并发应用程序具有两个或更多个共享数据结构并相互交互的线程,这增加了测试阶段的难度。当您测试并发应用程序时,将面临的最大问题是线程的执行是不确定的。您无法保证线程执行的顺序,因此很难重现错误。
在本章中,您将学习:
-
如何获取有关并发应用程序中的元素的信息。这些信息可以帮助您测试并发应用程序。
-
如何使用集成开发环境(IDE)和其他工具,如 FindBugs,来测试并发应用程序。
-
如何使用诸如 MultithreadedTC 之类的库来自动化您的测试。
监视 Lock 接口
Lock接口是 Java 并发 API 提供的基本机制之一,用于同步代码块。它允许定义临界区。临界区是访问共享资源的代码块,不能同时由多个线程执行。这个机制由Lock接口和ReentrantLock类实现。
在本示例中,您将学习可以获取有关Lock对象的哪些信息以及如何获取这些信息。
准备工作
本示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyLock的类,继承ReentrantLock类。
public class MyLock extends ReentrantLock {
- 实现
getOwnerName()方法。该方法使用Lock类的受保护方法getOwner()返回控制锁的线程(如果有)的名称。
public String getOwnerName() {
if (this.getOwner()==null) {
return "None";
}
return this.getOwner().getName();
}
- 实现
getThreads()方法。该方法使用Lock类的受保护方法getQueuedThreads()返回排队在锁中的线程列表。
public Collection<Thread> getThreads() {
return this.getQueuedThreads();
}
- 创建一个名为
Task的类,实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
lock的私有Lock属性。
private Lock lock;
- 实现类的构造函数以初始化其属性。
public Task (Lock lock) {
this.lock=lock;
}
- 实现
run()方法。创建一个包含五个步骤的循环。
@Override
public void run() {
for (int i=0; i<5; i++) {
- 使用
lock()方法获取锁并打印一条消息。
lock.lock();
System.out.printf("%s: Get the Lock.\n",Thread.currentThread().getName());
- 使线程休眠 500 毫秒。使用
unlock()方法释放锁并打印一条消息。
try {
TimeUnit.MILLISECONDS.sleep(500);
System.out.printf("%s: Free the Lock.\n",Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
- 通过创建一个名为
Main的类和一个main()方法来创建示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
lock的MyLock对象。
MyLock lock=new MyLock();
- 为五个
Thread对象创建一个数组。
Thread threads[]=new Thread[5];
- 创建并启动五个线程来执行五个
Task对象。
for (int i=0; i<5; i++) {
Task task=new Task(lock);
threads[i]=new Thread(task);
threads[i].start();
}
- 创建一个包含 15 个步骤的循环。
for (int i=0; i<15; i++) {
- 在控制台中写入锁的所有者的名称。
System.out.printf("Main: Logging the Lock\n");
System.out.printf("************************\n");
System.out.printf("Lock: Owner : %s\n",lock.getOwnerName());
- 显示排队等待锁的线程的数量和名称。
.out.printf("Lock: Queued Threads: %s\n",lock.hasQueuedThreads());
if (lock.hasQueuedThreads()){
System.out.printf("Lock: Queue Length: %d\n",lock.getQueueLength());
System.out.printf("Lock: Queued Threads: ");
Collection<Thread> lockedThreads=lock.getThreads();
for (Thread lockedThread : lockedThreads) {
System.out.printf("%s ",lockedThread.getName());
}
System.out.printf("\n");
}
- 显示关于
Lock对象的公平性和状态的信息。
System.out.printf("Lock: Fairness: %s\n",lock.isFair());
System.out.printf("Lock: Locked: %s\n",lock.isLocked());
System.out.printf("************************\n");
- 将线程休眠 1 秒并关闭循环和类。
TimeUnit.SECONDS.sleep(1);
}
}
}
工作原理...
在这个食谱中,您已经实现了MyLock类,该类扩展了ReentrantLock类,以返回原本无法获得的信息-这是ReentrantLock类的受保护数据。MyLock类实现的方法有:
-
getOwnerName():只有一个线程可以执行由Lock对象保护的临界区。锁存储正在执行临界区的线程。此线程由ReentrantLock类的受保护getOwner()方法返回。此方法使用getOwner()方法返回该线程的名称。 -
getThreads():当一个线程执行临界区时,试图进入它的其他线程被放到睡眠状态,直到它们可以继续执行该临界区。ReentrantLock类的受保护方法getQueuedThreads()返回等待执行临界区的线程列表。此方法返回getQueuedThreads()方法返回的结果。
我们还使用了ReentrantLock类中实现的其他方法:
-
hasQueuedThreads():此方法返回一个Boolean值,指示是否有线程正在等待获取此锁 -
getQueueLength():此方法返回正在等待获取此锁的线程数 -
isLocked():此方法返回一个Boolean值,指示此锁是否由线程拥有 -
isFair():此方法返回一个Boolean值,指示此锁是否已激活公平模式
还有更多...
ReentrantLock类中还有其他方法可用于获取有关Lock对象的信息:
-
getHoldCount():返回当前线程获取锁的次数 -
isHeldByCurrentThread():返回一个Boolean值,指示锁是否由当前线程拥有
另请参阅
-
第二章“基本线程同步”中的使用锁同步代码块食谱
-
第七章“自定义并发类”中的实现自定义锁类食谱
监视Phaser类
Java 并发 API 提供的最复杂和强大的功能之一是使用Phaser类执行并发分阶段任务的能力。当我们有一些并发任务分为步骤时,这种机制非常有用。Phaser类为我们提供了在每个步骤结束时同步线程的机制,因此在所有线程完成第一步之前,没有线程开始其第二步。
在这个食谱中,您将学习有关Phaser类状态的信息以及如何获取该信息。
准备工作
此食谱的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
Task的类,实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
time的私有int属性。
private int time;
- 声明一个名为
phaser的私有Phaser属性。
private Phaser phaser;
- 实现类的构造函数以初始化其属性。
public Task(int time, Phaser phaser) {
this.time=time;
this.phaser=phaser;
}
- 实现
run()方法。首先,使用arrive()方法指示phaser属性任务开始执行。
@Override
public void run() {
phaser.arrive();
- 在控制台中写入一条消息,指示第一阶段的开始,将线程休眠指定
time属性的秒数,在控制台中写入一条消息,指示第一阶段的结束,并使用phaser属性的arriveAndAwaitAdvance()方法与其余任务同步。
System.out.printf("%s: Entering phase 1.\n",Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s: Finishing phase 1.\n",Thread.currentThread().getName());
phaser.arriveAndAwaitAdvance();
- 重复第二和第三阶段的行为。在第三阶段结束时,使用
arriveAndDeregister()方法而不是arriveAndAwaitAdvance()。
System.out.printf("%s: Entering phase 2.\n",Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s: Finishing phase 2.\n",Thread.currentThread().getName());
phaser.arriveAndAwaitAdvance();
System.out.printf("%s: Entering phase 3.\n",Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s: Finishing phase 3.\n",Thread.currentThread().getName());
phaser.arriveAndDeregister();
- 通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
phaser的新Phaser对象,其中包含三个参与者。
Phaser phaser=new Phaser(3);
- 创建并启动三个线程来执行三个任务对象。
for (int i=0; i<3; i++) {
Task task=new Task(i+1, phaser);
Thread thread=new Thread(task);
thread.start();
}
- 创建一个包含 10 个步骤的循环,以写入关于
phaser对象的信息。
for (int i=0; i<10; i++) {
- 写入关于已注册任务、phaser 阶段、已到达任务和未到达任务的信息。
for (int i=0; i<10; i++) {
System.out.printf("********************\n");
System.out.printf("Main: Phaser Log\n");
System.out.printf("Main: Phaser: Phase: %d\n",phaser.getPhase());
System.out.printf("Main: Phaser: Registered Parties: %d\n",phaser.getRegisteredParties());
System.out.printf("Main: Phaser: Arrived Parties: %d\n",phaser.getArrivedParties());
System.out.printf("Main: Phaser: Unarrived Parties: %d\n",phaser.getUnarrivedParties());
System.out.printf("********************\n");
- 将线程休眠 1 秒并关闭循环和类。
TimeUnit.SECONDS.sleep(1);
}
}
}
工作原理...
在这个食谱中,我们在Task类中实现了一个分阶段任务。这个分阶段任务有三个阶段,并使用Phaser接口与其他Task对象同步。主类启动三个任务,当这些任务执行它们的阶段时,它会在控制台上打印关于phaser对象状态的信息。我们使用以下方法来获取phaser对象的状态:
-
getPhase():此方法返回phaser对象的实际阶段 -
getRegisteredParties():此方法返回使用phaser对象作为同步机制的任务数 -
getArrivedParties():此方法返回已到达实际阶段结束的任务数 -
getUnarrivedParties():此方法返回尚未到达实际阶段结束的任务数
以下屏幕截图显示了程序的部分输出:

另请参阅
- 在第三章的线程同步工具中的运行并发分阶段任务食谱
监视执行器框架
执行器框架提供了一种机制,将任务的实现与线程的创建和管理分开,以执行这些任务。如果使用执行器,只需实现Runnable对象并将它们发送到执行器。执行器负责管理线程。当将任务发送到执行器时,它会尝试使用池化线程来执行此任务,以避免创建新线程。这种机制由Executor接口及其实现类ThreadPoolExecutor类提供。
在这个食谱中,您将学习如何获取关于ThreadPoolExecutor执行器状态的信息以及如何获取它。
准备工作
这个食谱的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE 如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的实现Runnable接口的类。
public class Task implements Runnable {
- 声明一个名为
milliseconds的私有long属性。
private long milliseconds;
- 实现类的构造函数以初始化其属性。
public Task (long milliseconds) {
this.milliseconds=milliseconds;
}
- 实现
run()方法。将线程休眠milliseconds属性指定的毫秒数。
@Override
public void run() {
System.out.printf("%s: Begin\n",Thread.currentThread().getName());
try {
TimeUnit.MILLISECONDS.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s: End\n",Thread.currentThread().getName());
}
- 通过创建一个名为
Main的类并实现main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 使用
Executors类的newCachedThreadPool()方法创建一个新的Executor对象。
ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newCachedThreadPool();
- 创建并提交 10 个
Task对象到执行器。使用随机数初始化对象。
Random random=new Random();
for (int i=0; i<10; i++) {
Task task=new Task(random.nextInt(10000));
executor.submit(task);
}
- 创建一个包含五个步骤的循环。在每个步骤中,调用
showLog()方法写入关于执行器的信息,并将线程休眠一秒。
for (int i=0; i<5; i++){
showLog(executor);
TimeUnit.SECONDS.sleep(1);
}
- 使用
shutdown()方法关闭执行器。
executor.shutdown();
- 创建另一个包含五个步骤的循环。在每个步骤中,调用
showLog()方法写入关于执行器的信息,并将线程休眠一秒。
for (int i=0; i<5; i++){
showLog(executor);
TimeUnit.SECONDS.sleep(1);
}
- 使用
awaitTermination()方法等待执行器的完成。
executor.awaitTermination(1, TimeUnit.DAYS);
- 显示关于程序结束的消息。
System.out.printf("Main: End of the program.\n");
}
- 实现
showLog()方法,该方法接收Executor作为参数。写入关于池的大小、任务数和执行器状态的信息。
private static void showLog(ThreadPoolExecutor executor) {
System.out.printf("*********************");
System.out.printf("Main: Executor Log");
System.out.printf("Main: Executor: Core Pool Size: %d\n",executor.getCorePoolSize());
System.out.printf("Main: Executor: Pool Size: %d\n",executor.getPoolSize());
System.out.printf("Main: Executor: Active Count: %d\n",executor.getActiveCount());
System.out.printf("Main: Executor: Task Count: %d\n",executor.getTaskCount());
System.out.printf("Main: Executor: Completed Task Count: %d\n",executor.getCompletedTaskCount());
System.out.printf("Main: Executor: Shutdown: %s\n",executor.isShutdown());
System.out.printf("Main: Executor: Terminating: %s\n",executor.isTerminating());
System.out.printf("Main: Executor: Terminated: %s\n",executor.isTerminated());
System.out.printf("*********************\n");
}
工作原理...
在此食谱中,您已经实现了一个任务,该任务会阻塞其执行线程一段随机毫秒数。然后,您已经将 10 个任务发送到执行器,同时等待它们的完成,您已经将有关执行器状态的信息写入控制台。您已使用以下方法来获取Executor对象的状态:
-
getCorePoolSize(): 此方法返回一个int数字,表示核心线程数。这是执行器在不执行任何任务时内部线程池中的最小线程数。 -
getPoolSize(): 此方法返回一个int值,表示内部线程池的实际大小。 -
getActiveCount(): 此方法返回一个int数字,表示当前正在执行任务的线程数。 -
getTaskCount(): 此方法返回一个long数字,表示已安排执行的任务数。 -
getCompletedTaskCount(): 此方法返回一个long数字,表示已由此执行器执行并已完成执行的任务数。 -
isShutdown(): 当执行器的shutdown()方法已被调用以结束其执行时,此方法返回一个Boolean值。 -
isTerminating(): 当执行器正在执行shutdown()操作但尚未完成时,此方法返回一个Boolean值。 -
isTerminated(): 当此执行器已完成其执行时,此方法返回一个Boolean值。
另请参阅
-
在第四章的创建线程执行器食谱中,线程执行器
-
在第七章的自定义 ThreadPoolExecutor 类食谱中,自定义并发类
-
在第七章的实现基于优先级的 Executor 类食谱中,自定义并发类
监视 Fork/Join 池
执行器框架提供了一种机制,允许将任务实现与执行这些任务的线程的创建和管理分离。Java 7 包括执行器框架的扩展,用于一种特定类型的问题,将改善其他解决方案的性能(如直接使用Thread对象或执行器框架)。这就是 Fork/Join 框架。
该框架旨在使用fork()和join()操作将问题分解为较小的任务来解决问题。实现此行为的主要类是ForkJoinPool类。
在此食谱中,您将学习有关ForkJoinPool类的信息以及如何获取它。
准备工作
此食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类,该类扩展了RecursiveAction类。
public class Task extends RecursiveAction{
- 声明一个私有的
int数组属性,命名为array,以存储要增加的元素数组。
private int array[];
- 声明两个私有的
int属性,命名为start和end,以存储此任务必须处理的元素块的起始和结束位置。
private int start;
private int end;
- 实现类的构造函数以初始化其属性。
public Task (int array[], int start, int end) {
this.array=array;
this.start=start;
this.end=end;
}
- 使用
compute()方法实现任务的主要逻辑。如果任务需要处理超过 100 个元素,则将这组元素分成两部分,创建两个任务来执行这些部分,使用fork()方法开始执行,使用join()方法等待其完成。
protected void compute() {
if (end-start>100) {
int mid=(start+end)/2;
Task task1=new Task(array,start,mid);
Task task2=new Task(array,mid,end);
task1.fork();
task2.fork();
task1.join();
task2.join();
- 如果任务需要处理 100 个或更少的元素,则通过在每个操作后使线程休眠 5 毫秒来增加这些元素。
} else {
for (int i=start; i<end; i++) {
array[i]++;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建名为
pool的ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 创建名为
array的整数数组,其中包含 10,000 个元素。
int array[]=new int[10000];
- 创建一个新的
Task对象来处理整个数组。
Task task1=new Task(array,0,array.length);
- 使用
execute()方法将任务发送到池中执行。
pool.execute(task1);
- 在任务未完成执行时,调用
showLog()方法以写入有关ForkJoinPool类状态的信息,并使线程休眠一秒钟。
while (!task1.isDone()) {
showLog(pool);
TimeUnit.SECONDS.sleep(1);
}
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
awaitTermination()方法等待池的完成。
pool.awaitTermination(1, TimeUnit.DAYS);
- 调用
showLog()方法以写入有关ForkJoinPool类状态的信息,并在控制台中写入程序结束的消息。
showLog(pool);
System.out.printf("Main: End of the program.\n");
- 实现
showLog()方法。它接收一个ForkJoinPool对象作为参数,并写入有关其状态以及正在执行的线程和任务的信息。
private static void showLog(ForkJoinPool pool) {
System.out.printf("**********************\n");
System.out.printf("Main: Fork/Join Pool log\n");
System.out.printf("Main: Fork/Join Pool: Parallelism: %d\n",pool.getParallelism());
System.out.printf("Main: Fork/Join Pool: Pool Size: %d\n",pool.getPoolSize());
System.out.printf("Main: Fork/Join Pool: Active Thread Count: %d\n",pool.getActiveThreadCount());
System.out.printf("Main: Fork/Join Pool: Running Thread Count: %d\n",pool.getRunningThreadCount());
System.out.printf("Main: Fork/Join Pool: Queued Submission: %d\n",pool.getQueuedSubmissionCount());
System.out.printf("Main: Fork/Join Pool: Queued Tasks: %d\n",pool.getQueuedTaskCount());
System.out.printf("Main: Fork/Join Pool: Queued Submissions: %s\n",pool.hasQueuedSubmissions());
System.out.printf("Main: Fork/Join Pool: Steal Count: %d\n",pool.getStealCount());
System.out.printf("Main: Fork/Join Pool: Terminated : %s\n",pool.isTerminated());
System.out.printf("**********************\n");
}
工作原理...
在此示例中,您已经实现了一个任务,该任务使用ForkJoinPool类和扩展RecursiveAction类的Task类来增加数组的元素;这是您可以在ForkJoinPool类中执行的任务类型之一。在任务处理数组时,您将有关ForkJoinPool类状态的信息打印到控制台。您已使用以下方法来获取ForkJoinPool类的状态:
-
getPoolSize():此方法返回一个int值,即 fork join 池的内部池的工作线程数 -
getParallelism():此方法返回为池建立的所需并行级别 -
getActiveThreadCount():此方法返回当前执行任务的线程数 -
getRunningThreadCount():此方法返回未在任何同步机制中阻塞的工作线程数 -
getQueuedSubmissionCount():此方法返回已提交到池中但尚未开始执行的任务数 -
getQueuedTaskCount():此方法返回已提交到池中并已开始执行的任务数 -
hasQueuedSubmissions():此方法返回一个Boolean值,指示此池是否有已提交但尚未开始执行的任务 -
getStealCount():此方法返回一个long值,表示工作线程从另一个线程中窃取任务的次数 -
isTerminated():此方法返回一个Boolean值,指示 fork/join 池是否已完成执行
另请参阅
-
第五章中的创建 Fork/Join 池示例,Fork/Join Framework
-
第七章中的实现 ThreadFactory 接口以为 Fork/Join 框架生成自定义线程示例,自定义并发类
-
第七章中的自定义 Fork/Join 框架中运行的任务示例,自定义并发类
编写有效的日志消息
日志系统是一种机制,允许您将信息写入一个或多个目的地。Logger 具有以下组件:
-
一个或多个处理程序:处理程序将确定日志消息的目的地和格式。您可以将日志消息写入控制台、文件或数据库。
-
一个名称:通常,Logger 的名称基于类名和其包名。
-
级别:日志消息具有与之关联的级别,指示其重要性。Logger 还具有一个级别,用于决定它将要写入哪些消息。它只会写入与其级别一样重要或更重要的消息。
您应该使用日志系统来实现以下两个主要目的:
-
捕获异常时尽可能多地写入信息。这将有助于定位错误并解决问题。
-
写入程序正在执行的类和方法的信息。
在这个示例中,你将学习如何使用java.util.logging包提供的类为你的并发应用程序添加日志系统。
准备工作
这个示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyFormatter的类,它继承了java.util.logging.Formatter类。实现抽象的format()方法。它接收一个LogRecord对象作为参数,并返回一个带有日志消息的String对象。
public class MyFormatter extends Formatter {
@Override
public String format(LogRecord record) {
StringBuilder sb=new StringBuilder();
sb.append("["+record.getLevel()+"] - ");
sb.append(new Date(record.getMillis())+" : ");
sb.append(record.getSourceClassName()+ "."+record.getSourceMethodName()+" : ");
sb.append(record.getMessage()+"\n");.
return sb.toString();
}
- 创建一个名为
MyLogger的类。
public class MyLogger {
- 声明一个私有静态的
Handler属性,名为handler。
private static Handler handler;
- 实现公共静态方法
getLogger()来创建你要用来写日志消息的Logger对象。它接收一个名为name的String参数。
public static Logger getLogger(String name){
- 使用
Logger类的getLogger()方法,获取与接收的名称相关联的java.util.logging.Logger。
Logger logger=Logger.getLogger(name);
- 使用
setLevel()方法将日志级别设置为写入所有日志消息。
logger.setLevel(Level.ALL);
- 如果 handler 属性的值为
null,则创建一个新的FileHandler对象,将日志消息写入recipe8.log文件中。使用setFormatter()方法将一个MyFormatter对象分配给该 handler 作为格式化程序。
try {
if (handler==null) {
handler=new FileHandler("recipe8.log");
Formatter format=new MyFormatter();
handler.setFormatter(format);
}
- 如果
Logger对象没有与之关联的处理程序,使用addHandler()方法分配处理程序。
if (logger.getHandlers().length==0) {
logger.addHandler(handler);
}
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
- 返回创建的
Logger对象。
return logger;
}
- 创建一个名为
Task的类,实现Runnable接口。它将是用来测试你的Logger对象的任务。
public class Task implements Runnable {
- 实现
run()方法。
@Override
public void run() {
- 首先,声明一个名为
logger的Logger对象。使用MyLogger类的getLogger()方法初始化它,传递这个类的名称作为参数。
Logger logger= MyLogger.getLogger(this.getClass().getName());
- 使用
entering()方法编写一个日志消息,指示方法执行的开始。
logger.entering(Thread.currentThread().getName(), "run()");
Sleep the thread for two seconds.
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用
exiting()方法编写一个日志消息,指示方法执行的结束。
logger.exiting(Thread.currentThread().getName(), "run()",Thread.currentThread());
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 声明一个名为
logger的Logger对象。使用MyLogger类的getLogger()方法初始化它,传递字符串Core作为参数。
Logger logger=MyLogger.getLogger("Core");
- 使用
entering()方法编写一个日志消息,指示主程序的执行开始。
logger.entering("Core", "main()",args);
- 创建一个
Thread数组来存储五个线程。
Thread threads[]=new Thread[5];
- 创建五个
Task对象和五个线程来执行它们。编写日志消息来指示你将要启动一个新线程,并指示你已经创建了该线程。
for (int i=0; i<threads.length; i++) {
logger.log(Level.INFO,"Launching thread: "+i);
Task task=new Task();
threads[i]=new Thread(task);
logger.log(Level.INFO,"Thread created: "+ threads[i].getName());
threads[i].start();
}
- 写一个日志消息来指示你已经创建了线程。
logger.log(Level.INFO,"Ten Threads created."+
"Waiting for its finalization");
- 使用
join()方法等待五个线程的完成。在每个线程完成后,编写一个日志消息,指示该线程已经完成。
for (int i=0; i<threads.length; i++) {
try {
threads[i].join();
logger.log(Level.INFO,"Thread has finished its execution",threads[i]);
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Exception", e);
}
}
- 使用
exiting()方法编写一个日志消息,指示主程序的执行结束。
logger.exiting("Core", "main()");
}
它是如何工作的...
在这个示例中,你已经使用了 Java 日志 API 提供的Logger类来在并发应用程序中写入日志消息。首先,你实现了MyFormatter类来给日志消息提供格式。这个类扩展了声明了抽象方法format()的Formatter类。这个方法接收一个带有日志消息所有信息的LogRecord对象,并返回一个格式化的日志消息。在你的类中,你使用了LogRecord类的以下方法来获取有关日志消息的信息:
-
getLevel(): 返回消息的级别 -
getMillis(): 返回消息被发送到Logger对象时的日期 -
getSourceClassName(): 返回发送消息给 Logger 的类的名称 -
getSourceMessageName(): 返回发送消息给 Logger 的方法的名称
getMessage()返回日志消息。MyLogger类实现了静态方法getLogger(),它创建一个Logger对象,并分配一个Handler对象来将应用程序的日志消息写入recipe8.log文件,使用MyFormatter格式化程序。您可以使用该类的静态方法getLogger()创建Logger对象。此方法根据传递的名称返回不同的对象。您只创建了一个Handler对象,因此所有Logger对象都将在同一个文件中写入其日志消息。您还配置了记录器以写入所有日志消息,而不管其级别如何。
最后,您已实现了一个Task对象和一个主程序,它在日志文件中写入不同的日志消息。您已使用以下方法:
-
entering(): 用FINER级别写入指示方法开始执行的消息 -
exiting(): 用FINER级别写入指示方法结束执行的消息 -
log(): 用指定级别写入消息
还有更多...
当您使用日志系统时,您必须考虑两个重要点:
-
编写必要的信息:如果您写的信息太少,日志记录器将不会有用,因为它无法实现其目的。如果您写的信息太多,将生成太大的日志文件,这将使其难以管理,并且难以获取必要的信息。
-
使用适当的消息级别:如果您使用更高级别的信息消息或更低级别的错误消息,将会使查看日志文件的用户感到困惑。在错误情况下更难知道发生了什么,或者您将获得太多信息以知道错误的主要原因。
还有其他提供比java.util.logging包更完整的日志系统的库,比如 Log4j 或 slf4j 库。但java.util.logging包是 Java API 的一部分,其所有方法都是多线程安全的,因此我们可以在并发应用中使用它而不会出现问题。
另请参阅
-
第六章中的使用非阻塞线程安全列表配方,并发集合
-
第六章中的使用阻塞线程安全列表配方,并发集合
-
第六章中的使用按优先级排序的阻塞线程安全列表配方,并发集合
-
第六章中的使用延迟元素的线程安全列表配方,并发集合
-
第六章中的使用线程安全可导航映射配方,并发集合
-
第六章中的生成并发随机数配方,并发集合
使用 FindBugs 分析并发代码
静态代码分析工具是一组分析应用程序源代码寻找潜在错误的工具。这些工具,如 Checkstyle、PMD 或 FindBugs,具有一组预定义的最佳实践规则,并解析源代码以查找违反这些规则的情况。其目标是在应用程序执行之前尽早发现错误或导致性能不佳的地方。编程语言通常提供此类工具,Java 也不例外。用于分析 Java 代码的工具之一是 FindBugs。这是一个开源工具,包括一系列规则来分析 Java 并发代码。
在此配方中,您将学习如何使用此工具分析您的 Java 并发应用程序。
准备就绪
在使用此配方之前,您应该从项目网页下载 FindBugs(findbugs.sourceforge.net/)。您可以下载一个独立的应用程序或一个 Eclipse 插件。在此配方中,您将使用独立版本。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task的类,该类扩展了Runnable接口。
public class Task implements Runnable {
- 声明一个名为
Lock的私有ReentrantLock属性。
private ReentrantLock lock;
- 实现类的构造函数。
public Task(ReentrantLock lock) {
this.lock=lock;
}
- 实现
run()方法。获取锁的控制权,使线程休眠 2 秒并释放锁。
@Override
public void run() {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 通过创建一个带有
main()方法的名为Main的类来创建示例的主类。
public class Main {
public static void main(String[] args) {
- 声明并创建一个名为
lock的ReentrantLock对象。
ReentrantLock lock=new ReentrantLock();
- 创建 10 个
Task对象和 10 个线程来执行这些任务。调用run()方法启动线程。
for (int i=0; i<10; i++) {
Task task=new Task(lock);
Thread thread=new Thread(task);
thread.run();
}
}
-
将项目导出为
jar文件。将其命名为recipe8.jar。使用 IDE 的菜单选项或javac和jar命令来编译和压缩应用程序。 -
运行
findbugs.bat命令(Windows)或findbugs.sh命令(Linux)启动 FindBugs 独立应用程序。 -
使用菜单栏中的文件菜单中的新建项目选项创建新项目。
![如何做...]()
-
FindBugs应用程序显示了一个配置项目的窗口。在项目名称字段中输入文本
Recipe08。在分析的类路径字段中添加带有项目的jar文件,在源目录字段中添加示例源代码的目录。参考以下屏幕截图:
** -
单击分析按钮以创建新项目并分析其代码。
-
FindBugs应用程序显示了代码分析的结果。在这种情况下,它发现了两个错误。
-
单击其中一个错误,您将在右侧面板看到错误的源代码,并在屏幕底部的面板中看到错误的描述。
它是如何工作的...
以下屏幕截图显示了 FindBugs 的分析结果:

分析已检测到应用程序中以下两个潜在错误:
-
一个在
Task类的run()方法中。如果抛出InterruptedExeption异常,则任务不会释放锁,因为它不会执行unlock()方法。这可能会导致应用程序中的死锁情况。 -
另一个在
Main类的main()方法中,因为您直接调用了线程的run()方法,但没有调用start()方法来开始线程的执行。
如果您在两个错误中的一个上双击,您将看到有关它的详细信息。由于您已在项目的配置中包含了源代码引用,因此您还将看到检测到错误的源代码。以下屏幕截图显示了一个示例:

还有更多...
请注意,FindBugs 只能检测一些有问题的情况(与并发代码相关或不相关)。例如,如果您在Task类的run()方法中删除unlock()调用并重复分析,FindBugs 不会警告您在任务中获取了锁但从未释放它。
使用静态代码分析工具来帮助提高代码质量,但不要期望能够检测到代码中的所有错误。
另请参阅
- 第八章中的配置 NetBeans 以调试并发代码配方,测试并发应用程序**
**# 配置 Eclipse 以调试并发代码
如今,几乎每个程序员,无论使用何种编程语言,都会使用 IDE 创建他们的应用程序。它们提供了许多有趣的功能集成在同一个应用程序中,例如:
-
项目管理
-
自动生成代码
-
自动生成文档
-
与版本控制系统集成
-
用于测试应用程序的调试器
-
创建项目和应用程序元素的不同向导
IDE 最有用的功能之一是调试器。您可以逐步执行应用程序并分析程序的所有对象和变量的值。
如果您使用 Java 编程语言,Eclipse 是最受欢迎的 IDE 之一。它具有集成的调试器,允许您测试应用程序。默认情况下,当您调试并发应用程序并且调试器找到断点时,它只会停止具有该断点的线程,而其他线程会继续执行。
在本篇文章中,您将学习如何更改该配置,以帮助您测试并发应用程序。
准备工作
您必须安装 Eclipse IDE。打开它并选择一个包含并发应用程序的项目,例如,本书中实现的某个示例。
如何做...
按照以下步骤实现示例:
-
选择菜单选项窗口|首选项。
-
在左侧菜单中,展开Java选项。
-
在左侧菜单中,选择调试选项。以下屏幕截图显示了该窗口的外观:
![如何做...]()
-
将新断点的默认挂起策略的值从挂起线程更改为挂起 VM(在屏幕截图中标为红色)。
-
单击确定按钮以确认更改。
它是如何工作的...
正如我们在本篇文章的介绍中提到的,默认情况下,在 Eclipse 中调试并发 Java 应用程序时,如果调试过程找到断点,它只会挂起首先触发断点的线程,而其他线程会继续执行。以下屏幕截图显示了这种情况的示例:

您可以看到只有worker-21被挂起(在屏幕截图中标为红色),而其他线程正在运行。但是,如果将新断点的默认挂起策略更改为挂起 VM,则在调试并发应用程序并且调试过程遇到断点时,所有线程都会暂停执行。以下屏幕截图显示了这种情况的示例:

通过更改,您可以看到所有线程都被挂起。您可以继续调试任何您想要的线程。选择最适合您需求的挂起策略。
为并发代码配置 NetBeans 调试
在今天的世界中,软件是必不可少的,以开发正常工作的应用程序,满足公司的质量标准,并且将来可以轻松修改,而且时间有限,成本尽可能低。为了实现这一目标,必须使用一个集成了多个工具(编译器和调试器)的 IDE,以便在一个公共界面下轻松开发应用程序。
如果您使用 Java 编程语言,NetBeans 是最受欢迎的 IDE 之一。它具有集成的调试器,允许您测试应用程序。
在本篇文章中,您将学习如何更改该配置,以帮助您测试并发应用程序。
准备工作
您应该已经安装了 NetBeans IDE。打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
Task1的类,并指定它实现Runnable接口。
public class Task1 implements Runnable {
- 声明两个私有的
Lock属性,命名为lock1和lock2。
private Lock lock1, lock2;
- 实现类的构造函数以初始化其属性。
public Task1 (Lock lock1, Lock lock2) {
this.lock1=lock1;
this.lock2=lock2;
}
- 实现
run()方法。首先,使用lock()方法获取lock1对象的控制权,并在控制台中写入一条消息,指示您已经获得了它。
@Override
public void run() {
lock1.lock();
System.out.printf("Task 1: Lock 1 locked\n");
- 然后,使用
lock()方法获取lock2对象的控制权,并在控制台中写入一条消息,指示您已经获得了它。
lock2.lock();
System.out.printf("Task 1: Lock 2 locked\n");
Finally, release the two lock objects. First, the lock2 object and then the lock1 object.
lock2.unlock();
lock1.unlock();
}
- 创建一个名为
Task2的类,并指定它实现Runnable接口。
public class Task2 implements Runnable{
- 声明两个私有的
Lock属性,命名为lock1和lock2。
private Lock lock1, lock2;
- 实现类的构造函数以初始化其属性。
public Task2(Lock lock1, Lock lock2) {
this.lock1=lock1;
this.lock2=lock2;
}
- 实现
run()方法。首先使用lock()方法获取lock2对象的控制权,并在控制台中写入一条消息,指示您已经获得了它。
@Override
public void run() {
lock2.lock();
System.out.printf("Task 2: Lock 2 locked\n");
- 然后使用
lock()方法获取lock1对象的控制权,并在控制台中写入一条消息,指示您已经获得了它。
lock1.lock();
System.out.printf("Task 2: Lock 1 locked\n");
- 最后,释放两个锁对象。首先是
lock1对象,然后是lock2对象。
lock1.unlock();
lock2.unlock();
}
- 通过创建名为
Main的类并向其中添加main()方法来实现示例的主类。
public class Main {
- 创建名为
lock1和lock2的两个锁对象。
Lock lock1, lock2;
lock1=new ReentrantLock();
lock2=new ReentrantLock();
- 创建名为
task1的Task1对象。
Task1 task1=new Task1(lock1, lock2);
- 创建名为
task2的Task2对象。
Task2 task2=new Task2(lock1, lock2);
- 使用两个线程执行两个任务。
Thread thread1=new Thread(task1);
Thread thread2=new Thread(task2);
thread1.start();
thread2.start();
- 当两个任务尚未完成执行时,每 500 毫秒在控制台中写入一条消息。使用
isAlive()方法检查线程是否已经完成执行。
while ((thread1.isAlive()) &&(thread2.isAlive())) {
System.out.println("Main: The example is"+ "running");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
-
在
Task1类的run()方法的第一个println()方法调用中添加断点。 -
调试程序。您将在 NetBeans 主窗口的左上角看到调试窗口。下一个屏幕截图显示了该窗口的外观,其中显示了执行
Task1对象的线程因为已到达断点而休眠,而其他线程正在运行:![如何做...]()
-
暂停主线程的执行。选择该线程,右键单击,然后选择暂停选项。以下屏幕截图显示了调试窗口的新外观。参考以下屏幕截图:
![如何做...]()
-
恢复两个暂停的线程。选择每个线程,右键单击,然后选择恢复选项。
它是如何工作的...
在使用 NetBeans 调试并发应用程序时,当调试器命中断点时,它会暂停命中断点的线程,并在左上角显示调试窗口,其中显示当前正在运行的线程。
您可以使用该窗口使用暂停或恢复选项暂停或恢复当前正在运行的线程。您还可以使用变量选项卡查看线程的变量或属性的值。
NetBeans 还包括死锁检测器。当您在调试菜单中选择检查死锁选项时,NetBeans 会对您正在调试的应用程序进行分析,以确定是否存在死锁情况。此示例呈现了明显的死锁。第一个线程首先获取锁lock1,然后获取锁lock2。第二个线程以相反的方式获取锁。插入的断点引发了死锁,但如果使用 NetBeans 死锁检测器,您将找不到任何东西,因此应谨慎使用此选项。更改两个任务中使用的锁对象的同步关键字,并再次调试程序。Task1的代码将如下所示:
@Override
public void run() {
synchronized(lock1) {
System.out.printf("Task 1: Lock 1 locked\n");
synchronized(lock2) {
System.out.printf("Task 1: Lock 2 locked\n");
}
}
}
Task2类的代码将类似于此,但更改锁的顺序。如果再次调试示例,您将再次获得死锁,但在这种情况下,它将被死锁检测器检测到,如下屏幕截图所示:

还有更多...
有选项来控制调试器。在工具菜单中选择选项选项。然后选择其他选项和Java 调试器选项卡。以下屏幕截图显示了该窗口的外观:

该窗口上有两个选项,用于控制前面描述的行为:
-
新断点暂停:使用此选项,您可以配置 NetBeans 的行为,该行为在线程中找到断点。您可以仅暂停具有断点的线程,也可以暂停应用程序的所有线程。
-
步骤摘要:使用此选项,您可以配置 NetBeans 在恢复线程时的行为。您可以只恢复当前线程或所有线程。
两个选项都已在之前呈现的屏幕截图中标记。
另请参阅
- 第八章中的为调试并发代码配置 Eclipse食谱,测试并发应用程序
使用 MultithreadedTC 测试并发代码
MultithreadedTC 是一个用于测试并发应用程序的 Java 库。它的主要目标是解决并发应用程序是非确定性的问题。您无法控制它们的执行顺序。为此,它包括一个内部的节拍器来控制应用程序的不同线程的执行顺序。这些测试线程被实现为一个类的方法。
在这个示例中,您将学习如何使用 MultithreadedTC 库为LinkedTransferQueue实现测试。
准备工作
您还必须从code.google.com/p/multithreadedtc/下载 MultithreadedTC 库和 JUnit 库,版本为 4.10,从www.junit.org/。将junit-4.10.jar和MultithreadedTC-1.01.jar文件添加到项目的库中。
如何做...
按照以下步骤实现示例:
- 创建一个名为
ProducerConsumerTest的类,它继承自MultithreadedTestCase类。
public class ProducerConsumerTest extends MultithreadedTestCase {
- 声明一个私有的
LinkedTransferQueue属性,参数化为String类,命名为queue。
private LinkedTransferQueue<String> queue;
- 实现
initialize()方法。这个方法不接收任何参数,也不返回任何值。它调用其父类的initialize()方法,然后初始化队列属性。
@Override
public void initialize() {
super.initialize();
queue=new LinkedTransferQueue<String>();
System.out.printf("Test: The test has been initialized\n");
}
- 实现
thread1()方法。它将实现第一个消费者的逻辑。调用队列的take()方法,然后将返回的值写入控制台。
public void thread1() throws InterruptedException {
String ret=queue.take();
System.out.printf("Thread 1: %s\n",ret);
}
- 实现
thread2()方法。它将实现第二个消费者的逻辑。首先,使用waitForTick()方法等待第一个线程在take()方法中休眠。然后,调用队列的take()方法,然后将返回的值写入控制台。
public void thread2() throws InterruptedException {
waitForTick(1);
String ret=queue.take();
System.out.printf("Thread 2: %s\n",ret);
}
- 实现
thread3()方法。它将实现生产者的逻辑。首先,使用waitForTick()方法两次等待两个消费者在take()方法中被阻塞。然后,调用队列的put()方法在队列中插入两个String。
public void thread3() {
waitForTick(1);
waitForTick(2);
queue.put("Event 1");
queue.put("Event 2");
System.out.printf("Thread 3: Inserted two elements\n");
}
- 最后,实现
finish()方法。在控制台中写入一条消息,指示测试已经完成执行。使用assertEquals()方法检查两个事件是否已被消耗(因此队列的大小为0)。
public void finish() {
super.finish();
System.out.printf("Test: End\n");
assertEquals(true, queue.size()==0);
System.out.printf("Test: Result: The queue is empty\n");
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Throwable {
- 创建一个名为
test的ProducerConsumerTest对象。
ProducerConsumerTest test=new ProducerConsumerTest();
- 使用
TestFramework类的runOnce()方法执行测试。
System.out.printf("Main: Starting the test\n");
TestFramework.runOnce(test);
System.out.printf("Main: The test has finished\n");
工作原理...
在这个示例中,您已经使用 MultithreadedTC 库为LinkedTransferQueue类实现了一个测试。您可以使用这个库及其节拍器为任何并发应用程序或类实现测试。在这个示例中,您已经实现了经典的生产者/消费者问题,其中有两个消费者和一个生产者。您希望测试第一个引入缓冲区的String对象是否被第一个到达缓冲区的消费者消耗,第二个引入缓冲区的String对象是否被第二个到达缓冲区的消费者消耗。
MultithreadedTC 库基于 JUnit 库,这是 Java 中最常用的用于实现单元测试的库。要使用 MultithreadedTC 库实现基本测试,您必须扩展MultithreadedTestCase类。此类扩展了junit.framework.AssertJUnit类,其中包含检查测试结果的所有方法。它不扩展junit.framework.TestCase类,因此无法将 MultithreadedTC 测试与其他 JUnit 测试集成。
然后,您可以实现以下方法:
-
initialize(): 此方法的实现是可选的。当您启动测试时执行,因此您可以使用它来初始化正在使用测试的对象。 -
finish(): 此方法的实现是可选的。当测试完成时执行。您可以使用它来关闭或释放测试期间使用的资源,或者检查测试的结果。 -
实现测试的方法:这些方法包含您实现的测试的主要逻辑。它们必须以
thread关键字开头,后跟一个字符串。例如,thread1()。
要控制线程执行顺序,您可以使用waitForTick()方法。此方法接收一个integer类型的参数,并使执行该方法的线程休眠,直到测试中运行的所有线程都被阻塞。当它们被阻塞时,MultithreadedTC 库会恢复被waitForTick()方法阻塞的线程。
您传递给waitForTick()方法的整数参数用于控制执行顺序。MultithreadedTC 库的节拍器有一个内部计数器。当所有线程都被阻塞时,库会将该计数器递增到waitForTick()调用中指定的下一个数字。
在内部,当 MultithreadedTC 库需要执行一个测试时,首先执行initialize()方法。然后,它为每个以thread关键字开头的方法创建一个线程(在您的示例中,方法thread1(),thread2()和thread3()),当所有线程都完成执行时,执行finish()方法。要执行测试,您已经使用了TestFramework类的runOnce()方法。
还有更多...
如果 MultithreadedTC 库检测到测试的所有线程都被阻塞,但没有一个线程被阻塞在waitForTick()方法中,那么测试将被声明为死锁状态,并且将抛出java.lang.IllegalStateException异常。
另请参阅
- 在第八章中的使用 FindBugs 分析并发代码食谱,测试并发应用程序**




**


浙公网安备 33010602011771号