Java7 并发秘籍(二)
原文:
zh.annas-archive.org/md5/F8E5EF0E7E4290BD7C1CC58C96A57EB0译者:飞龙
第三章:线程同步工具
在本章中,我们将涵盖:
-
控制对资源的并发访问
-
控制对多个资源副本的并发访问
-
等待多个并发事件
-
在一个公共点同步任务
-
运行并发分阶段任务
-
控制并发分阶段任务中的阶段变化
-
在并发任务之间交换数据
介绍
在第二章,基本线程同步,我们学习了同步和关键部分的概念。基本上,当多个并发任务共享一个资源时,例如一个对象或对象的属性时,我们谈论同步。访问这个共享资源的代码块被称为关键部分。
如果不使用适当的机制,可能会出现错误的结果、数据不一致或错误条件,因此我们必须采用 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子句来控制它。
另请参阅
- 在第四章的创建线程执行者配方中,线程执行者