Java9-并发秘籍第二版-全-

Java9 并发秘籍第二版(全)

原文:zh.annas-archive.org/md5/7b263b9d74e4fd8b396ceda88879636b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

当你使用计算机工作时,你可以同时做几件事情。你可以在文字处理软件中编辑文档的同时听音乐,阅读你的电子邮件。这是因为你的操作系统允许任务并发。并发编程是关于平台提供的元素和机制,以便同时运行多个任务或程序,并相互通信,交换数据或相互同步。Java 是一个并发平台,它提供了许多类来在 Java 程序中执行并发任务。随着每个版本的发布,Java 都增加了提供给程序员的函数,以简化并发程序的开发。本书涵盖了 Java 并发 API 第 9 版中包含的最重要和最有用的机制,因此你将能够直接在应用程序中使用它们。这些机制如下:

  • 基本线程管理

  • 线程同步机制

  • 使用执行器进行线程创建和管理委托

  • Fork/Join 框架以提升应用程序的性能

  • 并行流以并行方式处理大数据集,包括新的 Java 9 反应式流

  • 并发程序的数据结构

  • 调整某些并发类的默认行为以满足你的需求

  • 测试 Java 并发应用程序

本书涵盖的内容

*第一章, 线程管理,教你如何对线程进行基本操作。通过基本示例解释了线程的创建、执行和状态管理。

*第二章, 基本线程同步,介绍了如何使用低级 Java 机制来同步代码。详细解释了锁和 synchronized 关键字。

第三章, 线程同步工具,教授如何使用 Java 的高级工具来管理 Java 中线程之间的同步。它包括如何使用 Phaser 类来同步划分为不同阶段的任务。

第四章, 线程执行器,探讨了将线程管理委托给执行器的做法。它们允许运行、管理和获取并发任务的结果。

第五章, Fork/Join 框架,介绍了 Fork/Join 框架的使用。它是一种特殊的执行器,旨在执行将被划分为更小任务的任务,使用分而治之的技术。

第六章, 并行和响应式流,教你如何创建流并使用其所有中间和终端操作以并行和函数式的方式处理大量数据。流是在 Java 8 中引入的。Java 9 已经包含了一些新的接口来实现响应式流。

第七章, 并发集合,解释了如何使用 Java 语言提供的一些并发数据结构。这些数据结构必须在并发程序中使用,以避免在其实施中使用同步代码块。

第八章, 自定义并发类,教你如何将 Java 并发 API 中最有用的类适应到你的需求中。

第九章, 测试并发应用程序,介绍了如何获取 Java 7 并发 API 中最有用的一些结构的状态信息。你还将学习如何使用一些免费工具来调试并发应用程序,例如 Eclipse、NetBeans IDE 或 FindBugs 应用程序来检测应用程序中可能存在的错误。

第十章, 附加信息,探讨了同步、执行器、Fork/Join 框架、并发数据结构和并发对象的监控等概念,这些概念在相应的章节中没有包括。

第十一章, 并发编程设计,提供了一些每个程序员在开发并发应用程序时都应该考虑的建议。

本书所需内容

为了遵循本书,你需要一些 Java 编程语言的基本知识。你应该知道如何使用 IDE,例如 Eclipse 或 NetBeans,但这不是必要的前提条件。

本书面向对象

如果你是一名对进一步增强你的并发编程和多线程知识感兴趣,以及发现 Java 8 和 Java 9 的新并发特性的 Java 开发者,那么Java 9 并发食谱就是为你准备的。你应该已经熟悉一般的 Java 开发实践,并且对线程有基本的了解将是一个优势。

部分

在本书中,你会发现一些经常出现的标题(准备就绪、如何操作、工作原理、更多内容,以及参见)。

为了清楚地说明如何完成食谱,我们使用以下部分:

准备就绪

本节将告诉你可以在食谱中期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包括对上一节发生情况的详细解释。

更多内容…

本节包含有关食谱的附加信息,以便让读者对食谱有更深入的了解。

参见

本节提供了指向其他有用信息的链接,这些信息有助于食谱。

规范

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“执行main()方法的那个”

代码块设置如下:

Thread task=new PrimeGenerator(); 
task.start(); 

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中会以如下形式出现:“通过点击文件菜单下的“新建项目”选项来创建一个新项目”

警告或重要提示会以这样的框出现。

小贴士和技巧会以这样的形式呈现。

读者反馈

我们欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中受益的标题。

要向我们发送一般反馈,请简单地发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。

如果您在某个主题领域有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您的 Packt 账户下载本书的示例代码文件。www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”选项卡上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从您购买此书的下拉菜单中选择。

  7. 点击“代码下载”。

您还可以通过点击 Packt Publishing 网站上的书籍网页上的“代码文件”按钮下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录到您的 Packt 账户。

下载文件后,请确保您使用最新版本的软件解压缩或提取文件夹:

  • Windows 上的 WinRAR / 7-Zip

  • Mac 上的 Zipeg / iZip / UnRarX

  • Linux 上的 7-Zip / PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Java-9-Concurrency-Cookbook-Second-Edition。我们还有其他来自我们丰富图书和视频目录的代码包可供使用,网址为github.com/PacktPublishing/。请查看它们!

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误表部分现有的勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在勘误表部分显示。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

询问

如果您在这本书的任何方面遇到问题,您可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:线程管理

在本章中,我们将涵盖以下主题:

  • 创建、运行和设置线程的特性

  • 中断一个线程

  • 控制线程的中断

  • 睡眠和恢复线程

  • 等待线程的最终化

  • 创建和运行守护线程

  • 在线程中处理未受控的异常

  • 使用线程局部变量

  • 分组线程和处理线程组中的未受控异常

  • 通过工厂创建线程

简介

在计算机世界中,当我们谈论并发时,我们指的是一系列在计算机上同时独立且无关的任务。这种同时性可以是真实的,如果计算机有多个处理器或多核处理器,或者如果计算机只有一个核心处理器,它也可以是明显的。

所有现代操作系统都允许执行并发任务。您可以在听音乐或阅读网页上的新闻时阅读电子邮件。我们可以称这为进程级别的并发。但在一个进程内部,我们也可以有各种同时进行的任务。在进程内部运行的并发任务被称为线程。与并发相关的另一个概念是并行性。关于并发概念有不同的定义和关系。一些作者在您在单核处理器上使用多个线程执行应用程序时谈论并发。有了这个,您可以看到程序执行何时明显。他们谈论并行性,当您在多核处理器或具有多个处理器的计算机上使用多个线程执行应用程序时,这种情况也是真实的。其他作者在应用程序的线程没有预定义的顺序执行时谈论并发,他们在所有这些线程有顺序执行时讨论并行性。

本章介绍了一系列的食谱,展示了如何使用 Java 9 API 对线程执行基本操作。您将了解如何在 Java 程序中创建和运行线程,如何控制它们的执行,处理它们抛出的异常,以及如何将一些线程分组以作为一个单元来操作。

创建、运行和设置线程的特性

在这个食谱中,我们将学习如何使用 Java API 对线程执行基本操作。与 Java 语言中的每个元素一样,线程是对象。在 Java 中创建线程有两种方式:

  • 扩展Thread类并重写run()方法。

  • 创建一个实现Runnable接口和run()方法的类,然后通过传递Runnable对象作为参数来创建Thread类的对象--这是首选的方法,并且给您更多的灵活性。

在这个菜谱中,我们将使用第二种方法来创建线程。然后,我们将学习如何更改线程的一些属性。Thread 类保存一些信息属性,可以帮助我们识别线程,了解其状态或控制其优先级。这些属性包括:

  • ID:此属性存储每个线程的唯一标识符。

  • 名称:此属性存储线程的名称。

  • 优先级:此属性存储 Thread 对象的优先级。在 Java 9 中,线程的优先级介于 1 和 10 之间,其中 1 是最低优先级,10 是最高优先级。不建议更改线程的优先级。它只是对底层操作系统的提示,并不保证任何事情,但它是一个你可以使用的可能性。

  • 状态:此属性存储线程的状态。在 Java 中,线程可以存在于 Thread.State 枚举中定义的六个状态之一:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED。以下是一个列表,说明了这些状态的含义:

    • NEW:线程已被创建,但尚未启动

    • RUNNABLE:线程正在 JVM 中执行

    • BLOCKED:线程被阻塞,正在等待监视器

    • WAITING:线程正在等待另一个线程

    • TIMED_WAITING:线程正在等待另一个具有指定等待时间的线程

    • TERMINATED:线程已完成其执行

在这个菜谱中,我们将实现一个示例,该示例将创建并运行 10 个线程,这些线程将计算前 20,000 个数字内的素数。

准备工作

此菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 Calculator 的类,该类实现了 Runnable 接口:
        public class Calculator implements Runnable {

  1. 实现 run() 方法。此方法将执行我们创建的线程的指令,因此此方法将在前 20000 个数字内计算素数:
        @Override 
        public void run() { 
          long current = 1L; 
          long max = 20000L; 
          long numPrimes = 0L; 

          System.out.printf("Thread '%s': START\n",
                            Thread.currentThread().getName()); 
          while (current <= max) { 
            if (isPrime(current)) { 
              numPrimes++; 
            } 
            current++; 
          } 
          System.out.printf("Thread '%s': END. Number of Primes: %d\n",
                          Thread.currentThread().getName(), numPrimes); 
        }

  1. 然后,实现 辅助isPrime() 方法。此方法确定一个数是否为素数:
        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; 
        }

  1. 现在实现应用程序的主类。创建一个名为 Main 的类,其中包含 main() 方法:
        public class Main { 
                public static void main(String[] args) {

  1. 首先,编写一些关于线程的最大、最小和默认优先级值的信息:
        System.out.printf("Minimum Priority: %s\n",
                          Thread.MIN_PRIORITY); 
        System.out.printf("Normal Priority: %s\n",
                          Thread.NORM_PRIORITY); 
        System.out.printf("Maximun Priority: %s\n",
                          Thread.MAX_PRIORITY);

  1. 然后,创建 10 个 Thread 对象来执行 10 个 Calculator 任务。同时,创建两个数组来存储 Thread 对象及其状态。我们将在以后使用这些信息来检查线程的最终化。执行五个线程(偶数个)以最大优先级,其他五个以最小优先级:
        Thread threads[]; 
        Thread.State status[]; 
        threads = new Thread[10]; 
        status = new Thread.State[10]; 
        for (int i = 0; i < 10; i++) { 
        threads[i] = new Thread(new Calculator()); 
          if ((i % 2) == 0) { 
            threads[i].setPriority(Thread.MAX_PRIORITY); 
          } else { 
            threads[i].setPriority(Thread.MIN_PRIORITY); 
          } 
            threads[i].setName("My Thread " + i); 
        }

  1. 我们将要在一个文本文件中写入信息,因此创建一个 try-with-resources 语句来管理文件。在这个代码块内部,在启动线程之前,将线程的状态写入文件。然后,启动线程:
        try (FileWriter file = new FileWriter(".\\data\\log.txt");
        PrintWriter pw = new PrintWriter(file);) { 

          for (int i = 0; i < 10; i++) { 
            pw.println("Main : Status of Thread " + i + " : " + 
                        threads[i].getState()); 
            status[i] = threads[i].getState(); 
          } 
          for (int i = 0; i < 10; i++) { 
            threads[i].start(); 
          }

  1. 然后,等待线程的最终化。正如我们将在本章的等待线程最终化食谱中学习的那样,我们可以使用join()方法等待这一事件发生。在这种情况下,我们想在线程状态改变时写入有关线程的信息,因此不能使用此方法。我们使用以下代码块:
            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); 
              } 
            } 

          } catch (IOException e) {
            e.printStackTrace(); 
          } 
        }

  1. 在前面的代码块中,我们调用了writeThreadInfo()方法来将有关线程状态的信息写入文件。这是此方法的代码:
        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"); 
        }

  1. 运行程序并查看不同的线程是如何并行工作的。

它是如何工作的...

以下截图显示了程序的输出控制台部分。我们可以看到,我们创建的所有线程都在并行运行,以完成它们各自的工作:

在这个截图中,你可以看到线程是如何创建的,以及具有偶数编号的线程由于具有最高优先级,因此首先执行,而其他线程由于具有最低优先级,因此稍后执行。以下截图显示了log.txt文件的部分输出,我们在其中记录了线程的状态信息:

每个 Java 程序至少有一个执行线程。当你运行程序时,JVM 会运行调用程序main()方法的执行线程。

当我们调用Thread对象的start()方法时,我们正在创建另一个执行线程。我们的程序将拥有与调用start()方法的次数一样多的执行线程。

Thread类有属性来存储线程的所有信息。操作系统调度程序使用线程的优先级来选择在每个时刻使用 CPU 的线程,并根据其情况实际化每个线程的状态。

如果你没有为线程指定名称,JVM 会自动按此格式为其分配一个名称:Thread-XX,其中 XX 是一个数字。你不能修改线程的 ID 或状态。Thread类没有实现setId()setStatus()方法,因为这些方法会在代码中引入修改。

当 Java 程序的所有线程都结束时(更具体地说,当所有非守护线程都结束时),Java 程序结束。如果初始线程(执行main()方法的线程)结束,其余的线程将继续执行,直到它们完成。如果其中一个线程使用System.exit()指令结束程序的执行,所有线程将结束各自的执行。

创建Thread类的对象不会创建一个新的执行线程。同样,调用实现Runnable接口的类的run()方法也不会创建一个新的执行线程。只有当你调用start()方法时,才会创建一个新的执行线程。

还有更多...

如本菜谱的引言中所述,还有另一种创建新执行线程的方法。您可以实现一个扩展Thread类的类,并覆盖该类的run()方法。然后,您可以创建此类的一个对象并调用start()方法以创建一个新的执行线程。

您可以使用Thread类的静态方法currentThread()来访问正在运行当前对象的线程对象。

您必须考虑到,如果您尝试设置不在 1 到 10 之间的优先级,setPriority()方法可能会抛出IllegalArgumentException异常。

参见

  • 本章的通过工厂创建线程菜谱

中断线程

一个具有多个执行线程的 Java 程序仅在所有线程的执行结束(更具体地说,当所有非守护线程结束执行或当其中一个线程使用System.exit()方法时)后才结束。有时,您可能需要结束一个线程,因为您想终止程序或当程序的用户想要取消线程对象正在执行的任务时。

Java 提供了一个中断机制,该机制指示线程您想要结束它。这个机制的一个特点是线程对象必须检查它们是否被中断,并且它们可以决定是否响应终止请求。线程对象可以忽略它并继续执行。

在这个菜谱中,我们将开发一个程序,创建一个线程并在 5 秒后通过中断机制强制其终止。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为PrimeGenerator的类,该类扩展了Thread类:
        public class PrimeGenerator extends Thread{

  1. 覆盖run()方法,包括一个无限循环。在这个循环中,从 1 开始处理连续的数字。对于每个数字,计算它是否为素数;如果是,就像这个例子一样,将其写入控制台:
@Override 
        public void run() { 
          long number=1L; 
          while (true) { 
            if (isPrime(number)) { 
              System.out.printf("Number %d is Prime\n",number); 
            }

  1. 在处理一个数字后,通过调用isInterrupted()方法检查线程是否被中断。如果此方法返回true,则线程已被中断。在这种情况下,我们在控制台写入一条消息并结束线程的执行:
            if (isInterrupted()) { 
              System.out.printf("The Prime Generator has been
                                 Interrupted"); 
              return; 
            } 
            number++; 
          } 
        }

  1. 实现一个名为isPrime()的方法。您可以从本章的创建、运行和设置线程信息菜谱中获取其代码。

  2. 现在通过实现一个名为Main的类和main()方法来实现示例的主类:

        public class Main { 
          public static void main(String[] args) {

  1. 创建并启动PrimeGenerator类的一个对象:
        Thread task=new PrimeGenerator(); 
        task.start();

  1. 等待 5 秒并中断PrimeGenerator线程:
        try { 
          Thread.sleep(5000); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 
        task.interrupt();

  1. 然后,写入与中断线程状态相关的信息。此代码的输出将取决于线程是在执行结束之前还是之后结束:
          System.out.printf("Main: Status of the Thread: %s\n",
                            task.getState()); 
          System.out.printf("Main: isInterrupted: %s\n",
                            task.isInterrupted()); 
          System.out.printf("Main: isAlive: %s\n", task.isAlive()); 
        }

  1. 运行示例并查看结果。

它是如何工作的...

以下截图显示了上一个示例的执行结果。我们可以看到当 PrimeGenerator 线程检测到它被中断时,它会写入消息并结束其执行。请参考以下截图:

Thread 类有一个属性,用于存储一个表示线程是否被中断的 boolean 值。当你调用线程的 interrupt() 方法时,你将该属性设置为 trueisInterrupted() 方法仅返回该属性的值。

main() 方法会写入有关中断线程状态的信息。在这种情况下,由于此代码在线程完成执行之前执行,状态为 RUNNABLEisInterrupted() 方法的返回值为 true,同样 isAlive() 方法的返回值也是 true。如果中断的 Thread 在此代码块执行之前完成其执行(例如,你可以让主线程休眠一秒钟),则 isInterrupted()isAlive() 方法将返回 false 值。

更多内容...

Thread 类还有一个方法来检查线程是否被中断。这是静态方法 interrupted(),它检查当前线程是否被中断。

isInterrupted()interrupted() 方法之间存在一个重要的区别。第一个方法不会改变中断属性值,但第二个方法将其设置为 false

如前所述,线程对象可以忽略其中断,但这不是预期的行为。

控制线程的中断

在上一个菜谱中,你学习了如何中断线程的执行以及如何在线程对象中控制这种中断。如果可以中断的线程很简单,那么前面示例中显示的机制可以使用。但如果线程实现了一个复杂算法,该算法被分成几个方法,或者它有递归调用的方法,我们将需要使用更好的机制来控制线程的中断。Java 提供了 InterruptedException 异常来达到这个目的。当你检测到线程被中断时,你可以抛出这个异常,并在 run() 方法中捕获它。

在本菜谱中,我们将实现一个任务,该任务将在文件夹及其所有子文件夹中查找具有指定名称的文件。这是为了展示如何使用 InterruptedException 异常来控制线程的中断。

准备工作

本菜谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为 FileSearch 的类,并指定它实现 Runnable 接口:
        public class FileSearch implements Runnable {

  1. 声明两个私有属性:一个用于我们即将搜索的文件名,另一个用于初始文件夹。实现类的构造函数,初始化这些属性:
        private String initPath; 
        private String fileName; 
        public FileSearch(String initPath, String fileName) { 
          this.initPath = initPath; 
          this.fileName = fileName; 
        }

  1. 实现 FileSearch 类的 run() 方法。它检查属性 fileName 是否是目录;如果是,它将调用 directoryProcess() 方法。此方法可能会抛出 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()); 
            } 
          } 
        }

  1. 实现一个名为 directoryProcess() 的方法。此方法将获取文件夹中的文件和子文件夹,并对它们进行处理。对于每个目录,该方法将进行递归调用,并将目录作为参数传递。对于每个文件,该方法将调用 fileProcess() 方法。在处理完所有文件和文件夹后,该方法将检查线程是否被中断;如果是,就像在这个例子中,它将抛出一个 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(); 
          } 
        }

  1. 实现一个名为 fileProcess() 的方法。此方法将比较它正在处理的文件名与我们正在搜索的文件名。如果名称相等,我们将在控制台写入一条消息。在此比较之后,线程将检查它是否被中断;如果是,就像在这个例子中,它将抛出一个 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(); 
          } 
        }

  1. 现在我们来实现示例的主类。实现一个名为 Main 的类,其中包含 main() 方法:
        public class Main { 
          public static void main(String[] args) {

  1. 创建并初始化 FileSearch 类的对象和线程以执行其任务。然后开始执行线程。我使用了 Windows 操作系统的路径。如果您使用其他操作系统,例如 Linux 或 iOS,请将路径更改为您操作系统上存在的路径:
        FileSearch searcher = new FileSearch("C:\\Windows",
                                             "explorer.exe");
        Thread thread=new Thread(searcher); 
        thread.start();

  1. 等待 10 秒并中断线程:
          try { 
            TimeUnit.SECONDS.sleep(10); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
          thread.interrupt(); 
        }

  1. 运行示例并查看结果。

它是如何工作的...

以下截图显示了此示例的执行结果。您可以看到当 FileSearch 对象检测到它被中断时,它如何结束其执行。

在这个例子中,我们使用 Java 异常来控制线程的中断。当您运行示例时,程序开始通过检查文件夹中是否有文件来遍历文件夹。例如,如果您进入 \b\c\d 文件夹,程序将对 directoryProcess() 方法进行三次递归调用。当它检测到被中断时,它将抛出一个 InterruptedException 异常,并在 run() 方法中继续执行,无论已经进行了多少次递归调用。

还有更多...

InterruptedException 异常是由一些与并发 API 相关的 Java 方法抛出的,例如 sleep()。在这种情况下,如果线程在睡眠时被中断(使用 interrupt() 方法),则会抛出此异常。

参见

  • 本章的 中断线程 菜单

睡眠和恢复线程

有时候,你可能想在确定的时间内暂停线程的执行。例如,程序中的线程每分钟检查一次传感器状态。其余时间,它什么都不做。在这段时间内,线程不使用计算机的任何资源。当这个周期结束后,线程将准备好在操作系统调度器选择它执行时继续执行。你可以使用Thread类的sleep()方法来实现这个目的。此方法接收一个长整型参数,表示线程将暂停执行多少毫秒。在那之后,线程将在sleep()方法的下一个指令继续执行,当 JVM 分配给它 CPU 时间时。

另一种可能性是使用TimeUnit枚举元素中的sleep()方法。此方法使用Thread类的sleep()方法将当前线程休眠,但它接收的参数是以它所代表的单位,并将其转换为毫秒。

在本菜谱中,我们将开发一个使用sleep()方法每秒写入实际日期的程序。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为ConsoleClock的类,并指定它实现Runnable接口:
        public class ConsoleClock implements Runnable {

  1. 实现该run()方法:
        @Override 
        public void run() {

  1. 编写一个包含 10 次迭代的循环。在每次迭代中,创建一个Date对象,将其写入控制台,并调用TimeUnit类中SECONDS属性的sleep()方法来暂停线程的执行 1 秒。使用这个值,线程将睡眠大约 1 秒。由于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"); 
            } 
          } 
        }

  1. 我们已经实现了线程。现在让我们实现示例的主类。创建一个名为Main的类,其中包含main()方法:
        public class Main { 
          public static void main(String[] args) {

  1. 创建FileClock类的一个对象和一个执行它的thread。然后,开始执行线程:
        FileClock clock=new FileClock(); 
        Thread thread=new Thread(clock); 
        thread.start();

  1. 在主线程中调用TimeUnit类中SECONDS属性的sleep()方法以等待 5 秒:
        try { 
          TimeUnit.SECONDS.sleep(5); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        };

  1. 中断FileClock线程:
        thread.interrupt();

  1. 运行示例并查看结果。

它是如何工作的...

当你运行示例时,你会看到程序每秒写入一个Date对象,以及指示FileClock线程已被中断的消息。

当你调用sleep()方法时,线程离开 CPU 并停止执行一段时间。在这段时间内,它不消耗 CPU 时间,因此 CPU 可以执行其他任务。

当线程正在睡眠并被中断时,该方法会立即抛出InterruptedException异常,而不会等待睡眠时间结束。

还有更多...

Java 并发 API 还有一个使线程对象离开 CPU 的方法。它是yield()方法,它指示 JVM 线程对象可以离开 CPU 以执行其他任务。JVM 不保证它会遵守这个请求。通常,它仅用于调试目的。

等待线程的最终化

在某些情况下,我们将不得不等待线程(run()方法结束执行)的执行结束。例如,我们可能有一个程序,在继续执行其余部分之前,将开始初始化它需要的资源。我们可以将初始化任务作为线程运行,并在继续程序的其他部分之前等待它们的最终化。

为了这个目的,我们可以使用Thread类的join()方法。当我们使用线程对象调用此方法时,它会暂停调用线程的执行,直到被调用的对象完成执行。

在本食谱中,我们将通过初始化示例学习使用此方法。

准备工作

本食谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为DataSourcesLoader的类,并指定它实现Runnable接口:
        public class DataSourcesLoader implements Runnable {

  1. 实现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()); 
        }

  1. 创建一个名为NetworkConnectionsLoader的类,并指定它实现Runnable接口。实现run()方法。它将等于DataSourcesLoader类的run()方法,但它将睡眠 6 秒钟。

  2. 现在,创建一个名为Main的类,其中包含main()方法:

        public class Main { 
          public static void main(String[] args) {

  1. 创建一个DataSourcesLoader类的对象和一个运行它的线程:
       DataSourcesLoader dsLoader = new DataSourcesLoader(); 
       Thread thread1 = new Thread(dsLoader,"DataSourceThread");

  1. 创建一个NetworkConnectionsLoader类的对象和一个运行它的线程:
        NetworkConnectionsLoader ncLoader = new NetworkConnectionsLoader(); 
        Thread thread2 = new Thread(ncLoader,"NetworkConnectionLoader");

  1. 调用两个线程对象的start()方法:
        thread1.start(); 
        thread2.start();

  1. 使用join()方法等待两个线程的最终化。此方法可能会抛出InterruptedException异常,因此我们必须包含捕获它的代码:
       try { 
          thread1.join(); 
          thread2.join(); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 写一条消息来指示程序的结束:
        System.out.printf("Main: Configuration has been loaded: %s\n",
                          new Date());

  1. 运行程序并查看结果。

它是如何工作的...

当你运行这个程序时,你会理解线程对象是如何开始执行的。首先,DataSourcesLoader线程完成其执行。然后,NetworkConnectionsLoader类完成其执行。在这个时候,main线程对象继续执行并写入最终消息。

还有更多...

Java 提供了join()方法的两种附加形式:

  • join(long milliseconds)

  • join (long milliseconds, long nanos)

join() 方法的第一个版本中,调用线程不是无限期地等待被调用线程的终止,而是等待方法参数指定的毫秒数。例如,如果对象 thread1thread2.join(1000),则 thread1 暂停其执行,直到以下两个条件之一满足:

  • thread2 已完成其执行

  • 已经过去 1,000 毫秒

当这两个条件之一为真时,join() 方法返回。您可以通过检查线程的状态来了解 join() 方法是因为它完成了执行还是因为指定的超时时间已过而返回。

join() 方法的第二个版本与第一个版本类似,但它接收毫秒数和纳秒数作为参数。

创建和运行守护线程

Java 有一种特殊的线程称为 守护线程。当守护线程是程序中唯一运行的线程时,JVM 在完成这些线程后结束程序。

具有这些特性的守护线程通常用作同一程序中运行的正常(也称为 用户)线程的服务提供者。它们通常有一个无限循环,等待服务请求或执行线程的任务。这些线程的典型示例是 Java 垃圾收集器。

在本例中,我们将通过开发一个包含两个线程的示例来学习如何创建守护线程:一个用户线程将在队列上写入事件,一个守护线程将清理队列,移除生成时间超过 10 秒的事件。

准备工作

本例子的实现使用了 Eclipse IDE。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建 Event 类。此类仅存储程序将处理的事件的信息。声明两个私有属性:一个称为 java.util.Date 类型的日期,另一个称为 String 类型的 event。生成写入和读取其值的方法。

  2. 创建 WriterTask 类并指定它实现 Runnable 接口:

        public class WriterTask implements Runnable {

  1. 声明存储事件的队列并实现初始化此队列的类的构造函数:
        private Deque<Event> deque; 
        public WriterTask (Deque<Event> deque){ 
          this.deque=deque; 
        }

  1. 实现此任务的 run() 方法。该方法将有一个 100 次迭代的循环。在每次迭代中,我们创建一个新的事件,将其保存到队列中,并暂停 1 秒:
        @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(); 
            } 
          } 
        }

  1. 创建 CleanerTask 类并指定它扩展 Thread 类:
        public class CleanerTask extends Thread {

  1. 声明存储事件的队列并实现初始化此队列的类的构造函数。在构造函数中,使用 setDaemon() 方法将此线程标记为守护线程:
        private Deque<Event> deque; 
        public CleanerTask(Deque<Event> deque) { 
          this.deque = deque; 
          setDaemon(true); 
        }

  1. 实现该 run() 方法。它有一个无限循环,获取实际日期并调用 clean() 方法:
        @Override 
        public void run() { 
          while (true) { 
            Date date = new Date(); 
            clean(date); 
          } 
        }

  1. 实现一个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()); 
          } 
        }

  1. 现在实现main类。创建一个名为Main的类,其中包含一个main()方法:
        public class Main { 
          public static void main(String[] args) {

  1. 使用Deque类创建存储事件的队列:
        Deque<Event> deque=new ConcurrentLinkedDeque<Event>();

  1. 创建并启动尽可能多的WriterTask线程,数量与 JVM 可用的处理器数量相同,以及一个CleanerTask方法:
        WriterTask writer=new WriterTask(deque); 
        for (int i=0; i< Runtime.getRuntime().availableProcessors();
             i++){ 
          Thread thread=new Thread(writer); 
          thread.start(); 
        } 
        CleanerTask cleaner=new CleanerTask(deque); 
        cleaner.start();

  1. 运行程序并查看结果。

它是如何工作的...

如果你分析程序的一次执行输出,你会看到队列开始增长,直到它达到我们案例中的40个事件大小。然后,它的大小将围绕40个事件波动,直到执行结束。这个大小可能取决于你机器的核心数。我在一个四核处理器上执行了代码,所以我们启动了四个WriterTask任务。

程序从四个WriterTask线程开始。每个线程写入一个事件并休眠 1 秒。在最初的10秒后,队列中有40个事件。在这10秒内,CleanerTask正在执行,而四个WriterTask线程处于休眠状态;然而,它并没有删除任何事件,因为所有事件都是在10秒前生成的。在剩余的执行过程中,CleanerTask每秒删除四个事件,而四个WriterTask线程再写入四个;因此,队列的大小在40个事件左右波动。记住,这个示例的执行取决于你电脑的 JVM 可用的核心数。通常,这个数字等于你 CPU 的核心数。

你可以调整时间,直到WriterTask线程处于休眠状态。如果你使用较小的值,你会看到CleanerTask的 CPU 时间更少,队列的大小会增加,因为CleanerTask不会删除任何事件。

还有更多...

你只能在调用start()方法之前调用setDaemon()方法。一旦线程开始运行,就不能通过调用setDaemon()方法来修改其守护状态。如果你调用它,你会得到一个IllegalThreadStateException异常。

你可以使用isDaemon()方法来检查一个线程是否是守护线程(该方法返回true)或非守护线程(该方法返回false)。

在线程中处理未受控的异常

在每种编程语言中,一个非常重要的方面是帮助你在应用程序中管理错误情况的机制。Java 编程语言,就像几乎所有现代编程语言一样,实现了一个基于异常的机制来管理错误情况。当检测到错误情况时,Java 类会抛出这些异常。你也可以使用这些异常或实现你自己的异常来管理你在类中产生的错误。

Java 还提供了一个机制来捕获和处理这些异常。有一些异常必须使用方法的throws子句捕获或重新抛出。这些异常被称为检查异常。还有一些异常不需要指定或捕获。这些是未检查异常:

  • 检查异常:这些必须在方法的throws子句中指定或在其中捕获,例如IOExceptionClassNotFoundException

  • 未检查异常:这些不需要指定或捕获,例如NumberFormatException

当线程对象的run()方法内部抛出检查异常时,我们必须捕获并处理它们,因为run()方法不接受throws子句。当线程对象的run()方法内部抛出未检查异常时,默认行为是在控制台写入堆栈跟踪并退出程序。

幸运的是,Java 为我们提供了一个机制来捕获和处理线程对象中抛出的未检查异常,以避免程序结束。

在本食谱中,我们将通过示例学习这个机制。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 首先,我们必须实现一个类来处理未检查异常。这个类必须实现UncaughtExceptionHandler接口并实现该接口中声明的uncaughtException()方法。这是一个包含在Thread类中的接口。在我们的情况下,让我们称这个类为ExceptionHandler并创建一个方法来写入有关Exception和抛出它的Thread的信息。以下是其代码:
        public class ExceptionHandler implements UncaughtExceptionHandler { 
          @Override 
          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()); 
          } 
        }

  1. 现在实现一个抛出未检查异常的类。将此类命名为Task,指定它实现Runnable接口,实现run()方法,并强制抛出异常;例如,尝试将String值转换为int值:
        public class Task implements Runnable { 
          @Override 
          public void run() { 
            int numero=Integer.parseInt("TTT"); 
          } 
        }

  1. 现在实现示例的主类。实现一个名为Main的类,并实现其main()方法:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个Task对象和一个Thread来运行它。使用setUncaughtExceptionHandler()方法设置未捕获异常处理器并开始执行线程:
            Task task=new Task(); 
            Thread thread=new Thread(task); 
            thread.setUncaughtExceptionHandler(new ExceptionHandler()); 
            thread.start(); 
          } 
        }

  1. 运行示例并查看结果。

它是如何工作的...

在以下屏幕截图,你可以看到示例执行的输出结果。异常被抛出并由写入有关Exception和抛出它的Thread信息的处理器捕获。这些信息在控制台显示:

图片

当线程中抛出异常且未被捕获(它必须是一个未检查异常)时,JVM 会检查线程是否设置了相应方法指定的未捕获异常处理器。如果设置了,JVM 会使用Thread对象和Exception作为参数调用此方法。

如果线程没有未捕获异常处理程序,JVM 将在控制台打印堆栈跟踪,并结束抛出异常的线程的执行。

还有更多...

Thread类还有一个与未捕获异常处理过程相关的静态方法。它是setDefaultUncaughtExceptionHandler(),它为应用程序中所有线程对象设置异常处理程序。

当线程抛出未捕获异常时,JVM 会寻找三种可能的异常处理程序。

首先它查找线程对象的未捕获异常处理程序,正如我们在本配方中学到的。如果此处理程序不存在,JVM 将查找ThreadGroup的未捕获异常处理程序,正如我们在将线程分组并处理线程组中的未受控异常配方中解释的那样。如果此方法不存在,JVM 将查找默认的未捕获异常处理程序,正如我们在本配方中学到的。

如果没有处理程序退出,JVM 将在控制台写入异常的堆栈跟踪,并结束抛出异常的线程的执行。

参见

  • 本章的将线程分组并处理线程组中的未受控异常配方

使用线程局部变量

并发应用程序中最关键的一个方面是共享数据。这在扩展Thread类或实现Runnable接口的对象中,以及在两个或更多线程之间共享的对象中具有特殊的重要性。

如果你创建了一个实现Runnable接口的类的对象,然后使用相同的Runnable对象启动各种线程对象,所有这些线程都会共享相同的属性。这意味着如果你在一个线程中更改一个属性,所有线程都会受到影响。

有时候,你可能希望有一个属性,它不会被运行相同对象的全部线程共享。Java 并发 API 提供了一个性能非常好的干净机制,称为线程局部变量。它们也有一些缺点。它们在线程存活期间保留其值。在线程重用的情况下,这可能会成为问题。

在这个配方中,我们将开发两个程序:一个将暴露第一段中的问题,另一个将使用线程局部变量机制解决这个问题。

准备工作

本配方的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或 NetBeans 等不同的 IDE,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 首先,实现一个暴露了之前问题的程序。创建一个名为UnsafeTask的类,并指定它实现Runnable接口。声明一个私有的java.util.Date属性:
        public class UnsafeTask implements Runnable{ 
          private Date startDate;

  1. 实现对象UnsafeTaskrun()方法。此方法将初始化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); 
        }

  1. 现在,实现这个有问题的应用程序的主类。创建一个名为Main的类,并具有main()方法。此方法将创建一个UnsafeTask类的对象,并使用此对象启动10个线程,每个线程之间暂停 2 秒:
        public class Main { 
          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(); 
              } 
            } 
          } 
        }

  1. 在以下屏幕截图中,您可以看到此程序执行的结果。每个线程都有一个不同的开始时间,但它们完成时,属性值发生了变化。因此,它们正在写入一个错误值。例如,检查 ID 为 13 的线程:

图片

  1. 如前所述,我们将使用线程局部变量机制来解决这个问题。

  2. 创建一个名为SafeTask的类,并指定它实现Runnable接口:

        public class SafeTask implements Runnable {

  1. 声明一个ThreadLocal<Date>类的对象。此对象将具有隐式实现,包括initialValue()方法。此方法将返回实际日期:
           private static ThreadLocal<Date> startDate=new
                                                ThreadLocal<Date>(){ 
          protected Date initialValue(){ 
            return new Date(); 
          } 
        };

  1. 实现run()方法。它具有与UnsafeTask类的run()方法相同的功能,但它更改了访问startDate属性的方式。现在我们将使用startDate对象的get()方法:
        @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()); 
        }

  1. 本例中的Main类与不安全的示例相同。唯一的区别是它更改了Runnable类的名称。

  2. 运行示例并分析差异。

它是如何工作的...

在以下屏幕截图中,您可以看到安全样本执行的结果。十个Thread对象都有自己的startDate属性值:

图片

线程局部变量机制为使用这些变量的每个线程存储一个属性的值。您可以使用get()方法读取值,并使用set()方法更改值。第一次访问线程局部变量的值时,如果它为调用的线程对象没有值,线程局部变量将调用initialValue()方法为该线程分配一个值并返回初始值。

还有更多...

线程局部类还提供了remove()方法,用于删除调用线程的线程局部变量中存储的值。

Java 并发 API 包括InheritableThreadLocal类,它为从线程创建的线程提供值的继承。如果线程A在线程局部变量中有一个值,并且它创建了另一个线程B,那么线程B将具有与线程A相同的线程局部变量值。您可以通过覆盖在线程局部变量中初始化子线程值的childValue()方法来实现。它接收线程局部变量中的父线程值作为参数。

对线程进行分组和处理线程组中的未受控异常

Java 并发 API 提供的一个有趣的功能是能够对线程进行分组。这允许我们将一个组的线程视为单个单元,并提供访问属于该组的线程对象,以便对它们进行操作。例如,您有一些线程正在执行相同的任务,您想控制它们。例如,您可以通过单个调用中断该组中所有线程。

Java 提供了 ThreadGroup 类来处理线程组。可以通过线程对象和另一个 ThreadGroup 对象形成 ThreadGroup 对象,生成线程的树状结构。

控制线程中断 的示例中,您学习了如何使用通用方法处理在线程对象中抛出的所有未捕获异常。在 处理线程中的未受控异常 的示例中,我们编写了一个处理程序来处理线程抛出的未捕获异常。我们可以使用类似的机制来处理线程或线程组抛出的未捕获异常。

在本例中,我们将学习如何与 ThreadGroup 对象一起工作,以及如何在多个线程的组中实现和设置处理未捕获异常的处理程序。我们将通过一个示例来完成这项工作。

准备工作

本例的示例是通过 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 首先,通过创建一个名为 MyThreadGroup 的类来扩展 ThreadGroup 类,该类将扩展 ThreadGroup。您必须声明一个带有参数的构造函数,因为 ThreadGroup 类没有不带参数的构造函数。扩展 ThreadGroup 类以重写 uncaughtException() 方法,以便处理组中线程抛出的异常:
        public class MyThreadGroup extends ThreadGroup { 
          public MyThreadGroup(String name) { 
            super(name); 
          }

  1. 重写 uncaughtException() 方法。当 ThreadGroup 类中的一个线程抛出异常时,将调用此方法。在这种情况下,该方法将写入有关异常及其抛出线程的信息;它将在控制台中呈现这些信息。此外,请注意,此方法将中断 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(); 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 实现 run() 方法。在这种情况下,我们将引发 AritmethicException 异常。为此,我们将用随机数除以 1,000,直到随机生成器生成零以抛出异常:
        @Override 
        public void run() { 
          int result; 
          Random random=new Random(Thread.currentThread().getId()); 
          while (true) { 
            result=1000/((int)(random.nextDouble()*1000000000)); 
            if (Thread.currentThread().isInterrupted()) { 
              System.out.printf("%d : Interrupted\n",
                                Thread.currentThread().getId()); 
              return; 
            } 
          } 
        }

  1. 现在,通过创建一个名为 Main 的类并实现 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 首先,计算你将要启动的线程数量。我们使用Runtime类的availableProcessors()方法(我们通过该类的静态方法getRuntime()获取与当前 Java 应用程序关联的运行时对象)。此方法返回 JVM 可用的处理器数量,通常等于运行应用程序的计算机的核心数:
        int numberOfThreads = 2 * Runtime.getRuntime()
                                      .availableProcessors();

  1. 创建MyThreadGroup类的对象:
        MyThreadGroup threadGroup=new MyThreadGroup("MyThreadGroup");

  1. 创建Task类的对象:
        Task task=new Task();

  1. 使用此Task类创建计算出的线程数量,并启动它们:
        for (int i = 0; i < numberOfThreads; i++) { 
          Thread t = new Thread(threadGroup, task); 
          t.start(); 
        }

  1. 然后,在控制台中写入有关ThreadGroup的信息:
        System.out.printf("Number of Threads: %d\n",
                          threadGroup.activeCount()); 
        System.out.printf("Information about the Thread Group\n"); 
        threadGroup.list();

  1. 最后,写出组成该组的线程的状态:
            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()); 
            } 
          } 
        }

  1. 运行示例并查看结果。

它是如何工作的...

在以下屏幕截图中,你可以看到ThreadGroup类的list()方法输出以及我们写入每个Thread对象状态时生成的输出:

ThreadGroup类存储与它关联的线程对象和其他ThreadGroup对象,以便它可以访问它们的所有信息(例如状态)并对其所有成员执行操作(例如中断)。

查看其中一个线程对象如何抛出中断其他对象的异常:

Thread对象中抛出未捕获的异常时,JVM 会寻找三个可能的异常处理器:

首先,它寻找线程的未捕获异常处理器,如在线程中处理未受控异常配方中所述。如果此处理器不存在,那么 JVM 将寻找线程的ThreadGroup类的未捕获异常处理器,如本配方中所学。如果此方法不存在,JVM 将寻找默认的未捕获异常处理器,如在线程中处理未受控异常配方中所述。

如果没有处理器存在,JVM 将在控制台写入异常的堆栈跟踪,并结束抛出异常的线程的执行。

参考以下内容

  • 在线程中处理未受控异常的配方

通过工厂创建线程

工厂模式是面向对象编程世界中用得最多的设计模式之一。它是一个创建型模式,其目标是开发一个对象,其使命应该是创建一个或多个类中的对象。有了这个,如果你想创建这些类中的一个对象,你就可以使用工厂而不是使用 new 运算符。

使用这个工厂,我们可以集中创建对象,并带来一些优势:

  • 改变创建的对象的类或创建方式很容易。

  • 对于有限的资源,限制对象的创建很容易;例如,我们只能有给定类型的n个对象。

  • 生成关于对象创建的统计数据很容易。

Java 提供了一个接口,即 ThreadFactory 接口,用于实现线程对象工厂。Java 并发 API 的一些高级实用工具使用线程工厂来创建线程。

在这个菜谱中,你将学习如何实现一个 ThreadFactory 接口,以创建具有个性化名称的线程对象,同时保存创建的线程对象的统计信息。

准备工作

这个菜谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 MyThreadFactory 的类,并指定它实现 ThreadFactory 接口:
       public class MyThreadFactory implements ThreadFactory {

  1. 声明三个属性:一个名为 counter 的整数,我们将用它来存储创建的线程对象数量;一个名为 name 的字符串,包含每个创建的线程的基本名称;以及一个名为 stats 的字符串对象列表,用于保存关于创建的线程对象的统计数据。同时,实现类的构造函数,初始化这些属性:
        private int counter; 
        private String name; 
        private List<String> stats; 

        public MyThreadFactory(String name){ 
          counter=0; 
          this.name=name; 
          stats=new ArrayList<String>(); 
        }

  1. 实现 newThread() 方法。这个方法将接收一个 Runnable 接口,并返回一个对应于这个 Runnable 接口的线程对象。在我们的例子中,我们生成线程对象的名称,创建新的线程对象,并保存统计信息:
        @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; 
        }

  1. 实现 getStatistics() 方法;它返回一个包含所有创建的线程对象统计数据的 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(); 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口。在这个例子中,这些任务除了休眠 1 秒外,将不会做任何事情:
        public class Task implements Runnable { 
          @Override 
          public void run() { 
            try { 
              TimeUnit.SECONDS.sleep(1); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 创建示例的主类。创建一个名为 Main 的类,并实现 main() 方法:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个 MyThreadFactory 对象和一个 Task 对象:
        MyThreadFactory factory=new MyThreadFactory("MyThreadFactory"); 
        Task task=new Task();

  1. 使用 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(); 
        }

  1. 将线程工厂的统计信息写入控制台:
        System.out.printf("Factory stats:\n"); 
        System.out.printf("%s\n",factory.getStats());

  1. 运行示例并查看结果。

工作原理...

ThreadFactory 接口只有一个方法,称为 newThread()。它接收一个 Runnable 对象作为参数,并返回一个 Thread 对象。当你实现 ThreadFactory 接口时,你必须实现它并重写 newThread 方法。最基本的 ThreadFactory 只有一行:

    return new Thread(r);

你可以通过添加一些变体来改进这个实现,如下所示:

  • 创建个性化的线程,如示例中所示,使用特殊的名称格式,甚至创建自己的 Thread 类,该类将继承 Java 的 Thread

  • 保存线程创建统计信息,如前一个示例所示

  • 限制创建的线程数量

  • 验证线程的创建

你可以将任何你能想到的其他内容添加到前面的列表中。使用工厂设计模式是一种良好的编程实践,但如果你要实现一个ThreadFactory接口来集中创建线程,你将不得不审查代码以确保所有线程都是使用相同的工厂创建的。

参见

  • 参见第八章中的实现 ThreadFactory 接口以生成自定义线程在 Executor 对象中使用我们的 ThreadFactory配方,自定义并发类

第二章:基本线程同步

在本章中,我们将涵盖以下主题:

  • 同步方法

  • 在同步代码中使用条件

  • 使用锁同步代码块

  • 使用读写锁同步数据访问

  • 在锁中使用多个条件

  • 使用 StampedLock 类的高级锁定

简介

并发编程中最常见的情况之一是多个执行线程共享一个资源。在并发应用程序中,多个线程读取或写入相同的数据结构或访问相同的文件或数据库连接是正常的。这些共享资源可能会引发错误情况或数据不一致,我们必须实现机制来避免这些错误。这些情况被称为竞态条件,它们发生在不同线程同时访问同一共享资源时。因此,最终结果取决于线程执行的顺序,大多数情况下,结果是错误的。你还会遇到变化可见性的问题。所以如果一个线程改变了共享变量的值,这些更改只会写入该线程的本地缓存;其他线程将无法访问这些更改(它们只能看到旧值)。

解决这些问题的方案在于临界区的概念。临界区是一段访问共享资源的代码块,不能同时被多个线程执行。

为了帮助程序员实现临界区,Java(以及几乎所有编程语言)提供了同步机制。当一个线程想要访问临界区时,它会使用这些同步机制之一来检查是否有其他线程正在执行临界区。如果没有,线程将进入临界区。如果有,线程将被同步机制挂起,直到当前执行临界区的线程结束。当多个线程都在等待一个线程完成临界区的执行时,JVM 会选择其中一个,其余的则等待轮到它们。本章将介绍一些食谱,教你如何使用 Java 语言提供的两种基本同步机制:

  • synchronized关键字

  • Lock接口及其实现

同步方法

在这个食谱中,你将学习如何使用 Java 中最基本的同步方法之一,即使用synchronized关键字来控制对方法或代码块的并发访问。所有synchronized语句(用于方法或代码块)都使用一个对象引用。只有一个线程可以执行由相同对象引用保护的同一个方法或代码块。

当您在方法中使用同步关键字时,对象引用是隐式的。当您在一个或多个对象的方法中使用同步关键字时,只有一个执行线程可以访问所有这些方法。如果另一个线程尝试访问同一对象声明为同步关键字的方法,它将被挂起,直到第一个线程完成方法的执行。换句话说,每个声明为同步关键字的方法都是一个临界区,Java 只允许同时执行一个对象的临界区。在这种情况下,使用的对象引用是own对象,由this关键字表示。静态方法有不同的行为。只有一个执行线程可以访问声明为同步关键字的静态方法,但不同的线程可以访问该类对象的其它非静态方法。您必须非常小心这一点,因为如果一个是静态的而另一个不是,两个线程可以访问两个不同的同步方法。如果两个方法更改相同的数据,您可能会遇到数据不一致错误。在这种情况下,使用的对象引用是类对象。

当您使用同步关键字保护代码块时,您必须传递一个对象引用作为参数。通常,您将使用this关键字来引用执行方法的对象,但您也可以使用其他对象引用。通常,这些对象将专门为此目的创建。您应该将用于同步的对象保持为私有。例如,如果您有一个由多个线程共享的类中的两个独立属性,您必须同步访问每个变量;然而,如果一个线程同时访问一个属性,而另一个线程访问另一个属性,这不会成为问题。请注意,如果您使用own对象(由this关键字表示),您可能会干扰其他同步代码(如前所述,this对象用于同步标记为同步关键字的方法)。

在本食谱中,您将学习如何使用同步关键字来实现一个模拟停车场的应用程序,该程序具有以下传感器:当汽车或摩托车进入或离开停车场时,一个用于存储停放车辆统计信息的对象,以及一个控制现金流量的机制。我们将实现两个版本:一个没有同步机制,我们将看到如何得到错误的结果,另一个版本正确工作,因为它使用了同步关键字的两个变体。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等不同的 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 首先,创建应用程序而不使用任何同步机制。创建一个名为 ParkingCash 的类,其中包含一个内部常量和用于存储通过提供此停车服务所赚取的总金额的属性:
          public class ParkingCash { 
          private static final int cost=2; 
          private long cash; 

          public ParkingCash() { 
            cash=0; 
          }

  1. 实现一个名为 vehiclePay() 的方法,当车辆(汽车或摩托车)离开停车场时将被调用。它将增加现金属性:
        public void vehiclePay() { 
          cash+=cost; 
        }

  1. 最后,实现一个名为 close() 的方法,将现金属性的值写入控制台并将其重新初始化为零:
          public void close() { 
            System.out.printf("Closing accounting"); 
            long totalAmmount; 
            totalAmmount=cash; 
            cash=0; 
            System.out.printf("The total amount is : %d",
                              totalAmmount); 
          } 
        }

  1. 创建一个名为 ParkingStats 的类,具有三个私有属性和构造函数,该构造函数将初始化它们:
          public class ParkingStats { 
          private long numberCars; 
          private long numberMotorcycles; 
          private ParkingCash cash; 

          public ParkingStats(ParkingCash cash) { 
            numberCars = 0; 
            numberMotorcycles = 0; 
              this.cash = cash; 
          }

  1. 然后,实现当汽车或摩托车进入或离开停车场时将执行的方法。当车辆离开停车场时,应增加现金:
        public void carComeIn() { 
          numberCars++; 
        } 

        public void carGoOut() { 
          numberCars--; 
          cash.vehiclePay(); 
        }

        public void motoComeIn() { 
          numberMotorcycles++; 
        } 

        public void motoGoOut() { 
          numberMotorcycles--; 
          cash.vehiclePay(); 
        }

  1. 最后,实现两个方法以分别获取停车场内的汽车和摩托车的数量。

  2. 创建一个名为 Sensor 的类,该类将模拟停车场内车辆的移动。它实现了 Runnable 接口,并有一个 ParkingStats 属性,该属性将在构造函数中初始化:

        public class Sensor implements Runnable { 

          private ParkingStats stats; 

          public Sensor(ParkingStats stats) { 
            this.stats = stats; 
          }

  1. 实现 run() 方法。在这个方法中,模拟两辆汽车和一辆摩托车进入并随后离开停车场。每个传感器将执行此操作 10 次:
        @Override 
        public void run() { 
          for (int i = 0; i< 10; i++) { 
            stats.carComeIn(); 
            stats.carComeIn(); 
            try { 
              TimeUnit.MILLISECONDS.sleep(50); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
            stats.motoComeIn(); 
            try { 
              TimeUnit.MILLISECONDS.sleep(50); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            }


            stats.motoGoOut(); 
            stats.carGoOut(); 
            stats.carGoOut(); 
          } 
        }

  1. 最后,实现主方法。创建一个名为 Main 的类,其中包含 main() 方法。它需要 ParkingCashParkingStats 对象来管理停车:
        public class Main { 

          public static void main(String[] args) { 

            ParkingCash cash = new ParkingCash(); 
            ParkingStats stats = new ParkingStats(cash); 

            System.out.printf("Parking Simulator\n");

  1. 然后,创建 Sensor 任务。使用 availableProcessors() 方法(该方法返回 JVM 可用的处理器数量,通常等于处理器的核心数)来计算我们的停车场将有多少个传感器。创建相应的 Thread 对象并将它们存储在数组中:
        intnumberSensors=2 * Runtime.getRuntime()
                                           .availableProcessors(); 
        Thread threads[]=new Thread[numberSensors]; 
        for (int i = 0; i<numberSensors; i++) { 
          Sensor sensor=new Sensor(stats); 
          Thread thread=new Thread(sensor); 
          thread.start(); 
          threads[i]=thread; 
        }

  1. 然后,使用 join() 方法等待线程的最终化:
        for (int i=0; i<numberSensors; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 最后,编写 Parking 的统计信息:
            System.out.printf("Number of cars: %d\n",
                              stats.getNumberCars()); 
            System.out.printf("Number of motorcycles: %d\n",
                               stats.getNumberMotorcycles()); 
            cash.close(); 
          } 
        }

在我们的案例中,我们在四核处理器上执行了示例,因此我们将有八个 Sensor 任务。每个任务执行 10 次迭代,在每次迭代中,三辆车进入停车场,同样的三辆车离开。因此,每个 Sensor 任务将模拟 30 辆车。

如果一切顺利,最终的统计信息将显示以下内容:

  • 停车场内没有车辆,这意味着所有进入停车场的车辆都已经离开

  • 执行了八个 Sensor 任务,其中每个任务模拟了 30 辆车,每辆车收费 2 美元;因此,总共赚取的现金金额为 480 美元

当你执行此示例时,每次都会获得不同的结果,其中大多数将是错误的。以下截图显示了示例:

图片

我们遇到了竞态条件,所有线程访问的不同共享变量给出了错误的结果。让我们使用 synchronized 关键字修改之前的代码以解决这些问题:

  1. 首先,将同步关键字添加到ParkingCash类的vehiclePay()方法中:
        public synchronized void vehiclePay() { 
          cash+=cost; 
        }

  1. 然后,将使用this关键字的同步代码块添加到close()方法中:
        public void close() { 
          System.out.printf("Closing accounting"); 
          long totalAmmount; 
          synchronized (this) { 
            totalAmmount=cash; 
            cash=0; 
          } 
          System.out.printf("The total amount is : %d",totalAmmount); 
        }

  1. 现在向ParkingStats类添加两个新的属性,并在类的构造函数中初始化它们:
        private final Object controlCars, controlMotorcycles; 
        public ParkingStats (ParkingCash cash) { 
          numberCars=0; 
          numberMotorcycles=0; 
          controlCars=new Object(); 
          controlMotorcycles=new Object(); 
          this.cash=cash; 
        }

  1. 最后,修改增加和减少汽车和摩托车数量的方法,包括使用同步关键字。numberCars属性将由controlCars对象保护,而numberMotorcycles属性将由controlMotorcycles对象保护。您还必须将getNumberCars()getNumberMotorcycles()方法与相关引用对象同步:
        public void carComeIn() { 
          synchronized (controlCars) { 
            numberCars++; 
          } 
        } 

        public void carGoOut() { 
          synchronized (controlCars) { 
            numberCars--; 
          } 
          cash.vehiclePay(); 
        } 


        public void motoComeIn() { 
          synchronized (controlMotorcycles) { 
            numberMotorcycles++; 
          } 
        } 

        public void motoGoOut() { 
          synchronized (controlMotorcycles) { 
            numberMotorcycles--; 
          } 
          cash.vehiclePay(); 
        }

  1. 现在执行示例,并与之前的版本进行比较,看看差异。

它是如何工作的...

以下截图显示了示例的新版本输出。无论您执行多少次,您都将始终获得正确的结果:

图片

让我们看看示例中同步关键字的用法:

  • 首先,我们保护了vehiclePay()方法。如果有两个或更多Sensor任务同时调用此方法,只有一个会执行它,其余的将等待它们的轮次;因此,最终金额总是正确的。

  • 我们使用了两个不同的对象来控制对汽车和摩托车计数器的访问。这样,一个Sensor任务可以修改numberCars属性,另一个Sensor任务可以修改numberMotorcycles属性,但同时不会有两个Sensor任务能够同时修改相同的属性,所以计数器的最终值总是正确的。

最后,我们还同步了getNumberCars()getNumberMotorcycles()方法。使用同步关键字,我们可以在并发应用程序中保证对共享数据的正确访问。

如本食谱介绍中所述,只有一个线程可以访问使用同步关键字声明的方法的对象。如果线程(A)正在执行同步方法,而线程(B)想要执行同一对象的另一个同步方法,它将被阻塞,直到线程(A)完成。但如果线程(B)可以访问同一类的不同对象,则它们都不会被阻塞。

当您使用同步关键字来保护代码块时,您使用一个对象作为参数。JVM 保证只有一个线程可以访问由该对象保护的代码块(注意我们总是谈论对象,而不是类)。

我们还使用了TimeUnit类。TimeUnit类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS。这些表示我们传递给 sleep 方法的单位时间。在我们的例子中,我们让线程休眠 50 毫秒。

还有更多...

同步关键字会惩罚应用程序的性能,因此你只能在并发环境中修改共享数据的方法上使用它。如果有多个线程调用同步方法,则一次只有一个线程执行它们,而其他线程将保持等待。如果操作没有使用同步关键字,所有线程都可以同时执行该操作,从而减少总执行时间。如果你知道一个方法不会被多个线程调用,不要使用同步关键字。无论如何,如果类是为多线程访问设计的,它应该始终是正确的。你必须将正确性置于性能之上。此外,你应该在方法和类中包含有关它们线程安全的文档。

你可以使用递归调用与同步方法。由于线程可以访问对象的同步方法,你可以调用该对象的其他同步方法,包括正在执行的方法。它不需要再次获取对同步方法的访问。

我们可以使用同步关键字来保护代码块(而不是整个方法)的访问。我们应该这样使用同步关键字来保护对共享数据的访问,将其他操作排除在这个代码块之外,从而获得更好的应用程序性能。目标是使关键部分(一次只能由一个线程访问的代码块)尽可能短。此外,避免在关键部分内部调用阻塞操作(例如,I/O 操作)。我们已经使用同步关键字来保护更新建筑物中人数的指令的访问,排除了该代码块中不使用共享数据的长时间操作。当你以这种方式使用同步关键字时,你必须传递一个对象引用作为参数。只有一个线程可以访问该对象的同步代码(代码块或方法)。通常,我们将使用this关键字来引用执行方法的对象:

    synchronized (this) { 
      // Java code 
    }

参见

  • 本章中的在同步代码中使用条件配方

在同步代码中使用条件

并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个生产者将数据保存到缓冲区中,一个或多个消费者从缓冲区中获取数据。

作为缓冲区是一个共享的数据结构,我们必须使用同步机制,例如同步关键字来控制对其的访问,但在这里我们有更多的限制。如果缓冲区已满,生产者无法在其中保存数据,如果缓冲区为空,消费者无法从中获取数据。

对于这些类型的情况,Java 提供了在 Object 类中实现的 wait()notify()notifyAll() 方法。一个线程可以在代码的 synchronized 块中调用 wait() 方法。如果它在 synchronized 块之外调用 wait() 方法,JVM 将抛出 IllegalMonitorStateException 异常。当线程调用 wait() 方法时,JVM 将线程置于睡眠状态,并释放控制其正在执行的 synchronized 块的对象,允许其他线程执行由该对象保护的另一个 synchronized 代码块。要唤醒线程,必须在由同一对象保护的代码块中调用 notify()notifyAll() 方法。

在本食谱中,你将学习如何使用 synchronized 关键字和 wait()notify()notifyAll() 方法实现生产者-消费者问题。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 EventStorage 的类。它有两个属性,即一个名为 maxSizeint 属性和一个名为 storageList<Date> 属性:
         public class EventStorage { 

          private int maxSize; 
          private Queue<Date> storage;

  1. 实现类的构造函数,初始化类的属性:
        public EventStorage(){ 
          maxSize=10; 
          storage=new LinkedList<>(); 
        }

  1. 实现一个 synchronized 方法 set() 以将事件存储在 storage 中。首先检查存储是否已满。如果已满,它将调用 wait() 方法直到有空闲空间。在方法结束时,我们调用 notify() 方法唤醒所有在 wait() 方法中睡眠的线程。在这种情况下,我们将忽略 InterruptedException。在实际实现中,你必须考虑对这些异常的处理。你可以重新抛出或将它们转换为应用程序的另一种类型的异常:
        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()); 
          notify(); 
        }

  1. 实现一个 synchronized 方法 get() 以获取用于存储的事件。首先检查存储是否有事件。如果没有事件,它将调用 wait() 方法直到有事件。在方法结束时,我们调用 notifyAll() 方法唤醒所有在 wait() 方法中睡眠的线程。在这种情况下,我们将忽略 InterruptedException。在实际实现中,你必须考虑对这些异常的处理。你可以重新抛出或将它们转换为应用程序的另一种类型的异常:
        public synchronized void get(){ 
          while (storage.size()==0){ 
            try { 
              wait(); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
          String element=storage.poll().toString(); 
          System.out.printf("Get: %d: %s\n",storage.size(),element); 
          notify(); 

        }

  1. 创建一个名为 Producer 的类并指定它实现 Runnable 接口。它将实现示例中的生产者:
        public class Producer implements Runnable {

  1. 声明一个 EventStore 对象并实现类的构造函数以初始化此对象:
        private EventStorage storage; 

        public Producer(EventStorage storage){ 
          this.storage=storage; 
        }

  1. 实现一个 run() 方法,该方法调用 EventStorage 对象的 set() 方法 100 次:
        @Override 
        public void run() { 
          for (int i=0; i<100; i++){ 
            storage.set(); 
          } 
        }

  1. 创建一个名为 Consumer 的类并指定它实现 Runnable 接口。它将实现示例中的消费者:
        public class Consumer implements Runnable {

  1. 声明一个EventStorage对象并实现该类的构造函数以初始化此对象:
        private EventStorage storage; 

        public Consumer(EventStorage storage){ 
          this.storage=storage; 
        }

  1. 实现一个run()方法。它调用EventStorage对象的get()方法 100 次:
        @Override 
        public void run() { 
          for (int i=0; i<100; i++){ 
            storage.get(); 
          } 
        }

  1. 通过实现一个名为Main的类并添加main()方法来创建示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建一个EventStorage对象:
        EventStorage storage=new EventStorage();

  1. 创建一个Producer对象和一个Thread来运行它:
        Producer producer=new Producer(storage); 
        Thread thread1=new Thread(producer);

  1. 创建一个Consumer对象和一个Thread来运行它:
        Consumer consumer=new Consumer(storage); 
        Thread thread2=new Thread(consumer);

  1. 启动两个线程:
        thread2.start(); 
        thread1.start();

它是如何工作的...

此示例的关键是EventStorage类的set()get()方法。首先,set()方法检查存储属性中是否有空闲空间。如果已满,它调用wait()方法等待空闲空间。当其他线程调用notify()方法时,此线程会醒来并再次检查条件。notify()方法并不保证条件得到满足。这个过程会一直重复,直到存储空间中有空闲空间,可以生成新事件并将其存储。

get()方法的行为类似。首先,它检查存储属性中是否有事件。如果EventStorage类为空,它调用wait()方法等待事件。当其他线程调用notify()方法时,此线程会醒来并再次检查条件,直到存储中有一些事件。

你必须在一个while循环中不断检查条件并调用wait()方法。你将无法继续,直到条件为true

如果你运行此示例,你会发现尽管生产者和消费者正在设置和获取事件,但存储空间永远不会包含超过 10 个事件。

更多内容...

synchronized关键字还有其他重要的用途。请参阅此配方中的参见部分,其中解释了该关键字的使用。

参见

  • 本章中的同步方法配方

使用锁同步代码块

Java 提供了另一种同步代码块的方法。它比synchronized关键字更强大、更灵活。它基于Lockjava.util.concurrent.locks包中的接口)及其实现类(如ReentrantLock)。这种机制具有一些优点,如下所述:

  • 它允许你以更灵活的方式组织同步块。使用synchronized关键字,你只能以结构化的方式控制代码中的同步块。然而,Lock接口允许你实现更复杂的结构以实现你的临界区。

  • Lock 接口提供了比 synchronized 关键字更多的功能。其中之一是通过 tryLock() 方法实现的。此方法尝试获取锁的控制权,如果无法获取,因为另一个线程正在使用它,则返回 false。使用 synchronized 关键字时,如果线程(A)在另一个线程(B)正在执行同步代码块时尝试执行同步代码块,则线程(A)将被挂起,直到线程(B)完成同步代码块的执行。使用锁,您可以执行 tryLock() 方法。此方法返回一个 Boolean 值,指示是否有另一个线程正在运行由该锁保护的代码。

  • ReadWriteLock 接口允许在多个读取者和仅有一个修改者之间分离读取和写入操作。

  • Lock 接口提供的性能优于 synchronized 关键字。

ReentrantLock 类的构造函数接受一个名为 fairboolean 参数;此参数允许您控制其行为。false 值是默认值,称为 非公平模式。在此模式下,如果有线程正在等待锁,并且锁必须从这些线程中选择一个来获取对临界区的访问权限,它会随机选择其中的任何一个。true 值称为 公平模式。在此模式下,如果有线程正在等待锁,并且锁必须选择一个来获取对临界区的访问权限,它会选择等待时间最长的线程。请注意,之前解释的行为仅在 lock()unlock() 方法中使用。由于 tryLock() 方法不会使线程休眠,如果使用 Lock 接口,则公平属性不会影响其功能。

在本食谱中,您将学习如何使用锁来同步代码块,并使用 Lock 接口及其实现类 ReentrantLock 创建临界区,实现一个模拟打印队列的程序。您还将了解公平参数如何影响 Lock 的行为。

准备工作

本食谱中的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 PrintQueue 的类,该类将实现打印队列:
         public class PrintQueue {

  1. 在构造函数中声明一个 Lock 对象,并用 ReentrantLock 类的新对象初始化它。构造函数将接收一个 Boolean 参数,我们将使用它来指定 Lock 的公平模式:
        private Lock queueLock; 
        public PrintQueue(booleanfairMode) { 
          queueLock = new ReentrantLock(fairMode); 
        }

  1. 实现 printJob() 方法。它将接收 Object 作为参数,并且不会返回任何值:
        public void printJob(Object document){

  1. printJob() 方法内部,通过调用 lock() 方法来获取 Lock 对象的控制权:
        queueLock.lock();

  1. 然后,包含以下代码以模拟打印文档的过程:
        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(); 
        }

  1. 最后,使用 unlock() 方法释放 Lock 对象的控制权:
        finally { 
          queueLock.unlock(); 
        }

  1. 然后,重复相同的流程。printJob()方法将帮助您获取锁并释放两次。这种奇怪的行为将使我们能够更好地看到公平模式和非公平模式之间的差异。我们将这段代码包含在printJob()方法中:
          queueLock.lock(); 
          try { 
            Long duration = (long) (Math.random() * 10000); 
            System.out.printf("%s: PrintQueue: Printing a Job during
                               %d seconds\n", Thread.currentThread()
                              .getName(),(duration / 1000)); 
            Thread.sleep(duration); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } finally { 
          queueLock.unlock(); 
        }

  1. 创建一个名为Job的类并指定它实现Runnable接口:
        public class Job implements Runnable {

  1. 声明一个PrintQueue类的对象并实现该类的构造函数以初始化此对象:
        private PrintQueue printQueue; 

        public Job(PrintQueue printQueue){ 
          this.printQueue=printQueue; 
        }

  1. 实现该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()); 
        }

  1. 通过实现一个名为Main的类并添加main()方法来创建应用程序的主类:
        public class Main { 

          public static void main (String args[]){

  1. 我们将使用具有公平模式的锁来测试PrintQueue类,并返回truefalse。我们将使用一个辅助方法来实现这两个测试,以便main()方法的代码简单:
          System.out.printf("Running example with fair-mode =
                             false\n"); 
          testPrintQueue(false); 
          System.out.printf("Running example with fair-mode = true\n"); 
          testPrintQueue(true); 
        }

  1. 创建testPrintQueue()方法并在其中创建一个共享的PrintQueue对象:
        private static void testPrintQueue(Boolean fairMode) { 
          PrintQueue printQueue=new PrintQueue(fairMode);

  1. 创建 10 个Job对象和 10 个线程来运行它们:
        Thread thread[]=new Thread[10]; 
        for (int i=0; i<10; i++){ 
          thread[i]=new Thread(new Job(printQueue),"Thread "+ i); 
        }

  1. 启动 10 个线程:
        for (int i=0; i<10; i++){ 
          thread[i].start(); 
        }

  1. 最后,等待 10 个线程的最终化:
        for (int i=0; i<10; i++) { 
          try { 
            thread[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

它是如何工作的...

在下面的屏幕截图中,您可以看到此示例一次执行的部分输出:

图片

示例的关键在于PrintQueue类的printJob()方法。当我们想要使用锁实现关键部分并保证只有一个执行线程运行代码块时,我们必须创建一个ReentrantLock对象。在关键部分的开始,我们必须使用lock()方法获取锁的控制权。当线程(A)调用此方法时,如果没有其他线程控制锁,它将立即将锁的控制权交给线程(A)并返回,允许线程执行关键部分。否则,如果有另一个,比如说线程(B),正在执行由该锁控制的关键部分,lock()方法将使线程(A)休眠,直到线程(B)完成关键部分的执行。

在关键部分结束时,我们必须使用unlock()方法释放锁的控制权,允许其他线程运行关键部分。如果在关键部分结束时没有调用unlock()方法,等待该块的线程将永远等待,导致死锁情况。如果在关键部分中使用 try-catch 块,别忘了将包含unlock()方法的句子放在finally部分中。

在此示例中,我们还测试了另一个主题,即公平模式。在每次任务中,我们都有两个关键部分。在前面的屏幕截图中,您可以看到所有任务在第一个任务之后立即执行第二个部分。这是通常的情况,但也有例外。这发生在我们具有非公平模式时,也就是说,我们将一个假值传递给ReentrantLock类的构造函数。

相反,当我们通过将 true 值传递给 Lock 类的构造函数来建立公平模式时,行为会有所不同。第一个请求控制锁的线程是 Thread 0,然后是 Thread 1,依此类推。当 Thread 0 正在运行由锁保护的第一个代码块时,我们有九个线程正在等待执行相同的代码块。当 Thread 0 释放锁后,它立即再次请求锁,因此我们有 10 个线程试图获取锁。由于启用了公平模式,Lock 接口将选择 Thread 1,因为它等待锁的时间更长。然后,它选择 Thread 2,然后是 Thread 3,依此类推。直到所有线程都通过了锁保护的第一个代码块,它们中的任何一个都不会执行锁保护的第二个代码块。一旦所有线程都执行了锁保护的第一个代码块,那么轮到 Thread 0 再次,然后是 Thread 1,依此类推。以下截图显示了差异:

还有更多...

Lock 接口(以及 ReentrantLock 类)还包括另一个获取锁控制的方法。它是 tryLock() 方法。与 lock() 方法最大的不同是,如果使用此方法的线程无法获取锁控制,它将立即返回,并且不会使线程休眠。如果线程获取了锁,它返回 booleantrue;如果没有获取,则返回 false。你也可以传递一个时间值和一个 TimeUnit 对象来指示线程等待获取锁的最大时间。如果时间流逝而线程没有获取到锁,该方法将返回 false 值。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS;这些表示我们传递给方法的时间单位。

请注意,程序员有责任考虑此方法的结果并相应地采取行动。如果方法返回 false,则显然你的程序无法执行临界区。如果它返回 true,你可能在应用程序中得到错误的结果。

ReentrantLock 类还允许使用递归调用。当一个线程控制了锁并进行了递归调用时,它将继续保持对锁的控制,因此对 lock() 方法的调用将立即返回,线程将继续执行递归调用。此外,我们还可以调用其他方法。你应该在代码中调用 unlock() 方法与调用 lock() 方法的次数相同。

避免死锁

你必须非常小心地使用锁,以避免死锁。这种情况发生在两个或更多线程在等待永远不会解锁的锁时被阻塞。例如,线程(A)锁定锁(X),而线程(B)锁定锁(Y)。现在,如果线程(A)试图锁定锁(Y),而线程(B)同时试图锁定锁(X),两个线程都将无限期地被阻塞,因为他们正在等待永远不会被释放的锁。请注意,问题发生是因为两个线程试图以相反的顺序获取锁。附录并发编程设计提供了一些很好的建议,以充分设计并发应用程序并避免这些死锁问题。

参见

  • 本章中的同步方法在锁中使用多个条件配方

  • 第九章中的监控锁接口配方,测试并发应用程序

使用读写锁同步数据访问

锁提供的最显著的改进之一是ReadWriteLock接口和ReentrantReadWriteLock类,这是唯一实现该接口的类。这个类有两个锁:一个用于读操作,一个用于写操作。可以有多个线程同时使用读操作,但只有一个线程可以使用写操作。如果一个线程正在进行写操作,其他线程就不能写或读。

在这个配方中,你将通过实现一个使用它来控制访问存储两个产品价格的对象的程序,来学习如何使用ReadWriteLock接口。

准备工作...

你应该阅读使用锁同步代码块的配方,以更好地理解这个配方。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为PricesInfo的类,用于存储两个产品价格的信息:
        public class PricesInfo {

  1. 声明两个名为price1price2double属性:
        private double price1; 
        private double price2;

  1. 声明一个名为lockReadWriteLock对象:
        private ReadWriteLock lock;

  1. 实现类的构造函数,初始化三个属性。对于lock属性,创建一个新的ReentrantReadWriteLock对象:
        public PricesInfo(){ 
          price1=1.0; 
          price2=2.0; 
          lock=new ReentrantReadWriteLock(); 
        }

  1. 实现返回price1属性值的getPrice1()方法。它使用读锁来控制对这个属性值的访问:
        public double getPrice1() { 
          lock.readLock().lock(); 
          double value=price1; 
          lock.readLock().unlock(); 
          return value; 
        }

  1. 实现返回price2属性值的getPrice2()方法。它使用读锁来控制对这个属性值的访问:
        public double getPrice2() { 
          lock.readLock().lock(); 
          double value=price2; 
          lock.readLock().unlock(); 
          return value; 
        }

  1. 实现设置两个属性值的setPrices()方法。它使用写锁来控制对它们的访问。我们将使线程休眠 5 秒钟。这表明即使它有写锁,也没有其他线程获得读锁:
        public void setPrices(double price1, double price2) { 
          lock.writeLock().lock(); 
          System.out.printf("%s: PricesInfo: Write Lock Adquired.\n",
                            new Date()); 
          try { 
            TimeUnit.SECONDS.sleep(10); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
          this.price1=price1; 
          this.price2=price2; 
          System.out.printf("%s: PricesInfo: Write Lock Released.\n",
                            new Date()); 
          lock.writeLock().unlock(); 
        }

  1. 创建一个名为Reader的类,并指定它实现Runnable接口。这个类实现了PricesInfo类属性的读取器:
        public class Reader implements Runnable {

  1. 声明一个 PricesInfo 对象并实现该类的构造函数,以便初始化此对象:
        private PricesInfo pricesInfo; 

        public Reader (PricesInfo pricesInfo){ 
          this.pricesInfo=pricesInfo; 
        }

  1. 为此类实现 run() 方法。它读取两个价格值 10 次:
        @Override 
        public void run() { 
          for (int i=0; i<20; i++){ 
            System.out.printf("%s: %s: Price 1: %f\n",new Date(),
                              Thread.currentThread().getName(),
                              pricesInfo.getPrice1()); 
            System.out.printf("%s: %s: Price 2: %f\n",new Date(),
                              Thread.currentThread().getName(),
                              pricesInfo.getPrice2()); 
          } 
        }

  1. 创建一个名为 Writer 的类并指定它实现 Runnable 接口。此类实现了 PricesInfo 类属性的值修改器:
        public class Writer implements Runnable {

  1. 声明一个 PricesInfo 对象并实现该类的构造函数,以便初始化此对象:
        private PricesInfo pricesInfo; 

        public Writer(PricesInfo pricesInfo){ 
          this.pricesInfo=pricesInfo; 
        }

  1. 实现 run() 方法。该方法在两次修改之间暂停 2 秒,修改两个价格值三次:
        @Override 
        public void run() { 
          for (int i=0; i<3; i++) { 
            System.out.printf("%s: Writer: Attempt to modify the
                              prices.\n", new Date()); 
            pricesInfo.setPrices(Math.random()*10, Math.random()*8); 
            System.out.printf("%s: Writer: Prices have been
                              modified.\n", new Date()); 
            try { 
              Thread.sleep(2); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个 PricesInfo 对象:
        PricesInfo pricesInfo=new PricesInfo();

  1. 创建五个 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]); 
        }

  1. 创建一个 Writer 对象和 Thread 来执行它:
        Writer writer=new Writer(pricesInfo); 
        Thread  threadWriter=new Thread(writer);

  1. 开始线程:
        for (int i=0; i<5; i++){ 
          threadsReader[i].start(); 
        } 
        threadWriter.start();

它是如何工作的...

在下面的屏幕截图,你可以看到此示例一次执行的部分输出:

图片

当写者获取写锁时,没有任何读任务可以读取数据。你可以在 Write Lock Acquired 消息之后看到一些读任务的消息,但它们是之前执行且尚未在控制台显示的指令。一旦写任务释放了锁,读任务再次获得对价格信息的访问权限并显示新的价格。

如前所述,ReentrantReadWriteLock 类有两个锁:一个用于读操作,一个用于写操作。用于读操作的锁是通过在 ReadWriteLock 接口中声明的 readLock() 方法获得的。这个锁是一个实现了 Lock 接口的对象,因此我们可以使用 lock()unlock()tryLock() 方法。用于写操作的锁是通过在 ReadWriteLock 接口中声明的 writeLock() 方法获得的。这个锁也是一个实现了 Lock 接口的对象,因此我们可以使用 lock()unlock()tryLock() 方法。确保正确使用这些锁,使用它们来完成它们被设计的目的。当你获得 Lock 接口的读锁时,你不能修改变量的值。否则,你可能会遇到与不一致相关的数据错误。

参见

  • 本章中 使用锁同步代码块 的配方

  • 在第九章 测试并发应用程序 中,监控 Lock 接口 的配方

在锁中使用多个条件

锁可以与一个或多个条件相关联。这些条件在 Condition 接口中声明。这些条件的目的允许线程控制锁并检查条件是否为 truefalse。如果是 false,则线程将被挂起,直到另一个线程唤醒它。Condition 接口提供了挂起线程和唤醒挂起线程的机制。

并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个生产者将数据保存到缓冲区中,以及一个或多个消费者从缓冲区中获取数据,如本章前面所述。

在这个菜谱中,你将学习如何使用锁和条件实现生产者-消费者问题。

准备工作

你应该阅读 使用锁同步代码块 的菜谱,以更好地理解这个菜谱。

如何做...

按照以下步骤实现示例:

  1. 首先,实现一个将模拟文本文件的类。创建一个名为 FileMock 的类,具有两个属性:一个名为 contentString 数组和一个名为 indexint。它们将存储文件的内容和将要检索的模拟文件的行:
        public class FileMock { 

          private String[] content; 
          private int index;

  1. 实现类的构造函数,该函数初始化文件内容为随机字符:
        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 randomCharacter= (int)Math.random()*255; 
              buffer.append((char)randomCharacter); 
            } 
            content[i] = buffer.toString(); 
          } 
          index=0; 
        }

  1. 实现返回 true 如果文件还有更多行要处理或返回 false 如果你已经到达模拟文件的末尾的 hasMoreLines() 方法:
        public boolean hasMoreLines(){ 
          return index <content.length; 
        }

  1. 实现返回由索引属性确定的行并增加其值的 getLine() 方法:
        public String getLine(){ 
          if (this.hasMoreLines()) { 
            System.out.println("Mock: " + (content.length-index)); 
            return content[index++]; 
          } 
          return null; 
        }

  1. 现在实现一个名为 Buffer 的类,它将实现生产者和消费者共享的缓冲区:
        public class Buffer {

  1. 这个类有六个属性:
  • 一个名为 bufferLinkedList<String> 属性,它将存储共享数据。例如:
                  private final LinkedList<String> buffer;

  • 一个名为 maxSizeint 类型,它将存储缓冲区的长度。例如:
                    private final int maxSize;

  • 一个名为 lockReentrantLock 对象,它将控制对修改缓冲区的代码块的访问。例如:
                    private final ReentrantLock lock;

  • 两个名为 linesspaceCondition 属性。例如:
                  private final Condition lines;

                  private final Condition space;

  • 一个名为 pendingLinesboolean 类型,它将指示缓冲区中是否有行。例如:
                  private boolean pendingLines;

  1. 实现类的构造函数。它初始化之前描述的所有属性:
        public Buffer(int maxSize) { 
          this.maxSize = maxSize; 
          buffer = new LinkedList<>(); 
          lock = new ReentrantLock(); 
          lines = lock.newCondition(); 
          space = lock.newCondition(); 
          pendingLines =true; 
        }

  1. 实现一个insert()方法。它接收一个String类型的参数,并尝试将其存储在缓冲区中。首先,它获取锁的控制权。当它拥有这个锁时,它会检查缓冲区中是否有空余空间。如果缓冲区已满,它会在space条件中调用await()方法等待空闲空间。当另一个线程在space条件中调用signal()signalAll()方法时,线程将被唤醒。当发生这种情况时,线程将行存储在缓冲区中,并在lines条件上调用signallAll()方法。正如我们马上会看到的,这个条件将唤醒所有等待缓冲区中行的线程。为了使代码更简单,我们忽略了InterruptedException异常。在实际情况下,你可能必须处理它:
        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(); 
          } 
        }

  1. 实现一个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; 
        }

  1. 实现一个setPendingLines()方法,用于设置pendingLines属性的值。当生产者没有更多行要生产时,将调用它:
        public synchronized void setPendingLines(boolean pendingLines) { 
          this.pendingLines = pendingLines; 
        }

  1. 实现一个hasPendingLines()方法。如果有更多行要处理,则返回true,否则返回false
        public synchronized boolean hasPendingLines() { 
          return pendingLines || buffer.size()>0; 
        }

  1. 现在轮到生产者了。实现一个名为Producer的类,并指定它实现Runnable接口:
        public class Producer implements Runnable {

  1. 声明两个属性,即FileMock类的一个对象和Buffer类的一个对象:
        private FileMock mock; 

        private Buffer buffer;

  1. 实现类的构造函数,初始化两个属性:
        public Producer (FileMock mock, Buffer buffer){ 
          this.mock = mock; 
          this.buffer = buffer; 
        }

  1. 实现一个run()方法,该方法读取FileMock对象中创建的所有行,并使用insert()方法将它们存储在缓冲区中。一旦完成,使用setPendingLines()方法通知缓冲区它将不再生成更多行:
        @Override 
        public void run() { 
          buffer.setPendingLines(true); 
          while (mock.hasMoreLines()){ 
            String line = mock.getLine(); 
            buffer.insert(line); 
          } 
          buffer.setPendingLines(false); 
        }

  1. 接下来是消费者的轮次。实现一个名为Consumer的类,并指定它实现Runnable接口:
        public class Consumer implements Runnable {

  1. 声明一个Buffer对象并实现类的构造函数,初始化它:
        private Buffer buffer; 

        public Consumer (Buffer buffer) { 
          this.buffer = buffer; 
        }

  1. 实现一个run()方法。当缓冲区有挂起的行时,它尝试获取一行并处理它:
        @Override   
        public void run() { 
          while (buffer.hasPendingLines()) { 
            String line = buffer.get(); 
            processLine(line); 
          } 
        }

  1. 实现一个辅助方法processLine()。它只睡眠 10 毫秒来模拟对行的某种处理:
        private void processLine(String line) { 
          try { 
            Random random = new Random(); 
            Thread.sleep(random.nextInt(100)); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建一个FileMock对象:
        FileMock mock = new FileMock(100, 10);

  1. 创建一个Buffer对象:
        Buffer buffer = new Buffer(20);

  1. 创建一个Producer对象和Thread来运行它:
        Producer producer = new Producer(mock, buffer); 
        Thread producerThread = new Thread(producer,"Producer");

  1. 创建三个Consumer对象和三个线程来运行它们:
        Consumer consumers[] = new Consumer[3]; 
        Thread consumersThreads[] = new Thread[3]; 

        for (int i=0; i<3; i++){ 
          consumers[i] = new Consumer(buffer); 
          consumersThreads[i] = new Thread(consumers[i],"Consumer "+i); 
        }

  1. 启动生产者和三个消费者:
        producerThread.start(); 
        for (int i = 0; i< 3; i++){ 
          consumersThreads[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 类是一个包含以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

  • awaitUninterruptibly(): 线程将休眠直到另一个线程调用 signal()signalAll() 方法,这不能被中断

  • awaitUntil(Date date): 线程将休眠直到:

    • 被中断了

    • 另一个线程在条件中调用 signal()signalAll() 方法

    • 指定的日期到达

你可以使用读写锁的 ReadLockWriteLock 锁与条件一起使用。

参见

  • 本章中关于 使用锁同步代码块使用读写锁同步数据访问 的配方

使用 StampedLock 类的高级锁定

StampedLock 类提供了一种特殊的锁,这种锁与 LockReadWriteLock 接口提供的锁不同。实际上,这个类没有实现这些接口,但它提供的功能非常相似。

关于这类锁的第一个要注意的点是其主要目的是作为一个辅助类来实现线程安全的组件,因此其使用在正常应用中不会非常常见。

StampedLock 锁的最重要特性如下:

  • 您可以通过三种不同的模式来获取锁的控制权:

    • 写入:在此模式下,您对锁有独占访问权。没有其他线程可以在这种模式下控制锁。

    • 读取:在此模式下,您对锁有非独占访问权。可以有其他线程在此模式或乐观读取模式下访问锁。

    • 乐观读取:在这里,线程不控制块。其他线程可以在写入模式下获取锁的控制权。当您在乐观读取模式下获取锁并想要访问由它保护的共享数据时,您将必须使用 validate() 方法检查您是否可以访问它们。

  • StampedLock 类提供了以下方法:

    • 在前述模式之一中获取锁的控制权。如果方法(readLock()writeLock()readLockInterruptibly())无法获取锁的控制权,当前线程将暂停,直到获取到锁。

    • 在前述模式之一中获取锁的控制权。如果方法(tryOptimisticRead()tryReadLock()tryWriteLock())无法获取锁的控制权,它们将返回一个特殊值来指示这种情况。

    • 如果可能,将一种模式转换为另一种模式。如果不可以,方法(asReadLock()asWriteLock()asReadWriteLock())将返回一个特殊值。

    • 释放锁。

  • 所有这些方法都返回一个名为 stamp 的长整型值,我们需要使用它来与锁一起工作。如果一个方法返回零,这意味着它尝试获取锁,但无法获取。

  • StampedLock 锁不是一个可重入锁,例如 LockReadWriteLock 接口。如果您调用尝试再次获取锁的方法,它可能会被阻塞,您将遇到死锁。

  • 它没有所有权的概念。它们可以被一个线程获取,然后由另一个线程释放。

  • 最后,它对下一个将获取锁控制的线程没有任何策略。

在本配方中,我们将学习如何使用 StampedLock 类的不同模式来保护对共享数据对象的访问。我们将使用一个共享对象在三个并发任务之间进行测试,以使用 StampedLock(写入、读取和乐观读取)测试三种访问模式。

准备就绪

本示例的配方已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 首先,实现共享数据对象。创建一个名为 Position 的类,具有两个整型属性,即 xy。您必须包括获取和设置属性值的方法。其代码非常简单,因此在此处未包含。

  2. 现在,让我们实现 Writer 任务。它实现了 Runnable 接口,并将有两个属性:一个名为 positionPosition 对象和一个名为 lockStampedLock。它们将在构造函数中初始化:

         public class Writer implements Runnable { 

          private final Position position; 
          private final StampedLock lock; 

          public Writer (Position position, StampedLock lock) { 
            this.position=position; 
            this.lock=lock; 
          }

  1. 实现run()方法。在一个我们将重复 10 次的循环中,以写入模式获取锁,更改位置对象的两个属性值,暂停线程执行一秒,释放锁(在try...catch...finally结构的finally部分释放锁,以确保在任何情况下都能释放锁),然后暂停线程一秒:
        @Override 
        public void run() { 

          for (int i=0; i<10; i++) { 
            long stamp = lock.writeLock(); 

            try { 
              System.out.printf("Writer: Lock acquired %d\n",stamp); 
              position.setX(position.getX()+1); 
              position.setY(position.getY()+1); 
              TimeUnit.SECONDS.sleep(1); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } finally { 
              lock.unlockWrite(stamp); 
              System.out.printf("Writer: Lock released %d\n",stamp); 
            } 

            try { 
              TimeUnit.SECONDS.sleep(1); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 

        }

  1. 然后,实现Reader任务以读取共享对象的价值。创建一个名为Reader的类,该类实现Runnable接口。它将有两个属性:一个名为positionPosition对象和一个名为lockStampedLock对象。它们将在类的构造函数中初始化:
        public class Reader implements Runnable { 

          private final Position position; 
          private final StampedLock lock; 

          public Reader (Position position, StampedLock lock) { 
            this.position=position; 
            this.lock=lock; 
          }

  1. 现在实现run()方法。在一个我们将重复50次的循环中,以读取模式获取锁的控制权,将位置对象的值写入控制台,并暂停线程200毫秒。最后,使用try...catch...finally结构的finally块释放锁:
        @Override 
        public void run() { 
          for (int i=0; i<50; i++) { 
            long stamp=lock.readLock(); 
            try { 
              System.out.printf("Reader: %d - (%d,%d)\n", stamp,
                                position.getX(), position.getY()); 
              TimeUnit.MILLISECONDS.sleep(200); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } finally { 
              lock.unlockRead(stamp); 
              System.out.printf("Reader: %d - Lock released\n", stamp); 
            } 
          } 
        }

  1. 然后,实现OptimisticReader任务。OptimisticReader类实现Runnable接口。它将有两个属性:一个名为positionPosition对象和一个名为lockStampedLock对象。它们将在类的构造函数中初始化:
        public class OptimisticReader implements Runnable { 

          private final Position position; 
          private final StampedLock lock; 

          public OptimisticReader (Position position, StampedLock lock) { 
            this.position=position; 
            this.lock=lock; 
          }

  1. 现在实现run()方法。首先使用tryOptimisticRead()方法以乐观读取模式获取锁的戳记。然后,重复循环100次。在循环中,使用validate()方法验证是否可以访问数据。如果此方法返回true,则在控制台写入位置对象的值。否则,在控制台写入一条消息,并再次使用tryOptimisticRead()方法获取另一个戳记。然后,暂停线程200毫秒:
        @Override 
        public void run() { 
          long stamp; 
          for (int i=0; i<100; i++) { 
            try { 
              stamp=lock.tryOptimisticRead(); 
              int x = position.getX(); 
              int y = position.getY(); 
              if (lock.validate(stamp)) { 
                System.out.printf("OptmisticReader: %d - (%d,%d)\n",
                                  stamp,x, y); 
              } else { 
                System.out.printf("OptmisticReader: %d - Not Free\n",
                                  stamp); 
              } 
              TimeUnit.MILLISECONDS.sleep(200); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 最后,实现带有main()方法的Main类。创建一个PositionStampedLock对象,创建三个线程(每个任务一个)启动线程,并等待它们的最终化:
        public class Main { 

          public static void main(String[] args) { 

            Position position=new Position(); 
            StampedLock lock=new StampedLock(); 

            Thread threadWriter=new Thread(new Writer(position,lock)); 
            Thread threadReader=new Thread(new Reader(position, lock)); 
            Thread threadOptReader=new Thread(new OptimisticReader
                                               (position, lock)); 

            threadWriter.start(); 
            threadReader.start(); 
            threadOptReader.start(); 

            try { 
              threadWriter.join(); 
              threadReader.join(); 
              threadOptReader.join(); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

它是如何工作的...

在这个例子中,我们测试了你可以与带戳记锁一起使用的三种模式。在Writer任务中,我们使用writeLock()方法(以写入模式获取锁)获取锁。在Reader任务中,我们使用readLock()方法(以读取模式获取锁)获取锁。最后,在OptimisticRead任务中,我们首先使用tryOptimisticRead(),然后使用validate()方法检查我们是否可以访问数据。

前两个方法,如果它们可以获取锁的控制权,将等待直到获取锁。tryOptimisticRead()方法总是返回一个值。如果我们无法使用锁,它将是0;如果我们可以使用它,它将是一个不同于0的值。记住,在这种情况下,我们始终需要使用validate()方法来检查我们是否真的可以访问数据。

以下截图显示了程序执行的部分输出:

图片

Writer任务控制锁时,ReaderOptimisticReader都无法访问值。Reader任务在readLock()方法中被挂起,而在OptimisticReader中,对validate()方法的调用返回false,对tryOptimisticRead()方法的调用返回0,以指示锁被另一个线程以写模式控制。当Writertask释放锁时,ReaderOptimisticReader任务将能够访问共享对象的价值。

更多内容...

StampedLock类还有其他你应该知道的方法:

  • tryReadLock()tryReadLock(long time, TimeUnit unit): 这些方法尝试以读模式获取锁。如果无法获取,第一个版本将立即返回,第二个版本将等待参数中指定的时间。这些方法还返回一个必须检查的戳记(stamp != 0)。

  • tryWriteLock()tryWriteLock(long time, TimeUnit unit): 这些方法尝试以写模式获取锁。如果无法获取,第一个版本将立即返回,第二个版本将等待参数中指定的时间。这些方法还返回一个必须检查的戳记(stamp != 0)。

  • isReadLocked()isWriteLocked(): 如果锁当前以读或写模式持有,则返回这些方法。

  • tryConvertToReadLock(long stamp), tryConvertToWriteLock(long stamp), 和 tryConvertToOptimisticRead(long stamp): 这些方法尝试将作为参数传递的戳记转换为方法名称中指示的模式。如果可以转换,它们将返回一个新的戳记。如果不能转换,它们返回0

  • unlock(long stamp): 这将释放锁的相应模式。

参见

  • 本章中关于同步代码块与锁的配方

第三章:线程同步工具

在本章中,我们将涵盖以下主题:

  • 控制对资源的一个或多个副本的并发访问

  • 等待多个并发事件

  • 在公共点同步任务

  • 运行并发阶段任务

  • 控制并发阶段任务的相变

  • 在并发任务之间交换数据

  • 异步完成和链接任务

简介

在第二章,“基本线程同步”中,你学习了同步和临界区的概念。基本上,当我们谈论同步时,是指多个并发任务共享一个资源,例如一个对象或对象的属性。访问这个共享资源的代码块被称为临界区。

如果你没有使用适当的机制,可能会得到错误的结果,数据不一致或错误条件。因此,我们必须采用 Java 语言提供的同步机制之一来避免这些问题。

第二章,“基本线程同步”,介绍了以下基本同步机制:

  • synchronized关键字

  • 锁接口及其实现类:ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock

  • StampedLock

在本章中,你将学习如何使用高级机制来同步多个线程。这些高级机制如下:

  • 信号量:信号量是一个计数器,用于控制对一个或多个共享资源的访问。这种机制是并发编程的基本工具之一,并被大多数编程语言提供。

  • CountDownLatchCountDownLatch类是 Java 语言提供的一种机制,允许一个线程等待多个操作的完成。

  • CyclicBarrierCyclicBarrier类是 Java 语言提供的另一种机制,允许在公共点同步多个线程。

  • PhaserPhaser类是 Java 语言提供的另一种机制,用于控制分阶段执行的并发任务。所有线程必须完成一个阶段,才能继续下一个阶段。

  • ExchangerExchanger类是 Java 语言提供的另一种机制,提供了两个线程之间数据交换的点。

  • CompletableFutureCompletableFuture类提供了一种机制,其中一个或多个任务可以等待另一个任务完成,该任务将在未来的某个时刻以异步方式显式完成。这个类是在 Java 8 中引入的,并在 Java 9 中引入了新的方法。

信号量是通用的同步机制,您可以使用它来保护任何问题中的任何临界区。其他机制被认为适用于具有特定功能的应用程序,如前所述。请确保根据您应用程序的特性选择适当的机制。

本章介绍了七个食谱,将向您展示如何使用所描述的机制。

控制对资源的一个或多个副本的并发访问

在本食谱中,您将学习如何使用 Java 语言提供的信号量机制。信号量是一个保护对一个或多个共享资源访问的计数器。

信号量的概念由 Edsger Dijkstra 于 1965 年提出,并首次用于 THEOS 操作系统。

当一个线程想要访问共享资源之一时,它必须首先获取信号量。如果信号量的内部计数器大于 0,信号量会递减计数器并允许访问共享资源。计数器大于 0 表示有可用的资源,因此线程可以访问并使用其中一个。

否则,如果计数器为 0,信号量会将线程置于休眠状态,直到计数器大于 0。计数器中的 0 值表示所有共享资源都被其他线程使用,因此想要使用其中一个的线程必须等待直到其中一个变为空闲。

当线程完成对共享资源的使用后,它必须释放信号量,以便另一个线程可以访问资源。此操作会增加信号量的内部计数器。

在本食谱中,您将学习如何使用Semaphore类来保护多个资源副本。您将实现一个示例,该示例有一个打印队列,可以在三个不同的打印机上打印文档。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为PrintQueue的类,该类将实现打印队列:
        public class PrintQueue {

  1. 此类将具有三个私有属性。一个名为semaphore的信号量,一个名为freePrinters的布尔数组,以及一个名为lockPrinters的锁,如下代码片段所示:
        private final Semaphore semaphore; 
        private final boolean freePrinters[]; 
        private final Lock lockPrinters;

  1. 实现类的构造函数。它初始化类的三个属性,如下代码片段所示:
        public PrintQueue(){ 
          semaphore=new Semaphore(3); 
          freePrinters=new boolean[3]; 
          for (int i=0; i<3; i++){ 
            freePrinters[i]=true; 
          } 
          lockPrinters=new ReentrantLock(); 
        }

  1. 实现模拟打印文档的printJob()方法。它接收一个名为document的对象作为参数:
        public void printJob (Object document){

  1. 首先,printJob()方法调用acquire()方法以获取对信号量的访问权限。由于此方法可能会抛出InterruptedException异常,您必须包括处理它的代码:
        try { 
          semaphore.acquire();

  1. 然后,使用私有方法getPrinter()获取分配给打印此作业的打印机数量:
        int assignedPrinter=getPrinter();

  1. 然后,通过创建一个名为Main的类并实现main()方法来实现示例的主要类:
        long duration=(long)(Math.random()*10); 
        System.out.printf("%s - %s: PrintQueue: Printing a Job in
                           Printer %d during %d seconds\n",
                          new Date(), Thread.currentThread().getName(),
                          assignedPrinter,duration);
        TimeUnit.SECONDS.sleep(duration);

  1. 最后,通过调用release()方法释放信号量,并将使用的打印机标记为空闲,并将true赋值给freePrinters数组中相应的索引:
          freePrinters[assignedPrinter]=true; 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } finally { 
          semaphore.release();       
        }

  1. 接着,实现getPrinter()方法。这是一个私有方法,它返回一个int值,没有参数:
        private int getPrinter() {

  1. 首先,声明一个用于存储打印机索引的int变量:
        int ret=-1;

  1. 然后,获取对lockPrinters对象的访问权限:
        try { 
          lockPrinters.lock();

  1. 之后,在freePrinters数组中找到第一个真值,并将其索引保存到变量中。将此值修改为false,因为此打印机将忙碌:
        for (int i=0; i<freePrinters.length; i++) { 
          if (freePrinters[i]){ 
            ret=i; 
            freePrinters[i]=false; 
            break; 
          } 
        }

  1. 最后,释放lockPrinters对象并返回真值索引:
        } catch (Exception e) { 
          e.printStackTrace(); 
        } finally { 
          lockPrinters.unlock(); 
        } 
        return ret;

  1. 接着,创建一个名为Job的类,并指定它实现Runnable接口。此类实现了将文档发送到打印机的作业:
        public class Job implements Runnable {

  1. Semaphore类有三种额外的acquire()方法版本:
        private PrintQueue printQueue;

  1. 实现类的构造函数。它初始化类中声明的PrintQueue对象:
        public Job(PrintQueue printQueue){ 
          this.printQueue=printQueue; 
        }

  1. 实现一个run()方法:
        @Override 
          public void run() {

  1. 首先,此方法向控制台写入一条消息,显示作业已开始执行:
        System.out.printf("%s: Going to print a job\n",
                          Thread.currentThread().getName());

  1. 然后,它调用PrintQueue对象的printJob()方法:
        printQueue.printJob(new Object());

  1. 首先,此方法向控制台写入一条消息,显示作业已开始执行:
          System.out.printf("%s: The document has been printed\n",
                            Thread.currentThread().getName());         
        }

  1. 然后,通过创建一个名为Main的类并实现main()方法来实现示例的主要类:
        public class Main { 
          public static void main (String args[]){

  1. 创建一个名为printQueuePrintQueue对象:
        PrintQueue printQueue=new PrintQueue();

  1. 创建 12 个线程。每个线程将执行一个Job对象,该对象将发送文档到打印队列:
        Thread[] threads=new Thread[12]; 
        for (int i=0; I < threads.length i++){ 
          thread[i]=new Thread(new Job(printQueue),"Thread"+i); 
        }

  1. 最后,启动 12 个线程:
        for (int i=0; I < threads.length; i++){ 
          thread[i].start(); 
        }

它是如何工作的...

之后,在freePrinters数组中找到第一个真值,并将其索引保存到变量中。将此值修改为false,因为此打印机将忙碌:

  1. 首先,使用acquire()方法获取信号量。

  2. 然后,对共享资源进行必要的操作。

  3. 最后,使用release()方法释放信号量。

在此示例中,另一个重要点是PrintQueue类的构造函数和Semaphore对象的初始化。您将值3作为此构造函数的参数传递,因此您正在创建一个将保护三个资源的信号量。前三个调用acquire()方法的线程将获得此示例的关键部分访问权限,其余的将被阻塞。当一个线程完成关键部分并释放信号量时,另一个线程将获取它。

以下截图显示了此示例的执行输出:

实现类的构造函数。它初始化类中声明的PrintQueue对象:

你可以看到前三个打印作业是同时开始的。然后,当一个打印机完成其作业时,另一个打印机开始工作。

还有更多...

Semaphore类有三种额外的acquire()方法版本:

  • acquireUninterruptibly(): acquire() 方法,当信号量的内部计数器为 0 时,会阻塞线程直到信号量被释放。在此期间,线程可能会被中断;如果发生这种情况,该方法将抛出 InterruptedException 异常。这个版本的 acquire 操作忽略线程的中断,并且不会抛出任何异常。

  • tryAcquire(): 此方法尝试获取信号量。如果可以,它返回 true 值。但如果不能,它将返回 false 而不是被阻塞并等待信号量的释放。根据返回值采取正确的行动是您的责任。

  • tryAcquire(long timeout, TimeUnit unit): 此方法与前面的方法等效,但它等待参数中指定的时间段的信号量。如果时间结束且方法尚未获取信号量,它将返回 false

acquire()acquireUninterruptibly()tryAcquire()release() 方法有一个额外的版本,它有一个 int 参数。此参数表示使用它们的线程想要获取或释放的许可证数量,换句话说,就是线程想要从信号量的内部计数器中删除或添加的单位数量。

acquire()acquireUninterruptibly()tryAcquire() 方法的案例中,如果计数器的值小于作为参数值传递的数字,线程将被阻塞,直到计数器达到相同的值或更大的值。

信号量的公平性

公平的概念被 Java 语言用于所有可能具有各种线程阻塞并等待同步资源(例如,信号量)释放的类。默认模式称为 非公平模式。在此模式下,当同步资源被释放时,会选择一个等待的线程并分配此资源;然而,选择是没有任何标准的。另一方面,公平模式改变了这种行为,并选择等待时间最长的线程。

就像其他类一样,Semaphore 类在其构造函数中接受一个第二个参数。此参数必须接受一个布尔值。如果您给它一个 false 值,您将创建一个在非公平模式下工作的信号量。如果您不使用此参数,您将获得相同的行为。如果您给它一个 true 值,您将创建一个在公平模式下工作的信号量。

参见

  • 第九章 中 监控锁接口 的配方,测试并发应用程序

  • 第二章 中 使用锁同步代码块 的配方,基本线程同步

等待多个并发事件

Java 并发 API 提供了一个允许一个或多个线程等待直到一组操作完成的类。它被称为CountDownLatch类。这个类使用一个整数初始化,这是线程将要等待的操作数。当线程想要等待这些操作的执行时,它使用await()方法。此方法将线程置于休眠状态,直到操作完成。当这些操作中的任何一个完成时,它使用countDown()方法来递减CountDownLatch类的内部计数器。当计数器到达0时,该类唤醒在await()方法中休眠的所有线程。

在这个菜谱中,你将学习如何使用CountDownLatch类来实现视频会议系统。视频会议系统应在开始之前等待所有参与者的到达:

准备中

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为Videoconference的类,并指定它实现Runnable接口。这个类将实现视频会议系统:
        public class Videoconference implements Runnable{

  1. 声明一个名为controllerCountDownLatch对象:
        private final CountDownLatch controller;

  1. 实现类的构造函数,初始化CountDownLatch属性。Videoconference类将等待接收到的参与者数量到达:
        public Videoconference(int number) { 
          controller=new CountDownLatch(number); 
        }

  1. 实现参与者的arrive()方法。该方法将在每次参与者到达视频会议时被调用。它接收一个名为nameString类型参数:
        public void arrive(String name){

  1. 首先,它使用接收到的参数写一条消息:
        System.out.printf("%s has arrived.",name);

  1. 然后,它调用CountDownLatch对象的countDown()方法:
        controller.countDown();

  1. 最后,它使用CountDownLatch对象的getCount()方法写另一条消息,其中包含等待到达的参与者数量:
        System.out.printf("VideoConference: Waiting for %d
                           participants.\n",controller.getCount());

  1. 接下来,实现视频会议系统的main方法。这是每个Runnable对象必须有的run()方法:
        @Override 
        public void run() {

  1. 首先,使用getCount()方法写一条消息,其中包含视频会议中的参与者数量:
        System.out.printf("VideoConference: Initialization: %d
                           participants.\n",controller.getCount());

  1. 然后,使用await()方法等待所有参与者。由于此方法可能会抛出InterruptedException异常,你必须包括处理它的代码:
        try { 
          controller.await();

  1. 最后,写一条消息来表明所有参与者都已到达:
          System.out.printf("VideoConference: All the participants have
                             come\n"); 
          System.out.printf("VideoConference: Let's start...\n"); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 接下来,创建Participant类,并指定它实现Runnable接口。这个类代表视频会议中的每个参与者:
        public class Participant implements Runnable {

  1. 声明一个名为conference的私有Videoconference属性:
        private Videoconference conference;

  1. 声明一个名为name的私有String属性:
        private String name;

  1. 实现类的构造函数,初始化前面提到的两个属性:
        public Participant(Videoconference conference, String name) { 
          this.conference=conference; 
          this.name=name; 
        }

  1. 实现参与者的run()方法:
        @Override 
        public void run() {

  1. 首先,让线程随机休眠一段时间:
        long duration=(long)(Math.random()*10); 
        try { 
          TimeUnit.SECONDS.sleep(duration); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 然后,使用Videoconference对象的arrive()方法来指示这个参与者的到达:
        conference.arrive(name);

  1. 最后,通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 接下来,创建一个名为conferenceVideoconference对象,它等待 10 个参与者:
        Videoconference conference=new Videoconference(10);

  1. 创建Thread来运行这个Videoconference对象并启动它:
        Thread threadConference=new Thread(conference); 
        threadConference.start();

  1. 创建 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,对其方法的任何调用都将没有效果。如果你想再次进行相同的同步,你必须创建一个新的对象。

下面的截图显示了示例的执行输出:

你可以看到参与者是如何到达的,一旦内部计数器达到0CountDownLatch对象就会唤醒写入消息的Videoconference对象,表明视频会议应该开始。

还有更多...

CountDownLatch类还有一个版本的await()方法,如下所示:

  • await(long time, TimeUnit unit):在这个方法中,线程将继续睡眠,直到它被中断,即CountDownLatch的内部计数器达到0或指定的时长过去。TimeUnit类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

在一个共同点上同步任务

Java 并发 API 提供了一个同步实用工具,允许在确定点同步两个或更多线程。它是 CyclicBarrier 类。这个类与本章中 等待多个并发事件 食谱中解释的 CountDownLatch 类类似,但它有一些差异,使其成为一个更强大的类。

CyclicBarrier 类使用一个整数初始化,这是将在确定点同步的线程数。当这些线程中的任何一个到达确定点时,它调用 await() 方法等待其他线程。当线程调用此方法时,CyclicBarrier 类阻塞正在睡眠的线程,直到其他线程到达。当最后一个线程调用 CyclicBarrier 对象的 await() 方法时,它唤醒所有等待的线程并继续其工作。

CyclicBarrier 类的一个有趣的优势是,您可以传递一个额外的 Runnable 对象作为初始化参数,当所有线程到达共同点时,CyclicBarrier 类将执行此对象作为线程。这一特性使得这个类适合使用分而治之编程技术并行化任务。

在这个食谱中,您将学习如何使用 CyclicBarrier 类在确定点同步一组线程。您还将使用一个 Runnable 对象,该对象将在所有线程到达此点后执行。在示例中,您将在数字矩阵中寻找一个数字。矩阵将被分成子集(使用分而治之技术),因此每个线程将在一个子集中寻找数字。一旦所有线程完成各自的工作,一个最终任务将统一他们的结果。

准备工作

本食谱的示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做这件事...

按照以下步骤实现示例:

  1. 通过实现两个辅助类来开始示例。首先,创建一个名为 MatrixMock 的类。这个类将生成一个介于 1 和 10 之间的随机数字矩阵,线程将在其中寻找一个数字:
        public class MatrixMock {

  1. 声明一个名为 data 的私有 int 矩阵:
        private final int data[][];

  1. 实现类的构造函数。这个构造函数将接收矩阵的行数、每行的长度以及我们要寻找的数字作为参数。所有三个参数都是 int 类型:
        public MatrixMock(int size, int length, int number){

  1. 初始化在构造函数中使用的变量和对象:
        int counter=0; 
        data=new int[size][length]; 
        Random random=new Random();

  1. 用随机数字填充矩阵。每次生成一个数字时,将其与要寻找的数字进行比较。如果它们相等,增加计数器:
        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++; 
            } 
          } 
        }

  1. 最后,在控制台打印一条消息,显示将在生成的矩阵中查找的数字的出现次数。此消息将用于检查线程是否得到正确的结果:
        System.out.printf("Mock: There are %d ocurrences of number in
                           generated data.\n",counter,number);

  1. 实现一个 getRow() 方法。此方法接收一个表示矩阵中行号的 int 参数;如果存在该行,则返回该行,如果不存在,则返回 null
        public int[] getRow(int row){ 
          if ((row>=0)&&(row<data.length)){ 
            return data[row]; 
          } 
          return null; 
        }

  1. 现在实现一个名为 Results 的类。此类将存储在数组中,以存储矩阵每行中搜索数字的出现次数:
        public class Results {

  1. 声明一个名为 data 的私有 int 数组:
        private final int data[];

  1. 实现类的构造函数。此构造函数接收一个表示数组元素数量的整数参数:
        public Results(int size){ 
          data=new int[size]; 
        }

  1. 实现一个 setData() 方法。此方法接收一个数组位置和一个值作为参数,并设置数组中该位置的价值:
        public void  setData(int position, int value){ 
          data[position]=value; 
        }

  1. 实现一个 getData() 方法。此方法返回包含结果数组的数组:
        public int[] getData(){ 
          return data; 
        }

  1. 现在有了辅助类,是时候实现线程了。首先,实现 Searcher 类。此类将在随机数字矩阵的确定行中查找数字。创建一个名为 Searcher 的类,并指定它实现 Runnable 接口:
        public class Searcher implements Runnable {

  1. 声明两个私有 int 属性,即 firstRowlastRow。这两个属性将确定此对象将搜索数字的行子集:
        private final int firstRow; 
        private final int lastRow;

  1. 声明一个名为 mock 的私有 MatrixMock 属性:
        private final MatrixMock mock;

  1. 声明一个名为 results 的私有 Results 属性:
        private final Results results;

  1. 声明一个名为 number 的私有 int 属性,该属性将存储将要查找的数字:
        private final int number;

  1. 声明一个名为 barrierCyclicBarrier 对象:
        private final CyclicBarrier barrier;

  1. 实现类的构造函数,该构造函数初始化之前声明的所有属性:
        public Searcher(int firstRow, int lastRow, MatrixMock 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; 
        }

  1. 实现一个 run() 方法,该方法将搜索数字。它使用一个名为 counter 的内部变量,该变量将存储每行中数字出现的次数:
        @Override 
        public void run() { 
          int counter;

  1. 在控制台打印一条消息,显示分配给此任务的任务行:
        System.out.printf("%s: Processing lines from %d to %d.\n",
                          Thread.currentThread().getName(),
                          firstRow,lastRow);

  1. 处理分配给此线程的所有行。对于每一行,计算要搜索的数字的出现次数,并将此数字存储在 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); 
        }

  1. 在控制台打印一条消息,指示此对象已完成搜索:
        System.out.printf("%s: Lines processed.\n",
                          Thread.currentThread().getName());

  1. 调用 CyclicBarrier 对象的 await() 方法,并添加必要的代码来处理此方法可能抛出的 InterruptedExceptionBrokenBarrierException 异常:
        try { 
          barrier.await(); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } catch (BrokenBarrierException e) { 
          e.printStackTrace(); 
        }

  1. 现在实现一个计算矩阵中数字出现总数的类。此类使用存储矩阵每行中数字出现次数的 Results 对象来进行计算。创建一个名为 Grouper 的类,并指定它实现 Runnable 接口:
        public class Grouper implements Runnable {

  1. 声明一个名为 results 的私有 Results 属性:
        private final Results results;

  1. 实现类的构造函数,该构造函数初始化 Results 属性:
        public Grouper(Results results){ 
          this.results=results; 
        }

  1. 实现一个名为 run() 的方法,该方法将计算数组中数字出现的总次数:
        @Override 
        public void run() {

  1. 声明一个 int 变量并向控制台写入一条消息以指示过程的开始:
        int finalResult=0; 
        System.out.printf("Grouper: Processing results...\n");

  1. 使用 results 对象的 getData() 方法获取每行中数字的出现次数。然后,处理数组的所有元素并将它们的值添加到 finalResult 变量中:
        int data[]=results.getData(); 
        for (int number:data){ 
          finalResult+=number; 
        }

  1. 在控制台打印结果:
        System.out.printf("Grouper: Total result: %d.\n", finalResult);

  1. 最后,通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 声明并初始化五个常量以存储应用程序的参数:
        final int ROWS=10000; 
        final int NUMBERS=1000; 
        final int SEARCH=5;  
        final int PARTICIPANTS=5; 
        final int LINES_PARTICIPANT=2000;

  1. 创建一个名为 mockMatrixMock 对象。它将有 10,000 行,每行有 1,000 个元素。现在,你将搜索数字五:
        MatrixMock mock=new MatrixMock(ROWS, NUMBERS,SEARCH);

  1. 创建一个名为 resultsResults 对象。它将有 10,000 个元素:
        Results results=new Results(ROWS);

  1. 创建一个名为 grouperGrouper 对象:
        Grouper grouper=new Grouper(results);

  1. 创建一个名为 barrierCyclicBarrier 对象。此对象将等待五个线程。当这五个线程完成时,它将执行之前创建的 Grouper 对象:
        CyclicBarrier barrier=new CyclicBarrier(PARTICIPANTS,grouper);

  1. 创建五个 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类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

这个类还提供了getNumberWaiting()方法,它返回在await()方法中被阻塞的线程数,以及getParties()方法,它返回将要与CyclicBarrier同步的任务数。

重置 CyclicBarrier 对象

CyclicBarrier类与CountDownLatch类有一些共同点,但它们也有一些不同之处。最重要的不同之处在于,一个CyclicBarrier对象可以被重置到其初始状态,将其内部计数器设置为初始化时的值。

这个重置操作可以使用CyclicBarrier类的reset()方法来完成。当发生这种情况时,所有在await()方法中等待的线程都会收到BrokenBarrierException异常。这个异常在本配方中通过打印堆栈跟踪来处理;然而,在一个更复杂的应用程序中,它可能执行其他操作,例如重新启动执行或恢复到中断时的操作点。

损坏的 CyclicBarrier 对象

一个CyclicBarrier对象可以处于一个特殊状态,称为损坏状态。当有多个线程在await()方法中等待时,其中一个线程被中断,中断的线程会收到InterruptedException异常,但其他线程会收到BrokenBarrierException异常;CyclicBarrier被置于损坏状态。

CyclicBarrier类提供了isBroken()方法。如果对象处于损坏状态,则返回true;否则返回false

参见

  • 本章中的等待多个并发事件配方

运行并发阶段任务

Java 并发 API 提供的最复杂和强大的功能之一是使用Phaser类执行并发阶段任务的能力。这种机制在我们有一些被分为步骤的并发任务时非常有用。Phaser类为我们提供了一种机制,在每一步结束时同步线程,因此没有线程会开始第二步,直到所有线程都完成了第一步。

与其他同步工具一样,我们必须使用参与同步操作的任务数量来初始化Phaser类,但我们可以通过增加或减少这个数字来动态地修改这个数字。

在这个菜谱中,你将学习如何使用Phaser类来同步三个并发任务。这三个任务在三个不同的文件夹及其子文件夹中查找扩展名为.log且在过去 24 小时内修改的文件。此任务分为三个步骤:

  1. 获取指定文件夹及其子文件夹中扩展名为.log的文件列表。

  2. 通过删除 24 小时前修改的文件来过滤第一步创建的列表。

  3. 在控制台打印结果。

在步骤 1 和步骤 2 结束时,我们检查列表中是否有任何元素。如果没有,线程结束其执行并被从Phaser类中删除。

准备工作

这个菜谱的示例是用 Eclipse IDE 实现的。如果你使用 Eclipse 或 NetBeans 等其他 IDE,打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为FileSearch的类,并指定它实现Runnable接口。此类实现了在文件夹及其子文件夹中搜索具有指定扩展名且在过去 24 小时内修改的文件的操作:
        public class FileSearch implements Runnable {

  1. 声明一个私有的String属性来存储搜索操作将开始的文件夹:
        private final String initPath;

  1. 声明另一个私有的String属性来存储我们将要查找的文件的扩展名:
        private final String fileExtension

  1. 声明一个私有的List属性来存储我们将找到具有所需特征的文件的完整路径:
        private List<String> results;

  1. 最后,声明一个私有的Phaser属性来控制任务不同阶段的同步:
        private Phaser phaser;

  1. 接下来,实现类的构造函数,该构造函数将初始化类的属性。它接收初始文件夹的完整路径作为参数,文件的扩展名,以及phaser
        public FileSearch(String initPath, String fileExtension,
                          Phaser phaser) { 
          this.initPath = initPath; 
          this.fileExtension = fileExtension; 
          this.phaser=phaser; 
          results=new ArrayList<>(); 
        }

  1. 现在,实现一些辅助方法,这些方法将由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]); 
              } 
            } 
          } 
        }

  1. 然后,实现fileProcess()方法。它接收一个File对象作为参数,并检查其扩展名是否与我们正在寻找的扩展名相等。如果它们相等,此方法将文件的绝对路径添加到结果列表中:
        private void fileProcess(File file) { 
          if (file.getName().endsWith(fileExtension)) { 
            results.add(file.getAbsolutePath()); 
          } 
        }

  1. 现在实现filterResults()方法。它不接受任何参数,并过滤第一阶段获取的文件列表;删除 24 小时前修改的文件。首先,创建一个新的空列表并获取实际日期:
        private void filterResults() { 
          List<String> newResults=new ArrayList<>(); 
          long actualDate=new Date().getTime();

  1. 然后,遍历结果列表中的所有元素。对于结果列表中的每个路径,为文件创建一个File对象并获取其最后修改日期:
        for (int i=0; i<results.size(); i++){ 
          File file=new File(results.get(i)); 
          long fileDate=file.lastModified();

  1. 然后,将此日期与实际日期进行比较,如果差异小于 1 天,则将文件的完整路径添加到新的结果列表中:
          if (actualDate-fileDate< TimeUnit.MILLISECONDS
                                        .convert(1,TimeUnit.DAYS)){ 
            newResults.add(results.get(i)); 
          } 
        }

  1. 最后,将旧的结果列表更改为新的列表:
          results=newResults; 
        }

  1. 接下来,实现 checkResults() 方法。此方法将在第一和第二阶段结束时被调用,并检查结果列表是否为空。此方法没有任何参数:
        private boolean checkResults() {

  1. 首先,检查结果列表的大小。如果为 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;

  1. 如果结果列表有元素,对象将向控制台写入一条消息表示这一点。然后,它调用 Phaser 对象的 arriveAndAwaitAdvance() 方法来通知此线程已完成实际阶段,并希望被阻塞,直到分阶段操作中的所有参与者线程完成实际阶段:
          } else { 
            System.out.printf("%s: Phase %d: %d results.\n",
                              Thread.currentThread().getName(),
                              phaser.getPhase(),results.size()); 
            phaser.arriveAndAwaitAdvance(); 
            return true; 
          }
        }

  1. 最后一个辅助方法是 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(); 
        }

  1. 是时候实现 run() 方法了,该方法使用前面描述的辅助方法执行操作。我们还将实现 Phaser 对象来控制阶段之间的转换。首先,调用 Phaser 对象的 arriveAndAwaitAdvance() 方法。搜索不会开始,直到所有线程都已创建:
        @Override 
        public void run() { 
          phaser.arriveAndAwaitAdvance();

  1. 然后,向控制台写入一条消息,指示搜索任务的开始:
        System.out.printf("%s: Starting.\n",
                          Thread.currentThread().getName());

  1. 检查 initPath 属性是否存储文件夹的名称,并使用 directoryProcess() 方法在该文件夹及其所有子文件夹中查找指定扩展名的文件:
        File file = new File(initPath); 
        if (file.isDirectory()) { 
          directoryProcess(file); 
        }

  1. 使用 checkResults() 方法检查是否有任何结果。如果没有结果,使用 return 关键字结束线程的执行:
        if (!checkResults()){ 
          return; 
        }

  1. 使用 filterResults() 方法过滤结果列表:
        filterResults();

  1. 再次使用 checkResults() 方法检查是否有任何结果。如果没有结果,使用 return 关键字结束线程的执行:
        if (!checkResults()){ 
          return; 
        }

  1. 使用 showInfo() 方法将最终结果列表打印到控制台,注销线程,并打印一条消息,指示线程的最终化:
        showInfo(); 
        phaser.arriveAndDeregister(); 
        System.out.printf("%s: Work completed.\n",
                          Thread.currentThread().getName());

  1. 现在,通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主要类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个具有三个参与者的 Phaser 对象:
        Phaser phaser=new Phaser(3);

  1. 创建三个 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);

  1. 创建并启动一个线程来执行第一个 FileSearch 对象:
        Thread systemThread=new Thread(system,"System"); 
        systemThread.start();

  1. 创建并启动一个线程来执行第二个 FileSearch 对象:
        Thread appsThread=new Thread(apps,"Apps"); 
        appsThread.start();

  1. 创建并启动一个线程来执行第三个 FileSearch 对象:
        Thread documentsThread=new Thread(documents, "Documents"); 
        documentsThread.start();

  1. 等待三个线程的最终化:
        try { 
          systemThread.join(); 
          appsThread.join(); 
          documentsThread.join(); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用 isFinalized() 方法写入 Phaser 对象的已最终化标志的值:
        System.out.println("Terminated: "+ phaser.isTerminated());

它是如何工作的...

程序开始创建一个 Phaser 对象,该对象将在每个阶段的末尾控制线程的同步。Phaser 的构造函数接收参与者数量作为参数。在我们的例子中,Phaser 有三个参与者。这个数字表示 Phaser 需要执行 arriveAndAwaitAdvance() 方法的线程数量,在 Phaser 能够更改阶段并唤醒休眠的线程之前。

一旦创建了 Phaser,我们就启动三个线程,这些线程使用三个不同的 FileSearch 对象来执行。

在这个例子中,我们使用 Windows 操作系统的路径。如果你使用的是其他操作系统,请修改路径以适应你环境中现有的路径,例如 /var/log 或类似路径。

这个 FileSearch 对象的 run() 方法中的第一条指令是调用 Phaser 对象的 arriveAndAwaitAdvance() 方法。如前所述,Phaser 知道我们想要同步的线程数量。当一个线程调用此方法时,Phaser 会减少需要最终确定实际阶段的线程数量,并将此线程置于休眠状态,直到所有剩余的线程完成此阶段。在 run() 方法的开始处调用此方法确保在所有线程创建之前,没有任何 FileSearch 线程开始工作。

在第一阶段和第二阶段结束时,我们检查阶段是否生成了结果以及结果列表是否有元素,或者阶段没有生成结果且列表为空。在第一种情况下,checkResults() 方法调用前面解释过的 arriveAndAwaitAdvance() 方法。在第二种情况下,如果列表为空,线程继续执行就没有意义了,因此它结束执行。但是你必须通知 Phaser 对象将有一个参与者减少。为此,我们使用了 arriveAndDeregister()。这通知 phaser 线程已经完成了实际阶段,但不会参与未来的阶段,因此 phaser 不必等待它继续。

showInfo() 方法中实现的第三阶段结束时,有一个调用 phaserarriveAndAwaitAdvance() 方法的调用。通过这个调用,我们保证所有线程同时完成。当此方法执行完毕时,有一个调用 phaserarriveAndDeregister() 方法的调用。通过这个调用,我们注销 phaser 的线程,如前所述,因此当所有线程完成时,phaser 将没有参与者。

最后,main() 方法等待三个线程完成,并调用 phaserisTerminated() 方法。当 phaser 的参与者为零时,它进入所谓的终止状态,并且此方法返回 true。当我们注销 phaser 的所有线程时,它将处于终止状态,并且此调用将 true 打印到控制台。

一个 Phaser 对象可以处于两种状态:

  • 活动:当Phaser接受新参与者的注册并在每个阶段的末尾进行同步时,它会进入此状态。在此状态下,Phaser的工作方式如本食谱中所述。此状态在 Java 并发 API 中未提及。

  • 终止:默认情况下,当Phaser中的所有参与者都已注销时,Phaser进入此状态,这意味着它没有参与者。此外,当onAdvance()方法返回true时,Phaser处于终止状态。如果你重写此方法,你可以更改默认行为。当Phaser处于此状态时,同步方法arriveAndAwaitAdvance()会立即返回,而不执行任何同步操作。

Phaser类的一个显著特点是,你不必控制与phaser相关的方法抛出的任何异常。与其他同步工具不同,在phaser中休眠的线程不会响应中断事件,也不会抛出InterruptedException异常。只有一个例外,将在下一节中解释。

以下截图显示了示例的一次执行结果:

它显示了执行的前两个阶段。你可以看到Apps线程在第二阶段完成其执行,因为其结果列表为空。当你执行示例时,你会看到一些线程在其余线程之前完成一个阶段,并且它们在所有线程完成一个阶段之前等待,然后继续执行。

还有更多...

Phaser类提供了其他与阶段变化相关的方法。这些方法如下:

  • arrive(): 此方法通知Phaser类一个参与者已完成实际阶段,但它不应等待其余参与者继续执行。在使用此方法时要小心,因为它不与其他线程同步。

  • awaitAdvance(int phase): 此方法使当前线程休眠,直到phaser参数的所有参与者完成当前阶段,也就是说,如果我们传递的参数数等于phaser的实际阶段。如果参数和phaser的实际阶段不相等,该方法将结束其执行。

  • 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可能会被终止,你应该验证这些方法(awaitAdvance()arriveAndAwaitAdvance())的返回值,以了解phaser是否已被终止。

相关内容

  • 在第九章的监控 Phaser 类菜谱中,测试并发应用程序,你可以找到更多相关信息。

控制并发分阶段任务中的阶段变化

Phaser类提供了一个在phaser改变阶段时执行的方法。它是onAdvance()方法。它接收两个参数:当前阶段的数量和注册的参与者数量。如果Phaser继续执行,则返回一个布尔值false;如果Phaser已完成并需要进入终止状态,则返回true

此方法的默认实现如果注册的参与者数量为零则返回true,否则返回false。但如果你扩展了Phaser类并重写了此方法,你可以修改这种行为。通常,当你需要在从一个阶段过渡到下一个阶段时执行一些操作时,你会有兴趣这样做。

在这个菜谱中,你将学习如何控制实现Phaser类并重写onAdvance()方法以在每个阶段变化时执行一些操作的phaser中的阶段变化。你将实现一个考试模拟,其中将有一些学生需要完成三个练习。所有学生必须完成一个练习后才能进行下一个练习。

准备工作

此菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为MyPhaser的类,并指定它从Phaser类扩展:
        public class MyPhaser extends Phaser {

  1. 覆盖onAdvance()方法。根据phase属性的值,我们调用不同的辅助方法。如果phase属性等于零,你必须调用studentsArrived()方法。如果phase等于一,你必须调用finishFirstExercise()方法。如果phase等于二,你必须调用finishSecondExercise()方法。最后,如果phase等于三,你必须调用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; 
          } 
        }

  1. 实现辅助方法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; 
        }

  1. 实现辅助方法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; 
        }

  1. 实现辅助方法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; 
        }

  1. 实现辅助方法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; 
        }

  1. 创建一个名为Student的类,并指定它实现Runnable接口。此类将模拟考试的学生:
        public class Student implements Runnable {

  1. 声明一个名为phaserPhaser对象:
        private Phaser phaser;

  1. 实现类的构造函数,初始化Phaser对象:
        public Student(Phaser phaser) { 
          this.phaser=phaser; 
        }

  1. 实现模拟考试实现的run()方法:
        @Override 
        public void run() {

  1. 首先,该方法向控制台写入一条消息,指示学生已到达考场,并调用phaserarriveAndAwaitAdvance()方法等待其他线程:
        System.out.printf("%s: Has arrived to do the exam. %s\n",
                          Thread.currentThread().getName(),new Date()); 
        phaser.arriveAndAwaitAdvance();

  1. 然后,向控制台写入一条消息,并调用模拟考试第一题实现的私有doExercise1()方法。之后,再向控制台写入另一条消息,并调用phaserarriveAndAwaitAdvance()方法等待其他学生完成第一题:
        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();

  1. 为第二题和第三题实现相同的代码:
        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();

  1. 实现辅助方法doExercise1()。此方法使当前线程或执行方法的线程随机休眠一段时间:
        private void doExercise1() { 
          try { 
            long duration=(long)(Math.random()*10); 
            TimeUnit.SECONDS.sleep(duration); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现辅助方法doExercise2()。此方法使当前线程或执行方法的线程随机休眠一段时间:
        private void doExercise2() { 
          try { 
            long duration=(long)(Math.random()*10); 
            TimeUnit.SECONDS.sleep(duration); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现辅助方法doExercise3()。此方法使线程随机休眠一段时间:
        private void doExercise3() { 
          try { 
            long duration=(long)(Math.random()*10); 
            TimeUnit.SECONDS.sleep(duration); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个MyPhaser对象:
        MyPhaser phaser=new MyPhaser();

  1. 创建五个Student对象,并使用register()方法将它们注册到phaser属性中:
        Student students[]=new Student[5]; 
        for (int i=0; i<students.length; i++){ 
          students[i]=new Student(phaser); 
          phaser.register(); 
        }

  1. 创建五个线程来运行学生并启动它们:
        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(); 
        }

  1. 等待五个线程的最终化:
        for (int i=0; i<threads.length; i++){ 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 使用 isTerminated() 方法写一条消息来显示 phaser 处于终止状态:
        System.out.printf("Main: The phaser has finished: %s.\n",
                          phaser.isTerminated());

它是如何工作的...

这个练习模拟了实现一个包含三项练习的考试。所有学生必须完成一项练习后才能开始下一项。为了实现这个同步要求,我们使用了 Phaser 类;然而,在这种情况下,您实现了自己的 phaser,通过扩展原始类来重写 onAdvance() 方法。

PhaserarriveAndAwaitAdvance() 方法中唤醒所有休眠的线程之前,该方法被 Phaser 调用并改变阶段。该方法由作为 arriveAndAwaitAdvance() 方法代码一部分的最后一个完成阶段的线程调用。该方法接收实际阶段的数字作为参数,其中 0 是第一个阶段的数字和注册的参与者数量。最有用的参数是实际阶段。如果您根据实际阶段执行不同的操作,您必须使用替代结构(if...elseswitch)来选择要执行的操作。在示例中,我们使用 switch 结构来选择每个阶段变化时不同的方法。

onAdvance() 方法返回一个布尔值,表示 phaser 是否已终止。如果 phaser 返回 false,则表示它尚未终止;如果发生这种情况,线程将继续执行其他阶段的操作。如果 phaser 返回 true,则 phaser 仍然会唤醒挂起的线程,但将 phaser 移至终止状态。因此,所有未来对 phaser 任何方法的调用都将立即返回,并且 isTerminated() 方法将返回 true

Main 类中,当您创建 MyPhaser 对象时,您没有指定 phaser 中的参与者数量。您为每个创建的 Student 对象调用 register() 方法来在 phaser 中注册一个参与者。这种调用并不在 Student 对象或执行它的线程与 phaser 之间建立关系。实际上,phaser 中的参与者数量只是一个数字。phaser 与参与者之间没有关系。

以下截图显示了此示例的执行结果:

您可以看到学生们完成第一项练习的不同时间。当所有学生都完成第一项练习后,phaser 调用 onAdvance() 方法在控制台写入日志消息,然后所有学生同时开始第二项练习。

参见

  • 本章的 Running concurrent-phased tasks 菜谱

  • 在第九章 Testing Concurrent ApplicationsMonitoring a Phaser class 菜谱中

在并发任务之间交换数据

Java 并发 API 提供了一个同步实用工具,允许两个并发任务之间交换数据。更详细地说,Exchanger 类允许您在两个线程之间定义一个同步点。当两个线程到达这个点时,它们交换一个数据结构,使得第一个线程的数据结构传递给第二个线程,反之亦然。

在类似生产者-消费者问题的场景中,此类可能非常有用。这是一个经典并发问题,其中有一个公共的数据缓冲区,一个或多个数据生产者,以及一个或多个数据消费者。由于 Exchanger 类仅同步两个线程,因此如果您有一个只有一个生产者和一个消费者的生产者-消费者问题,则可以使用它。

在本例中,您将学习如何使用 Exchanger 类解决一个生产者和一个消费者之间的生产者-消费者问题。

准备工作

本例的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 首先,开始实现生产者。创建一个名为 Producer 的类并指定它实现 Runnable 接口:
        public class Producer implements Runnable {

  1. 声明一个名为 bufferList<String> 字段。这将是一个生产者与消费者之间交换数据的数据结构:
        private List<String> buffer;

  1. 声明一个名为 exchangerExchanger<List<String>> 字段。这将是一个用于同步生产者和消费者的交换器对象:
        private final Exchanger<List<String>> exchanger;

  1. 实现类的构造函数以初始化两个属性:
        public Producer (List<String> buffer, Exchanger<List<String>>
                         exchanger){ 
          this.buffer=buffer; 
          this.exchanger=exchanger; 
        }

  1. 实现方法 run()。在其内部,实现 10 次交换循环:
        @Override 
        public void run() { 
          for (int cycle = 1; cycle <= 10; cycle++){ 
            System.out.printf("Producer: Cycle %d\n",cycle);

  1. 在每个循环中,向缓冲区添加 10 个字符串:
        for (int j=0; j<10; j++){ 
          String message="Event "+(((cycle-1)*10)+j); 
          System.out.printf("Producer: %s\n",message); 
          buffer.add(message); 
        }

  1. 调用 exchange() 方法与消费者交换数据。由于此方法可能会抛出 InterruptedException 异常,您必须添加一些代码来处理它。
        try { 
          buffer=exchanger.exchange(buffer); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 
          System.out.println("Producer: "+buffer.size()); 
        }

  1. 现在,实现消费者。创建一个名为 Consumer 的类并指定它实现 Runnable 接口:
        public class Consumer implements Runnable {

  1. 声明一个名为 bufferList<String> 字段。这将是一个生产者与消费者之间交换数据的数据结构:
        private List<String> buffer;

  1. 声明一个名为 exchangerExchanger<List<String>> 字段。这将是一个用于同步生产者和消费者的 exchanger 对象:
        private final Exchanger<List<String>> exchanger;

  1. 实现类的构造函数以初始化两个属性:
        public Consumer(List<String> buffer, Exchanger<List<String>>
                        exchanger){ 
          this.buffer=buffer; 
          this.exchanger=exchanger; 
        }

  1. 实现方法 run()。在其内部,实现 10 次交换循环:
        @Override 
        public void run() { 
          for (int cycle=1; cycle <= 10; cycle++){ 
            System.out.printf("Consumer: Cycle %d\n",cycle);

  1. 在每个循环中,首先调用 exchange() 方法以与生产者同步。消费者需要数据来消费。由于此方法可能会抛出 InterruptedException 异常,您必须添加一些代码来处理它:
        try { 
          buffer=exchanger.exchange(buffer); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 将生产者发送到其缓冲区的 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); 
          } 
        }

  1. 现在,通过创建一个名为 Main 的类并添加 main() 方法来实现示例的 main 类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建两个缓冲区,这些缓冲区将被生产者和消费者使用:
        List<String> buffer1=new ArrayList<>(); 
        List<String> buffer2=new ArrayList<>();

  1. 创建用于同步生产者和消费者的Exchanger对象:
        Exchanger<List<String>> exchanger=new Exchanger<>();

  1. 创建ProducerConsumer对象:
        Producer producer=new Producer(buffer1, exchanger); 
        Consumer consumer=new Consumer(buffer2, exchanger);

  1. 创建执行生产者和消费者的线程并启动线程:
        Thread threadProducer=new Thread(producer); 
        Thread threadConsumer=new Thread(consumer); 

        threadProducer.start(); 
        threadConsumer.start();

它是如何工作的...

消费者从一个空的缓冲区开始,并调用Exchanger来与生产者同步。它需要数据来消费。生产者从一个空的缓冲区开始执行。它创建了 10 个字符串,将它们存储在缓冲区中,并使用Exchanger来与消费者同步。

到目前为止,两个线程(生产者和消费者)都在Exchanger中,它改变了数据结构。因此,当消费者从exchange()方法返回时,它将有一个包含 10 个字符串的缓冲区。当生产者从exchange()方法返回时,它将有一个空的缓冲区来再次填充。这个操作将被重复 10 次。

如果你执行这个示例,你会看到生产者和消费者如何并发地完成他们的工作,以及两个对象在每一步中如何交换它们的缓冲区。正如其他同步工具所发生的那样,第一个调用exchange()方法的线程将被置于休眠状态,直到其他线程到达。

还有更多...

Exchanger类有一个exchange方法的另一个版本:exchange(V data, long time, TimeUnit unit)。其中,V是用于Phaser声明(在我们的情况下是List<String>)的参数类型。线程将休眠,直到它被中断,另一个线程到达,或者指定的时长过去。在这种情况下,将抛出TimeoutExceptionTimeUnit类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

异步完成和链接任务

Java 8 并发 API 通过CompletableFuture类引入了一种新的同步机制。这个类实现了Future对象和CompletionStage接口,使其具有以下两个特性:

  • 作为Future对象,CompletableFuture对象将在未来某个时间返回一个结果

  • 作为CompletionStage对象,你可以在一个或多个CompletableFuture对象完成之后执行更多的异步任务

你可以用不同的方式与CompletableFuture类一起工作:

  • 你可以显式创建一个CompletableFuture对象,并将其用作任务之间的同步点。一个任务将使用complete()方法设置CompletableFuture返回的值,而其他任务将使用get()join()方法等待这个值。

  • 你可以使用CompletableFuture类的静态方法使用runAsync()supplyAsync()方法来执行RunnableSupplier。这些方法将返回一个CompletableFuture对象,当这些任务完成执行时,该对象将被完成。在第二种情况下,Supplier返回的值将是CompletableFuture的完成值。

  • 你可以在一个或多个CompletableFuture对象完成之后,异步地指定其他要执行的任务。这个任务可以实现RunnableFunctionConsumerBiConsumer接口。

这些特性使得CompletableFuture类非常灵活和强大。在本章中,你将学习如何使用这个类来组织不同的任务。示例的主要目的是任务将按照以下图示执行:

图片

首先,我们将创建一个生成种子的任务。使用这个种子,下一个任务将生成一组随机数字。然后,我们将执行三个并行任务:

  1. 第一步将在一组随机数字中计算出最接近 1,000 的数字。

  2. 第二步将在一组随机数字中计算出最大的数字。

  3. 第三步将在一组随机数字中计算出最大和最小数字之间的平均值。

准备工作

本示例的食谱已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 首先,我们将实现示例中将要使用的辅助任务。创建一个名为SeedGenerator的类,该类实现了Runnable接口。它将有一个CompletableFuture对象作为属性,并在类的构造函数中初始化:
        public class SeedGenerator implements Runnable { 

          private CompletableFuture<Integer> resultCommunicator; 

          public SeedGenerator (CompletableFuture<Integer> completable) { 
            this.resultCommunicator=completable; 
          }

  1. 然后,实现run()方法。它将使当前线程休眠 5 秒(以模拟长时间操作),计算 1 到 10 之间的随机数,然后使用resultCommunicator对象的complete()方法来完成CompletableFuture
        @Override 
        public void run() { 

          System.out.printf("SeedGenerator: Generating seed...\n"); 
          // Wait 5 seconds 
          try { 
            TimeUnit.SECONDS.sleep(5); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
            int seed=(int) Math.rint(Math.random() * 10); 

            System.out.printf("SeedGenerator: Seed generated: %d\n",
                              seed); 

            resultCommunicator.complete(seed); 
          }

  1. 创建一个名为NumberListGenerator的类,该类实现了以List<Long>数据类型参数化的Supplier接口。这意味着由Supplier接口提供的get()方法将返回一个包含大数字的列表。这个类将有一个整数作为私有属性,该属性将在类的构造函数中初始化:
        public class NumberListGenerator implements Supplier<List<Long>> { 

          private final int size; 

          public NumberListGenerator (int size) { 
            this.size=size; 
          }

  1. 然后,实现get()方法,该方法将返回一个包含数百万个数字的列表,如较大随机数字的大小参数所指定:
          @Override 
          public List<Long> get() { 
            List<Long> ret = new ArrayList<>(); 
            System.out.printf("%s : NumberListGenerator : Start\n",
                              Thread.currentThread().getName()); 

            for (int i=0; i< size*1000000; i++) { 
              long number=Math.round(Math.random()*Long.MAX_VALUE); 
              ret.add(number); 
            } 
            System.out.printf("%s : NumberListGenerator : End\n",
                              Thread.currentThread().getName()); 

            return ret; 
          }

  1. 最后,创建一个名为NumberSelector的类,该类实现了以List<Long>Long数据类型参数化的Function接口。这意味着由Function接口提供的apply()方法将接收一个包含大数字的列表,并将返回一个Long数字:
        public class NumberSelector implements Function<List<Long>, Long> { 

          @Override 
          public Long apply(List<Long> list) { 

            System.out.printf("%s: Step 3: Start\n",
                              Thread.currentThread().getName()); 
            long max=list.stream().max(Long::compare).get(); 
            long min=list.stream().min(Long::compare).get(); 
            long result=(max+min)/2; 
            System.out.printf("%s: Step 3: Result - %d\n",
                              Thread.currentThread().getName(), result); 
            return result; 
          } 
        }

  1. 现在是时候实现Main类和main()方法了:
        public class Main { 
          public static void main(String[] args) {

  1. 首先,创建一个CompletableFuture对象和一个SeedGenerator任务,并将其作为一个Thread执行:
        System.out.printf("Main: Start\n"); 
        CompletableFuture<Integer> seedFuture = new CompletableFuture<>(); 
        Thread seedThread = new Thread(new SeedGenerator(seedFuture)); 
        seedThread.start();

  1. 然后,使用CompletableFuture对象的get()方法等待由SeedGenerator任务生成的种子:
        System.out.printf("Main: Getting the seed\n"); 
        int seed = 0; 
        try { 
          seed = seedFuture.get(); 
        } catch (InterruptedException | ExecutionException e) { 
          e.printStackTrace(); 
        } 
        System.out.printf("Main: The seed is: %d\n", seed);

  1. 现在创建另一个CompletableFuture对象来控制NumberListGenerator任务的执行,但在这个情况下,使用静态方法supplyAsync()
        System.out.printf("Main: Launching the list of numbers
                           generator\n"); 
        NumberListGenerator task = new NumberListGenerator(seed); 
        CompletableFuture<List<Long>> startFuture = CompletableFuture
                                                    .supplyAsync(task);

  1. 然后,配置三个并行化的任务,这些任务将基于前一个任务生成的数字列表进行计算。这三个步骤不能开始执行,直到NumberListGenerator任务完成其执行,因此我们使用前一步生成的CompletableFuture对象和thenApplyAsync()方法来配置这些任务。前两个步骤以函数式的方式实现,第三个步骤是一个NumberSelector类的对象:
        System.out.printf("Main: Launching step 1\n"); 
        CompletableFuture<Long> step1Future = startFuture
                                              .thenApplyAsync(list -> { 
          System.out.printf("%s: Step 1: Start\n",
                            Thread.currentThread().getName()); 
          long selected = 0; 
          long selectedDistance = Long.MAX_VALUE; 
          long distance; 
          for (Long number : list) { 
            distance = Math.abs(number - 1000); 
            if (distance < selectedDistance) { 
              selected = number; 
              selectedDistance = distance; 
            } 
          } 
          System.out.printf("%s: Step 1: Result - %d\n",
                            Thread.currentThread().getName(), selected); 
          return selected; 
        }); 

        System.out.printf("Main: Launching step 2\n"); 
        CompletableFuture<Long> step2Future = startFuture 
        .thenApplyAsync(list -> list.stream().max(Long::compare).get()); 

        CompletableFuture<Void> write2Future = step2Future
                                              .thenAccept(selected -> { 
          System.out.printf("%s: Step 2: Result - %d\n",
                            Thread.currentThread().getName(), selected); 
        }); 

        System.out.printf("Main: Launching step 3\n"); 
        NumberSelector numberSelector = new NumberSelector(); 
        CompletableFuture<Long> step3Future = startFuture
                                        .thenApplyAsync(numberSelector);

  1. 我们使用CompletableFuture类的allOf()静态方法等待三个并行步骤的最终完成:
        System.out.printf("Main: Waiting for the end of the three
                           steps\n"); 
        CompletableFuture<Void> waitFuture = CompletableFuture
                                      .allOf(step1Future, write2Future,
                                             step3Future);

  1. 此外,我们执行一个最终步骤,在控制台写入一条消息:
        CompletableFuture<Void> finalFuture = waitFuture
                                           .thenAcceptAsync((param) -> { 
          System.out.printf("Main: The CompletableFuture example has
                             been completed."); 
        }); 
        finalFuture.join();

它是如何工作的...

我们可以使用一个CompletableFuture对象实现两个主要目的:

  • 等待未来将产生的值或事件(创建一个对象并使用complete()get()join()方法)。

  • 为了按确定顺序执行一系列任务,确保一个或多个任务不会在其它任务完成执行之前开始执行。

在这个例子中,我们使用了CompletableFuture类的两种用法。首先,我们创建了这个类的实例,并将其作为参数发送给一个SeedGenerator任务。这个任务使用complete()方法发送计算值,而main()方法使用get()方法获取值。get()方法将当前线程挂起,直到CompletableFuture完成。

然后,我们使用了supplyAsync()方法来生成一个CompletableFuture对象。此方法接收一个Supplier接口的实现作为参数。该接口提供了一个必须返回值的get()方法。supplyAsync()方法返回CompletableFuture,当get()方法完成执行时将完成;完成值是该方法返回的值。返回的CompletableFuture对象将由ForkJoinPool中的任务执行,该任务返回静态方法commonPool()

然后,我们使用了thenApplyAsync()方法来链接一些任务。你需要在CompletableFuture对象中调用此方法,并且必须传递一个实现了Function接口的实现作为参数,该参数可以直接使用函数式风格或独立对象在代码中表达。一个强大的特性是,由CompletableFuture生成的值将被传递给Function作为参数。也就是说,在我们的情况下,所有三个步骤都将接收一个随机数字列表作为参数。返回的CompletableFuture类将由ForkJoinPool中的任务执行,该任务返回静态方法commonPool()

最后,我们使用了CompletableFuture类的allOf()静态方法来等待各种任务的最终化。此方法接收一个可变数量的CompletableFuture对象列表,并返回一个CompletableFuture类,当所有作为参数传递的CompletableFuture类完成时,它将被完成。我们还使用了thenAcceptAsync()方法作为同步任务的另一种方式,因为此方法接收Consumer作为参数,当使用CompletableFuture对象调用该方法时,默认执行器将执行它。最后,我们使用了join()方法来等待最后一个CompletableFuture对象的最终化。

以下截图显示了示例的执行。你可以看到任务是如何按照我们组织的顺序执行的:

更多...

在这个食谱的示例中,我们使用了CompletableFuture类的complete()get()join()supplyAsync()thenApplyAsync()thenAcceptAsync()allOf()方法。然而,这个类有很多有用的方法,有助于提高这个类的功能和灵活性。这些是最有趣的:

  • 完成完成CompletableFuture对象的方法:除了complete()方法之外,CompletableFuture类还提供了以下三个方法:

    • cancel(): 这个方法使用CancellationException异常来完成CompletableFuture

    • completeAsync(): 这个方法使用作为参数传递的Supplier对象的结果来完成CompletableFuture。默认情况下,Supplier对象会在不同的线程中由执行器执行。

    • completeExceptionally(): 这个方法使用作为参数传递的异常来完成CompletableFuture

  • 执行任务的方法:除了supplyAsync()方法之外,CompletableFuture类还提供了以下方法:

    • runAsync(): 这是CompletableFuture类的一个静态方法,它返回一个CompletableFuture对象。当将Runnable接口作为参数传递以完成其执行时,此对象将被完成。它将以空结果完成。
  • 同步不同任务执行的方法:除了 allOf()thenAcceptAsync()thenApplyAsync() 方法外,CompletableFuture 类还提供了以下方法来同步任务的执行:

    • anyOf(): 这是 CompletableFuture 类的静态方法。它接收一个 CompletableFuture 对象的列表,并返回一个新的 CompletableFuture 对象。此对象将使用第一个完成的 CompletableFuture 参数的结果来完成。

    • runAfterBothAsync(): 这个方法接收 CompletionStageRunnable 对象作为参数,并返回一个新的 CompletableFuture 对象。当 CompletableFuture(执行调用)和 CompletionStage(作为参数接收)完成时,Runnable 对象将由默认执行器执行,然后返回的 CompletableFuture 对象完成。

    • runAfterEitherAsync(): 这个方法与上一个方法类似,但在这里,Runnable 接口在两个(CompletableFutureCompletionStage)中的任何一个完成之后执行。

    • thenAcceptBothAsync(): 这个方法接收 CompletionStageBiConsumer 对象作为参数,并返回 CompetableFuture 作为参数。当 CompletableFuture(执行调用)和 CompletionStage(作为参数传递),BiConsumer 由默认执行器执行。它接收两个 CompletionStage 对象的结果作为参数,但它不会返回任何结果。当 BiConsumer 完成其执行时,返回的 CompletableFuture 类将完成,但没有结果。

    • thenCombineAsync(): 这个方法接收一个 CompletionStage 对象和一个 BiFunction 对象作为参数,并返回一个新的 CompletableFuture 对象。当 CompletableFuture(执行调用)和 CompletionStage(作为参数传递)完成时,BiFunction 对象将被执行;它接收两个对象的完成值,并返回一个新的结果,该结果将成为返回的 CompletableFuture 类的完成值。

    • thenComposeAsync(): 这个方法类似于 thenApplyAsync(),但当提供的函数也返回 CompletableFuture 时,它非常有用。

    • thenRunAsync(): 这个方法类似于 thenAcceptAsync() 方法,但在这个情况下,它接收一个 Runnable 对象作为参数,而不是 Consumer 对象。

  • 获取完成值的方法:除了 get()join() 方法外,CompletableFuture 对象还提供了以下方法来获取完成值:

    • getNow(): 这个方法接收与 CompletableFuture 完成值相同类型的值。如果对象已完成,它将返回完成值。否则,它将返回作为参数传递的值。

参见...

  • 创建线程执行器及其控制拒绝的任务在返回结果的执行器中执行任务 这两个配方在 第四章,线程执行器 中介绍。

第四章:线程执行器

在本章中,我们将涵盖以下主题:

  • 创建线程执行器并控制其拒绝的任务

  • 在执行器中执行返回结果的任务

  • 运行多个任务并处理第一个结果

  • 运行多个任务并处理所有结果

  • 在延迟后运行执行器中的任务

  • 定期在执行器中运行任务

  • 在执行器中取消任务

  • 控制执行器中任务完成

  • 在执行器中分离任务的启动和结果处理

简介

通常,当你用 Java 开发一个简单的并发编程应用程序时,首先创建一些Runnable对象,然后创建相应的Thread对象来执行它们。如果你必须开发一个运行大量并发任务的应用程序,这种方法将带来以下缺点:

  • 你将不得不实现所有与代码相关的信息来管理Thread对象(创建、结束和获取结果)。

  • 你将不得不为每个任务创建一个Thread对象。执行大量任务可能会影响应用程序的吞吐量。

  • 你将需要有效地控制和管理工作站的资源。如果你创建太多线程,可能会使系统饱和。

自 Java 5 以来,Java 并发 API 提供了一种旨在解决这些问题的机制。这个机制被称为执行器框架,它围绕Executor接口、其子接口ExecutorService以及实现这两个接口的ThreadPoolExecutor类。

此机制将任务创建和执行分离。使用执行器,你只需实现RunnableCallable对象并将它们发送到执行器。它负责它们的执行,使用必要的线程运行它们。但不仅如此;它通过线程池来提高性能。当你向执行器发送任务时,它会尝试使用池中的线程来执行任务。这样做是为了避免不断创建线程。Executor框架的另一个重要优点是Callable接口。它与Runnable接口类似,但提供了两个改进,具体如下:

  • 该接口的主要方法名为call(),可能返回一个结果。

  • 当你向执行器发送Callable对象时,你会得到一个实现了Future接口的对象。你可以使用此对象来控制Callable对象的状态和结果。

本章介绍了九个菜谱,展示了如何使用前面提到的类和其他 Java 并发 API 提供的变体来使用Executor框架。

创建线程执行器并控制其拒绝的任务

使用Executor框架的第一步是创建ThreadPoolExecutor类的对象。你可以使用这个类提供的四个构造函数,或者使用名为Executors的工厂类来创建ThreadPoolExecutor。一旦你有了执行器,你就可以发送RunnableCallable对象去执行。

当你想要完成执行器的执行时,使用shutdown()方法。执行器会等待正在运行或等待执行的任务完成。然后,它完成执行。

如果你在一个执行器的shutdown()方法和执行结束之间发送一个任务,该任务将被拒绝。这是因为执行器不再接受新的任务。ThreadPoolExecutor类提供了一个机制,当任务被拒绝时会被调用。

在这个菜谱中,你将学习如何使用Executors类创建一个新的ThreadPoolExecutor对象,如何向Executor发送任务,以及如何控制Executor类的拒绝任务。

准备工作

你应该阅读第一章中关于创建、运行和设置线程特性的菜谱,学习 Java 中线程创建的基本机制。你可以比较这两种机制,并根据问题选择一个。

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 首先,实现服务器将要执行的任务。创建一个名为Task的类,该类实现了Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为initDateDate属性来存储任务的创建日期,以及一个名为nameString属性来存储任务的名称:
        private final Date initDate; 
        private final String name;

  1. 实现类的构造函数,初始化两个属性:
        public Task(String name){ 
          initDate=new Date(); 
          this.name=name; 
        }

  1. 实现类的run()方法:
        @Override 
        public void run() {

  1. 首先,写入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());

  1. 然后,让任务随机休眠一段时间:
        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(); 
        }

  1. 最后,将任务的完成日期写入控制台:
         System.out.printf("%s: Task %s: Finished on: %s\n",
                           Thread.currentThread().getName(),
                           name,new Date());

  1. 创建一个名为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()); 
          }

  1. 现在实现Server类,该类将使用执行器执行它接收到的每个任务。创建一个名为Server的类:
        public class Server {

  1. 声明一个名为executorThreadPoolExecutor属性:
        private final ThreadPoolExecutor executor;

  1. 实现类的构造函数,使用Executors类初始化ThreadPoolExecutor对象,并设置拒绝任务的处理器:
        public Server(){ 
          executor =( ThreadPoolExecutor ) Executors.newFixedThreadPool(
                        Runtime.getRuntime().availableProcessors() ); 
          RejectedTaskController controller=new
                                         RejectedTaskController(); 
          executor.setRejectedExecutionHandler(controller); 
        }

  1. 实现executeTask()方法。它接收一个Task对象作为参数并将其发送到执行器。首先,向控制台写入一条消息,表明已到达一个新的任务:
        public void executeTask(Task task){ 
          System.out.printf("Server: A new task has arrived\n");

  1. 然后,调用执行器的execute()方法并将其发送到任务:
        executor.execute(task);

  1. 最后,将一些执行器数据写入控制台以查看其状态:
        System.out.printf("Server: Pool Size: %d\n",
                          executor.getPoolSize()); 
        System.out.printf("Server: Active Count: %d\n",
                          executor.getActiveCount()); 
        System.out.printf("Server: Task Count: %d\n",
                          executor.getTaskCount()); 
        System.out.printf("Server: Completed Tasks: %d\n",
                          executor.getCompletedTaskCount());

  1. 接下来,实现endServer()方法。在此方法中,调用执行器的shutdown()方法来完成其执行:
        public void endServer() { 
          executor.shutdown(); 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类。首先,创建 100 个任务并将它们发送到Executor
        public class Main { 
          public static void main(String[] args) { 
            Server server=new Server(); 

            System.out.printf("Main: Starting.\n"); 
              for (int i=0; i<100; i++){ 
                Task task=new Task("Task "+i); 
                server.executeTask(task); 
              }

  1. 然后,调用ServerendServer()方法来关闭执行器:
        System.out.printf("Main: Shuting down the Executor.\n"); 
        server.endServer();

  1. 最后,发送一个新的任务。此任务将被拒绝,因此我们将看到这个机制是如何工作的:
        System.out.printf("Main: Sending another Task.\n"); 
        Task task=new Task("Rejected task"); 
        server.executeTask(task); 

        System.out.printf("Main: End.\n");

它是如何工作的...

本例的关键是Server类。此类创建并使用ThreadPoolExecutor来执行任务。

第一个重要点是Server类的构造函数中创建ThreadPoolExecutorThreadPoolExecutor类有四个不同的构造函数,但由于它们的复杂性,Java 并发 API 提供了Executors类来构建执行器和相关对象。虽然您可以直接使用其构造函数之一创建ThreadPoolExecutor,但建议您使用Executors类。

在这种情况下,您使用Executors类的newFixedThreadPool()方法创建了一个缓存线程池来创建执行器。此方法创建了一个具有最大线程数的执行器。如果您发送的任务数量超过线程数,剩余的任务将阻塞,直到有可用的空闲线程来处理它们。此方法接收您希望执行器中拥有的最大线程数作为参数。在我们的例子中,我们使用了Runtime类的availableProcessors()方法,它返回 JVM 可用的处理器数量。通常,这个数字与计算机的核心数相匹配。

线程的重用具有减少线程创建时间的优势。然而,缓存线程池的缺点是对于新任务有恒定的空闲线程。因此,如果您向此执行器发送过多的任务,可能会超载系统。

一旦创建了执行器,您可以使用execute()方法发送RunnableCallable类型的任务进行执行。在这种情况下,您发送了实现了Runnable接口的Task类的对象。

您还打印了一些包含执行器信息的日志消息。具体来说,您使用了以下方法:

  • getPoolSize(): 此方法返回执行器池中的实际线程数。

  • getActiveCount(): 此方法返回在执行器中执行任务的线程数量。

  • getTaskCount(): 此方法返回已安排执行的任务数量。返回的值仅是一个近似值,因为它会动态变化。

  • getCompletedTaskCount(): 此方法返回执行器完成的任务数量。

ThreadPoolExecutor 类以及一般执行器的一个关键方面是,你必须显式地结束它们。如果你不这样做,执行器将继续执行,程序将不会结束。如果执行器没有要执行的任务,它将继续等待新任务,并且不会结束其执行。Java 应用程序不会结束,直到所有非守护线程完成它们的执行。因此,如果你不终止执行器,你的应用程序将永远不会结束。

要向执行器指示你想要结束它,请使用 ThreadPoolExecutor 类的 shutdown() 方法。当执行器完成所有挂起任务的执行后,它也会结束其执行。在你调用 shutdown() 方法后,如果你尝试向执行器发送另一个任务,它将被拒绝,并且执行器将抛出 RejectedExecutionException 异常,除非你已经实现了拒绝任务的经理,就像在我们的案例中一样。要管理执行器的拒绝任务,你需要创建一个实现 RejectedExecutionHandler 接口的类。此接口有一个名为 rejectedExecution() 的方法,它有两个参数:

  • 存储已拒绝的任务的 Runnable 对象

  • 存储拒绝任务的执行器的 Executor 对象

对于执行器拒绝的每个任务,都会调用此方法。你需要使用 ThreadPoolExecutor 类的 setRejectedExecutionHandler() 方法来建立拒绝任务的处理器。

以下截图显示了此示例执行的部分:

图片

注意,当最后一个任务到达执行器时,池中的线程数和正在执行的线程数都表示为 4。这指的是执行示例的 PC 的核心数,这是 availableProcessors() 方法返回的数值。一旦完成,我们关闭执行器,下一个任务将被拒绝。RejectedTaskController 将任务和执行器的信息写入控制台。

还有更多...

Executors 类提供了其他方法来创建 ThreadPoolExecutor

  • newCachedThreadPool(): 此方法返回一个 ExecutorService 对象,因此它已被转换为 ThreadPoolExecutor 以访问其所有方法。你创建的缓存线程池在需要时创建新线程来执行新任务。此外,如果它们已经完成了正在运行的任务的执行,它将重用它们。

  • newSingleThreadExecutor(): 这是一个固定大小线程执行器的极端情况。它创建一个只有一个线程的执行器,因此一次只能执行一个任务。

ThreadPoolExecutor 类提供了许多方法来获取有关其状态的信息。我们在示例中使用了 getPoolSize()getActiveCount()getCompletedTaskCount() 方法来获取有关池大小、线程数和执行器完成任务的数量的信息。您还可以使用 getLargestPoolSize() 方法;它返回一次在池中达到的最大线程数。

ThreadPoolExecutor 类还提供了与执行器最终化相关的一些其他方法。这些方法包括:

  • shutdownNow(): 这将立即关闭执行器。它不会执行挂起的任务。它返回一个包含所有挂起任务的列表。当您调用此方法时正在运行的任务将继续执行,但该方法不会等待它们的最终化。

  • isTerminated(): 此方法返回 true,如果您调用了 shutdown()shutdownNow() 方法;执行器将相应地完成关闭过程。

  • isShutdown(): 如果您调用了执行器的 shutdown() 方法,此方法返回 true

  • awaitTermination(long timeout, TimeUnit unit): 此方法阻塞调用线程,直到执行器的任务结束或发生超时。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

如果您想等待任务完成,无论它们的持续时间如何,请使用大超时,例如,DAYS

参见

  • 在第九章 “监控 Executor 框架”配方,测试并发应用程序

在返回结果的执行器中执行任务

Executor 框架的一个优点是它允许您运行返回结果的并发任务。Java 并发 API 通过以下两个接口实现这一点:

  • Callable:此接口有一个 call() 方法。在此方法中,您必须实现任务的逻辑。Callable 接口是一个参数化接口,这意味着您必须指明 call() 方法将返回的数据类型。

  • Future:此接口有一些方法可以获取 Callable 对象生成的结果并管理其状态。

在本配方中,您将学习如何实现返回结果的任务并在执行器上运行它们。

准备工作

本配方的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为 FactorialCalculator 的类。指定它实现由 Integer 类型参数化的 Callable 接口:
        public class FactorialCalculator implements Callable<Integer> {

  1. 声明一个名为 number 的私有 Integer 属性,用于存储此任务将用于其计算的数字:
        private final Integer number;

  1. 实现类的构造函数,初始化类的属性:
        public FactorialCalculator(Integer number){ 
          this.number=number; 
        }

  1. 实现call()方法。此方法返回FactorialCalculator的数字属性的阶乘:
        @Override 
        public Integer call() throws Exception {

  1. 首先,创建并初始化方法中使用的内部变量:
        int result = 1;

  1. 如果数字是01,则返回1。否则,计算数字的阶乘。为了教育目的,在两次乘法之间让这个任务休眠 20 毫秒:
        if ((number==0)||(number==1)) { 
          result=1; 
        } else { 
          for (int i=2; i<=number; i++) { 
            result*=i; 
            TimeUnit.MILLISECONDS.sleep(20); 
          } 
        }

  1. 使用操作的结果向控制台发送一条消息:
        System.out.printf("%s: %d\n",Thread.currentThread().getName(),
                          result);

  1. 返回操作的结果:
        return result;

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用Executors类的newFixedThreadPool()方法创建ThreadPoolExecutor来运行任务。将2作为参数传递,即作为执行器中的线程数:
        ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors
                                               .newFixedThreadPool(2);

  1. 创建一个Future<Integer>对象的列表:
        List<Future<Integer>> resultList=new ArrayList<>();

  1. 使用Random类创建一个随机数生成器:
        Random random=new Random();

  1. 创建一个包含十个步骤的循环。在每一步中,我们生成一个随机数:
        for (int i=0; i<10; i++){ 
          Integer number= random.nextInt(10);

  1. 然后,我们创建一个FactorialCalculator对象,将生成的随机数作为参数传递:
        FactorialCalculator calculator=new FactorialCalculator(number);

  1. 调用执行器的submit()方法将FactorialCalculator任务发送到执行器。此方法返回一个Future<Integer>对象来管理任务,并最终获取其结果:
        Future<Integer> result=executor.submit(calculator);

  1. Future对象添加到之前创建的列表中:
          resultList.add(result); 
        }

  1. 创建一个do循环来监控执行器的状态:
        do {

  1. 首先,使用执行器的getCompletedTaskNumber()方法向控制台发送一条消息,指示已完成任务的数目:
        System.out.printf("Main: Number of Completed Tasks: %d\n",
                          executor.getCompletedTaskCount());

  1. 然后,对于列表中的 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()); 
        }

  1. 让线程休眠 50 毫秒:
        try { 
          TimeUnit.MILLISECONDS.sleep(50); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 当执行器的已完成任务数少于 10 时,重复此循环:
        } while (executor.getCompletedTaskCount()<resultList.size());

  1. 在控制台,写出每个任务获得的结果。对于每个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(); 
          }

  1. 然后,将数字打印到控制台:
          System.out.printf("Main: Task %d: %d\n",i,number); 
        }

  1. 最后,调用执行器的shutdown()方法来最终化其执行:
        executor.shutdown();

它是如何工作的...

在这个菜谱中,你学习了如何使用Callable接口启动返回结果的并发任务。你实现了FactorialCalculator类,该类实现了Callable接口,并将Integer作为结果类型。因此,call()方法返回一个Integer值。

此例中的另一个关键点是Main类。你使用submit()方法向执行器发送了一个要执行的对象。此方法接收一个Callable对象作为参数,并返回一个Future对象,你可以用它实现两个主要目标:

  • 你可以控制任务的状况,你可以取消任务并检查它是否已经完成。为此,你使用了isDone()方法。

  • 您可以获取 call() 方法返回的结果。为此,您使用了 get() 方法。此方法等待 Callable 对象完成 call() 方法的执行并返回其结果。如果在 get() 方法等待结果时线程被中断,它将抛出一个 InterruptedException 异常。如果 call() 方法抛出异常,则 get() 方法也会抛出一个 ExecutionException 异常。

更多内容...

当您调用 Future 对象的 get() 方法,并且由该对象控制的任务尚未完成时,该方法将被阻塞,直到任务完成。Future 接口提供了 get() 方法的另一个版本:

  • get(long timeout, TimeUnit unit): 这个版本的 get 方法,如果任务的输出不可用,将等待指定的时间。如果在指定的时间内过去,结果仍然不可用,它将抛出一个 TimeoutException 异常。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

参见

  • 本章中关于创建线程执行器并控制其拒绝的任务运行多个任务并处理第一个结果以及运行多个任务并处理所有结果的食谱

运行多个任务并处理第一个结果

在并发编程中,当您有各种并发任务可供解决问题,但您只对第一个结果感兴趣时,会出现一个常见问题。例如,您想对一个数组进行排序。您有多种排序算法。您可以启动所有这些算法,并获取第一个排序数组的算法的结果,即给定数组的最快排序算法。

在本食谱中,您将学习如何使用 ThreadPoolExecutor 类实现此场景。您将使用两种机制来尝试验证用户。如果其中一种机制能够验证用户,则用户将被验证。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 UserValidator 的类,该类将实现用户验证的过程:
        public class UserValidator {

  1. 声明一个名为 name 的私有 String 属性,该属性将存储用户验证系统的名称:
        private final String name;

  1. 实现类的构造函数,初始化其属性:
        public UserValidator(String name) { 
          this.name=name; 
        }

  1. 实现名为 validate() 的方法。它接收两个 String 参数,即您想要验证的用户的名字和密码:
        public boolean validate(String name, String password) {

  1. 创建一个名为 randomRandom 对象:
        Random random=new Random();

  1. 等待一个随机的时间段以模拟用户验证的过程:
        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; 
        }

  1. 返回一个随机的 Boolean 值。当用户被验证时,validate() 方法返回 true,否则返回 false
          return random.nextBoolean(); 
        }

  1. 实现getName()方法。此方法返回名称属性的值:
        public String getName(){ 
          return name; 
        }

  1. 现在,创建一个名为ValidatorTask的类,该类将以UserValidation对象作为并发任务执行验证过程。指定它实现了由String类参数化的Callable接口:
        public class ValidatorTask implements Callable<String> {

  1. 声明一个名为validator的私有UserValidator属性:
        private final UserValidator validator;

  1. 声明两个名为userpassword的私有String属性:
        private final String user; 
        private final String password;

  1. 实现类的构造函数,该构造函数将初始化所有属性:
        public ValidatorTask(UserValidator validator, String user,
                             String password){ 
          this.validator=validator; 
          this.user=user; 
          this.password=password; 
        }

  1. 实现将返回String对象的call()方法:
        @Override 
        public String call() throws Exception {

  1. 如果用户没有被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"); 
        }

  1. 否则,向控制台发送一条消息,表明用户已验证,并返回UserValidator对象的名称:
        System.out.printf("%s: The user has been found\n",
                          validator.getName()); 
        return validator.getName();

  1. 现在通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建两个名为userpasswordString对象,并用测试值初始化它们:
        String username="test"; 
        String password="test";

  1. 创建两个名为ldapValidatordbValidatorUserValidator对象:
        UserValidator ldapValidator=new UserValidator("LDAP"); 
        UserValidator dbValidator=new UserValidator("DataBase");

  1. 创建两个名为ldapTaskdbTaskTaskValidator对象。分别用ldapValidatordbValidator初始化它们:
        TaskValidator ldapTask=new TaskValidator(ldapValidator,
                                                 username, password); 
        TaskValidator dbTask=new TaskValidator(dbValidator,
                                               username,password);

  1. 创建一个TaskValidator对象列表,并将你创建的两个对象添加到其中:
        List<TaskValidator> taskList=new ArrayList<>(); 
        taskList.add(ldapTask); 
        taskList.add(dbTask);

  1. 使用Executors类的newCachedThreadPool()方法和一个名为result的字符串变量创建一个新的ThreadPoolExecutor对象:
        ExecutorService executor=(ExecutorService)Executors
                                             .newCachedThreadPool(); 
        String result;

  1. 调用执行器对象的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(); 
        }

  1. 使用shutdown()方法终止执行器,并向控制台发送一条消息,表明程序已结束:
        executor.shutdown(); 
        System.out.printf("Main: End of the Execution\n");

它是如何工作的...

示例的关键在Main类中。ThreadPoolExecutor类的invokeAny()方法接收一个任务列表,然后启动它们,并返回第一个完成且未抛出异常的任务的结果。此方法返回与任务call()方法返回的数据类型相同。在这种情况下,它返回了一个String值。

以下截图显示了当其中一个任务验证用户时的示例执行输出:

示例有两个返回随机布尔值的UserValidator对象。每个UserValidator对象由TaskValidator类实现的Callable对象使用。如果UserValidator类的validate()方法返回一个假值,则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类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

参见

  • 本章的运行多个任务并处理所有结果菜谱

运行多个任务并处理所有结果

Executor框架允许你在不担心线程创建和执行的情况下执行并发任务。它为你提供了Future类,你可以使用这个类来控制任务的状态并获取在执行器中执行的任务的结果。

当你想要等待一个任务的最终完成时,你可以使用以下两种方法:

  • Future接口的isDone()方法如果任务已完成其执行,则返回true

  • ThreadPoolExecutor类的awaitTermination()方法在调用shutdown()方法后,将线程置于休眠状态,直到所有任务完成执行。

这两种方法有一些缺点。使用第一种方法,你只能控制任务的完成。使用第二种方法,你必须关闭执行器以等待线程;否则,方法的调用将立即返回。

ThreadPoolExecutor类提供了一个方法,允许你向执行器发送一个任务列表,并等待列表中所有任务的最终完成。在这个菜谱中,你将通过实现一个包含 10 个任务执行并打印它们完成后的结果的示例来学习如何使用这个功能。

准备工作

这个菜谱的示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为Result的类来存储这个示例中并发任务生成的结果:
        public class Result {

  1. 声明两个私有属性,即一个名为nameString属性和一个名为valueint属性:
        private String name; 
        private int value;

  1. 实现相应的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; 
        }

  1. 创建一个名为Task的类,该类实现了由Result类参数化的Callable接口:
        public class Task implements Callable<Result> {

  1. 声明一个名为name的私有String属性:
        private final String name;

  1. 实现类的构造函数以初始化其属性:
        public Task(String name) { 
          this.name=name; 
        }

  1. 实现类的call()方法。在这种情况下,该方法将返回一个Result对象:
        @Override 
        public Result call() throws Exception {

  1. 首先,向控制台发送一条消息以指示任务开始:
        System.out.printf("%s: Staring\n",this.name);

  1. 然后,等待一个随机的时间段:
        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(); 
        }

  1. 为了在Result对象中返回一个int值,计算五个随机数的总和:
        int value=0; 
        for (int i=0; i<5; i++){ 
          value+=(int)(Math.random()*100); 
        }

  1. 创建一个Result对象并用此Task对象的名称和之前执行的操作的结果初始化它:
        Result result=new Result(); 
        result.setName(this.name); 
        result.setValue(value);

  1. 向控制台发送一条消息以指示任务已完成:
        System.out.println(this.name+": Ends");

  1. 返回Result对象:
          return result; 
        }

  1. 最后,通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 使用Executors类的newCachedThreadPool()方法创建一个ThreadPoolExecutor对象:
        ExecutorService executor=(ExecutorService)Executors
                                               .newCachedThreadPool();

  1. 创建一个Task对象的列表。创建 10 个Task对象并将它们保存在这个列表中:
        List<Task> taskList=new ArrayList<>(); 
        for (int i=0; i<10; i++){ 
          Task task=new Task("Task-"+i); 
          taskList.add(task); 
        }

  1. 创建一个Future对象列表。这些对象由Result类参数化:
        List<Future<Result>>resultList=null;

  1. 调用ThreadPoolExecutor类的invokeAll()方法。这个类将返回之前创建的Future对象列表:
        try { 
          resultList=executor.invokeAll(taskList); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用shutdown()方法终止执行器:
        executor.shutdown();

  1. 将处理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 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

参考以下内容

  • 本章中的 在返回结果的执行器中执行任务运行多个任务并处理第一个结果 菜单

在延迟后运行任务

Executor 框架提供了 ThreadPoolExecutor 类,用于使用线程池执行 CallableRunnable 任务,这有助于您避免所有线程创建操作。当您向执行器发送任务时,它会根据执行器的配置尽可能快地执行。在某些情况下,您可能不希望立即执行任务。您可能希望在一段时间后执行任务或定期执行。为此,Executor 框架提供了 ScheduledExecutorService 接口及其实现,即 ScheduledThreadPoolExecutor 类。

在本菜谱中,您将学习如何创建 ScheduledThreadPoolExecutor 并使用它来安排在给定时间后的任务执行。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Task 的类,该类实现了由 String 类参数化的 Callable 接口:
        public class Task implements Callable<String> {

  1. 声明一个名为 name 的私有 String 属性,用于存储任务的名称:
        private final String name;

  1. 实现类的构造函数,初始化名称属性:
        public Task(String name) { 
          this.name=name; 
        }

  1. 实现该 call() 方法。向控制台发送一条包含实际日期的消息,并返回一些文本,例如,Hello, world
        public String call() throws Exception { 
          System.out.printf("%s: Starting at : %s\n",name,new Date()); 
          return "Hello, world"; 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用 Executors 类的 newScheduledThreadPool() 方法创建一个 ScheduledThreadPoolExecutor 类型的执行器,将 1 作为参数传递:
        ScheduledExecutorService executor=Executors
                                           .newScheduledThreadPool(1);

  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); 
        }

  1. 使用 shutdown() 方法请求执行器的最终化:
        executor.shutdown();

  1. 使用执行器的 awaitTermination() 方法等待所有任务的最终化:
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 编写一条消息以指示程序完成的时间:
        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()方法更改此行为。如果您将setExecuteExistingDelayedTasksAfterShutdownsPolicy()方法调用为传递false作为参数,则在调用shutdown()方法后,挂起的任务将不会执行。

参见

  • 本章中关于“在返回结果的执行器中执行任务”的配方

在执行器中定期运行任务

执行器框架提供了ThreadPoolExecutor类,用于使用线程池执行并发任务,这有助于您避免所有线程创建操作。当您向执行器发送任务时,它会根据其配置尽可能快地执行任务。当任务结束时,任务将从执行器中删除,如果您想再次执行它,您必须再次将其发送到执行器。

然而,Executor 框架通过ScheduledThreadPoolExecutor类提供了执行周期性任务的可能性。在这个菜谱中,你将学习如何使用这个类的这个功能来安排周期性任务。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为Task的类,并指定它实现Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为name的私有String属性,该属性将存储任务的名称:
        private final String name;

  1. 实现类的构造函数,初始化属性:
        public Task(String name) { 
          this.name=name; 
        }

  1. 实现run()方法。向控制台写入实际日期的消息以验证任务是否在指定期间执行:
        @Override 
        public void run() { 
          System.out.printf("%s: Executed at: %s\n",name,new Date()); 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用Executors类的newScheduledThreadPool()方法创建ScheduledExecutorService。将1作为参数传递给这个方法:
        ScheduledExecutorService executor=Executors
                                           .newScheduledThreadPool(1);

  1. 向控制台写入实际日期的消息:
        System.out.printf("Main: Starting at: %s\n",new Date());

  1. 创建一个新的Task对象:
        Task task=new Task("Task");

  1. 使用scheduledAtFixRate()方法将此对象发送到 executor。使用之前创建的任务作为参数:第一个数字、第二个数字和TimeUnit.SECONDS常量。此方法返回一个ScheduledFuture对象,你可以使用它来控制任务的状态:
        ScheduledFuture<?> result=executor.scheduleAtFixedRate(task, 1,
                                                  2, TimeUnit.SECONDS);

  1. 使用 10 步循环来编写下一次任务执行剩余时间。在循环中,使用ScheduledFuture对象的getDelay()方法来获取下一次任务执行前的毫秒数:
        for (int i=0; i<10; i++){ 
          System.out.printf("Main: Delay: %d\n",result
                                     .getDelay(TimeUnit.MILLISECONDS));

  1. 线程休眠 500 毫秒。
          try { 
            TimeUnit.MILLISECONDS.sleep(500); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 使用shutdown()方法结束 executor:
        executor.shutdown();

  1. 让线程休眠 5 秒以验证周期性任务已完成:
        try { 
          TimeUnit.SECONDS.sleep(5); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 编写一条消息以指示程序结束:
        System.out.printf("Main: Finished at: %s\n",new Date());

它是如何工作的...

当你想使用 Executor 框架执行周期性任务时,你需要一个ScheduledExecutorService对象。为了创建它(就像创建每个 executor 一样),Java 推荐使用Executors类。这个类作为 executor 对象的工厂。在这种情况下,你使用了newScheduledThreadPool()方法来创建一个ScheduledExecutorService对象。这个方法接收池中线程的数量作为参数。由于在这个例子中只有一个任务,你传递了1作为参数。

一旦你有了执行周期性任务所需的执行器,你就将任务发送到执行器。你使用了scheduledAtFixedRate()方法。此方法接受四个参数:你想要周期性执行的任务、任务首次执行前的延迟时间、两次执行之间的周期,以及第二和第三个参数的时间单位。它是TimeUnit类的一个常量。TimeUnit类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

一个需要考虑的重要点是,两次执行之间的周期是这两个执行开始之间的时间间隔。如果你有一个执行时间为 5 秒的周期性任务,并且你设置了 3 秒的周期,你将同时有两个任务实例在执行。

scheduleAtFixedRate()方法返回一个ScheduledFuture对象,它扩展了Future接口,并具有处理计划任务的方法。ScheduledFuture是一个参数化接口。在这个例子中,由于你的任务是一个未参数化的Runnable对象,你必须使用?符号作为参数来参数化它们。

你使用了ScheduledFuture接口的一个方法。getDelay()方法返回任务下一次执行的时间。此方法接收一个TimeUnit常量,表示你想要接收结果的时间单位。

以下截图显示了示例执行的输出:

你可以看到任务每 2 秒执行一次(以 Task: 前缀表示)和每 500 毫秒在控制台写入的延迟。这就是主线程被休眠的时间。当你关闭执行器时,计划中的任务结束执行,你将不再在控制台看到任何消息。

更多...

ScheduledThreadPoolExecutor提供了其他方法来安排周期性任务。它是scheduleWithFixedRate()方法。它具有与scheduledAtFixedRate()方法相同的参数,但有一个值得注意的差异。在scheduledAtFixedRate()方法中,第三个参数确定两次执行开始之间的时间间隔。在scheduledWithFixedRate()方法中,参数确定任务执行结束和开始之间的时间间隔。

你还可以使用shutdown()方法配置ScheduledThreadPoolExecutor类的实例的行为。默认行为是在你调用此方法时,计划中的任务完成。你可以使用ScheduledThreadPoolExecutor类的setContinueExistingPeriodicTasksAfterShutdownPolicy()方法通过传递一个 true 值来更改此行为。周期性任务在调用shutdown()方法时不会结束。

参见

  • 本章中的创建线程执行器并控制其拒绝的任务在执行器中延迟运行任务的食谱

在执行器中取消任务

当您与执行器一起工作时,您不需要管理线程。您只需实现 RunnableCallable 任务并将它们发送到执行器。执行器负责创建线程、在线程池中管理它们,并在不需要时结束它们。有时,您可能想要取消发送到执行器的任务。在这种情况下,您可以使用 Futurecancel() 方法,这允许您执行取消操作。在本菜谱中,您将学习如何使用此方法取消发送到执行器的任务。

准备工作

此菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为 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); 
            } 
          }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用 Executors 类的 newCachedThreadPool() 方法创建一个 ThreadPoolExecutor 对象:
        ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors
                                               .newCachedThreadPool();

  1. 创建一个新的 Task 对象:
        Task task=new Task();

  1. 使用 submit() 方法将任务发送到执行器:
        System.out.printf("Main: Executing the Task\n"); 
        Future<String> result=executor.submit(task);

  1. 使主任务休眠 2 秒:
        try { 
          TimeUnit.SECONDS.sleep(2); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用 result 对象的 cancel() 方法取消任务的执行,result 是由 submit() 方法返回的 Future 对象。将 true 值作为 cancel() 方法的参数传递:
        System.out.printf("Main: Canceling the Task\n"); 
        result.cancel(true);

  1. 将对 isCancelled()isDone() 方法的调用结果写入控制台。这是为了验证任务已被取消,因此已经完成:
        System.out.printf("Main: Canceled: %s\n",result.isCancelled()); 
        System.out.printf("Main: Done: %s\n",result.isDone());

  1. 使用 shutdown() 方法结束执行器,并写入一条消息以指示程序的最终化:
        executor.shutdown(); 
        System.out.printf("Main: The executor has finished\n");

它是如何工作的...

当您想要取消发送到执行器的任务时,您使用 Future 接口的 cancel() 方法。根据 cancel() 方法的参数和任务的状态,此方法的行为不同:

  • 如果任务已完成或之前已被取消,或者由于任何其他原因无法取消,则方法将返回 false 值,并且任务不会被取消。

  • 如果任务正在执行器中等待获取执行它的 Thread 对象,则任务将被取消,并且永远不会开始执行。如果任务已经在运行,则取决于方法参数。cancel() 方法接收一个布尔值作为参数。如果此参数的值为 true 且任务正在运行,则任务将被取消。如果参数的值为 false 且任务正在运行,则任务不会被取消。

以下截图显示了此示例的执行输出:

图片

更多...

如果你使用控制已取消任务的 Future 对象的 get() 方法,get() 方法将抛出 CancellationException 异常。

参见

  • 本章中关于 在执行器中执行返回结果的任务 的配方

控制在执行器中完成任务的执行

Java API 提供了 FutureTask 类作为可取消的异步计算。它实现了 RunnableFuture 接口,并提供了 Future 接口的基本实现。我们可以使用 CallableRunnable 对象(Runnable 对象不返回结果,因此在这种情况下我们必须传递 Future 对象将返回的结果作为参数)。它提供了取消执行和获取计算结果的方法。它还提供了一个名为 done() 的方法,允许你在执行器中执行的任务最终化后执行一些代码。它可以用来进行一些后处理操作,例如生成报告、通过电子邮件发送结果或释放一些资源。当 FutureTask 类内部调用控制此 FutureTask 对象的任务执行完成时,将调用此方法。该方法在设置任务的结果并将其状态更改为 isDone 后调用,无论任务是否已取消或正常完成。

默认情况下,此方法为空。你可以覆盖 FutureTask 类并实现此方法以更改行为。在本配方中,你将学习如何覆盖此方法以在任务最终化后执行代码。

准备工作

本配方的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为 ExecutableTask 的类,并指定它实现由 String 类参数化的 Callable 接口:
        public class ExecutableTask implements Callable<String> {

  1. 声明一个名为 name 的私有 String 属性。它将存储任务的名称。实现 getName() 方法以返回此属性的值:
        private final String name; 
        public String getName(){ 
          return name; 
        }

  1. 实现类的构造函数以初始化任务的名称:
        public ExecutableTask(String name){ 
          this.name=name; 
        }

  1. 实现类的 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; 
        }

  1. 实现一个名为 ResultTask 的类,该类扩展了由 String 类参数化的 FutureTask 类:
        public class ResultTask extends FutureTask<String> {

  1. 声明一个名为 name 的私有 String 属性。它将存储任务的名称:
        private final String name;

  1. 实现类的构造函数。它必须接收一个 Callable 对象作为参数。调用父类的构造函数并使用接收到的任务属性初始化 name 属性:
        public ResultTask(ExecutableTask callable) { 
          super(callable); 
          this.name= callable.getName(); 
        }

  1. 覆盖 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); 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用Executors类的newCachedThreadPool()方法创建ExecutorService
        ExecutorService executor=Executors.newCachedThreadPool();

  1. 创建一个数组来存储五个ResultTask对象:
        ResultTask resultTasks[]=new ResultTask[5];

  1. 初始化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]); 
        }

  1. 将主线程休眠 5 秒:
        try { 
          TimeUnit.SECONDS.sleep(5); 
        } catch (InterruptedException e1) { 
          e1.printStackTrace(); 
        }

  1. 取消你发送到执行器的所有任务:
        for (int i=0; i<resultTasks.length; i++) { 
          resultTasks[i].cancel(true); 
        }

  1. 使用ResultTask对象的get()方法将那些未被取消的任务的结果写入控制台:
        for (int i=0; i<resultTasks.length; i++) { 
          try { 
            if (!resultTasks[i].isCancelled()){ 
              System.out.printf("%s\n",resultTasks[i].get()); 
            } 
          } catch (InterruptedException | ExecutionException e) { 
            e.printStackTrace(); 
          }     
        }

  1. 使用shutdown()方法结束执行器:
            executor.shutdown(); 
          } 
        }

它是如何工作的...

当被控制的任务完成执行时,FutureTask类会调用done()方法。在这个例子中,你实现了Callable对象,ExecutableTask类,然后是控制ExecutableTask对象执行的FutureTask类的子类。

done()方法在FutureTask类内部被调用,在建立返回值并更改任务状态为isDone之后。你不能更改任务的结果值或更改其状态,但你可以关闭任务使用的资源,写入日志消息或发送通知。FutureTask类可以用来确保一个特定的任务只运行一次,因为调用它的run()方法将只执行其包装的Runnable/Callable接口一次(当结果可用时,可以使用get方法获取结果)。

参见

  • 本章中的在执行器中执行返回结果的任务菜谱

在执行器中分离任务的启动和结果的处理

通常情况下,当你使用执行器执行并发任务时,你会向执行器发送RunnableCallable任务,并获取Future对象来控制方法。你可以找到需要将任务发送到执行器在一个对象中,并在另一个对象中处理结果的情况。对于这种情况,Java 并发 API 提供了CompletionService类。

CompletionService类有一个方法可以发送任务到执行器,还有一个方法可以获取已完成执行的下个任务的Future对象。内部,它使用一个Executor对象来执行任务。这种行为的好处是共享一个CompletionService对象并将任务发送到执行器,以便其他人可以处理结果。限制是第二个对象只能获取已完成执行的任务的Future对象,因此这些Future对象只能用来获取任务的结果。

在这个菜谱中,你将学习如何使用CompletionService类来分离在执行器中启动任务的过程和其结果的处理。

准备中

此菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 ReportGenerator 的类,并指定它实现由 String 类参数化的 Callable 接口:
        public class ReportGenerator implements Callable<String> {

  1. 声明两个名为 sendertitle 的私有 String 属性。这些属性将代表报告的数据:
        private final String sender; 
        private final String title;

  1. 实现类的构造函数以初始化两个属性:
        public ReportGenerator(String sender, String title){ 
          this.sender=sender; 
          this.title=title; 
        }

  1. 实现 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(); 
          }

  1. 然后,使用 sendertitle 属性生成报告字符串,并返回该字符串:
          String ret=sender+": "+title; 
          return ret; 
        }

  1. 创建一个名为 ReportRequest 的类,并指定它实现 Runnable 接口。此类将模拟一些报告请求:
        public class ReportRequest implements Runnable {

  1. 声明一个名为 name 的私有 String 属性,用于存储 ReportRequest 的名称:
        private final String name;

  1. 声明一个名为 service 的私有 CompletionService 属性。CompletionService 接口是一个参数化接口。使用 String 类:
        private final CompletionService<String> service;

  1. 实现类的构造函数以初始化两个属性:
        public ReportRequest(String name, CompletionService<String>
                             service){ 
          this.name=name; 
          this.service=service; 
        }

  1. 实现 run() 方法。创建三个 ReportGenerator 对象,并使用 submit() 方法将它们发送到 CompletionService 对象:
        @Override 
        public void run() { 
          ReportGenerator reportGenerator=new ReportGenerator(name,
                                                              "Report"); 
          service.submit(reportGenerator); 

        }

  1. 创建一个名为 ReportProcessor 的类。此类将获取 ReportGenerator 任务的输出。指定它实现 Runnable 接口:
        public class ReportProcessor implements Runnable {

  1. 声明一个名为 service 的私有 CompletionService 属性。由于 CompletionService 接口是一个参数化接口,因此使用 String 类作为此 CompletionService 接口的参数:
        private final CompletionService<String> service;

  1. 声明一个名为 end 的私有 Boolean 属性。添加 volatile 关键字以确保所有线程都能访问属性的真正值:
        private volatile boolean end;

  1. 实现类的构造函数以初始化两个属性:
        public ReportProcessor (CompletionService<String> service){ 
          this.service=service; 
          end=false; 
        }

  1. 实现 run() 方法。当 end 属性为 false 时,调用 CompletionService 接口的 poll() 方法以获取完成服务已执行的任务的下一个 Future 对象:
        @Override 
        public void run() { 
          while (!end){ 
            try { 
              Future<String> result=service.poll(20, TimeUnit.SECONDS);

  1. 然后,使用 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"); 
        }

  1. 实现一个修改 end 属性值的 stopProcessing() 方法:
        public void stopProcessing() { 
          this.end = true; 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 使用 Executors 类的 newCachedThreadPool() 方法创建 ThreadPoolExecutor
        ExecutorService executor=Executors.newCachedThreadPool();

  1. 使用之前创建的执行器作为构造函数的参数创建 CompletionService
        CompletionService<String> service=new
                                   ExecutorCompletionService<>(executor);

  1. 创建两个 ReportRequest 对象和执行它们的线程:
        ReportRequest faceRequest=new ReportRequest("Face", service); 
        ReportRequest onlineRequest=new ReportRequest("Online", service);   
        Thread faceThread=new Thread(faceRequest); 
        Thread onlineThread=new Thread(onlineRequest);

  1. 创建一个 ReportProcessor 对象和执行它的线程:
        ReportProcessor processor=new ReportProcessor(service); 
        Thread senderThread=new Thread(processor);

  1. 启动三个线程:
        System.out.printf("Main: Starting the Threads\n"); 
        faceThread.start(); 
        onlineThread.start(); 
        senderThread.start();

  1. 等待 ReportRequest 线程的最终化:
        try { 
          System.out.printf("Main: Waiting for the report generators.\n"); 
          faceThread.join(); 
          onlineThread.join(); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用shutdown()方法完成执行器,并使用awaitTermination()方法等待任务的最终化:
        System.out.printf("Main: Shutting down the executor.\n"); 
        executor.shutdown(); 
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 完成对ReportSender对象的执行,将它的end属性设置为 true:
        processor.stopProcessing(); 
        System.out.println("Main: Ends");

它是如何工作的...

在示例的主类中,您使用Executors类的newCachedThreadPool()方法创建了ThreadPoolExecutor。然后,您使用该Executor对象初始化一个CompletionService对象,因为完成服务使用执行器来执行其任务。要使用完成服务执行任务,请使用submit()方法,如ReportRequest类中所示。

当这些任务之一在完成服务完成其执行时执行,服务将用于控制其执行的Future对象存储在队列中。poll()方法访问此队列以检查是否有任何任务已完成其执行;如果是,它返回队列的第一个元素,这是一个已完成执行的任务的Future对象。当poll()方法返回一个Future对象时,它将其从队列中删除。在这种情况下,您向方法传递了两个属性以指示您想要等待任务最终化的时间,以防已完成任务的队列中没有结果。

一旦创建了CompletionService对象,您就创建了两个ReportRequest对象,这些对象执行一个ReportGenerator任务,使用之前创建并作为参数传递给ReportRequest对象构造函数的CompletionService对象执行ReportGenerator任务

更多内容...

CompletionService类可以执行CallableRunnable任务。在这个例子中,您使用了Callable,但您也可以发送Runnable对象。由于Runnable对象不产生结果,CompletionService类的哲学在这种情况下不适用。

此类还提供了两个其他方法来获取已完成任务的Future对象。这些方法如下:

  • poll():不带参数的poll()方法检查队列中是否有任何Future对象。如果队列为空,它将立即返回 null。否则,它返回其第一个元素并将其从队列中删除。

  • take():此方法不带参数,检查队列中是否有任何Future对象。如果队列为空,它将阻塞线程,直到队列中有元素。如果队列中有元素,它将返回并从队列中删除其第一个元素。

在我们的情况下,我们使用带超时的poll()方法来控制何时结束ReportProcessor任务的执行。

参见

  • 本章中关于在返回结果的执行器中执行任务的配方

第五章:Fork/Join 框架

在本章中,我们将涵盖:

  • 创建 fork/join 池

  • 联合任务的结果

  • 异步运行任务

  • 在任务中抛出异常

  • 取消任务

简介

通常,当你实现一个简单的并发 Java 应用程序时,你会实现一些 Runnable 对象,然后是相应的 Thread 对象。你在程序中控制这些线程的创建、执行和状态。Java 5 通过引入 ExecutorExecutorService 接口及其实现类(例如,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用于返回一个结果的任务,以及CountedCompleter用于在所有子任务完成后启动完成动作的任务。

该框架提供的多数特性都包含在 Java 7 中,但 Java 8 在其中添加了一些小特性。它包括了一个默认的ForkJoinPool对象。你可以使用ForkJoinPool类的静态方法commonPool()来获取它。默认的 fork/join 执行器将默认使用由你的计算机可用的处理器数量确定的线程数。你可以通过更改系统属性java.util.concurrent.ForkJoinPool.common.parallelism的值来改变这种默认行为。这个默认池被并发 API 的其他类内部使用。例如,并行流使用它。Java 8 还包含了之前提到的CountedCompleter类。

本章介绍了五个配方,展示了如何高效地使用 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 个元素,它将把分配给它的列表部分分成两部分,并创建两个任务来更新相应部分的产品价格。

按照以下步骤实现示例:

  1. 创建一个名为Product的类来存储产品的名称和价格:
        public class Product { 

  1. 声明一个名为name的私有String属性和一个名为price的私有double属性:
        private String name; 
        private double price; 

  1. 实现这些字段的 getter 和 setter 方法。它们非常简单实现,所以不包括其源代码。

  2. 创建一个名为ProductListGenerator的类来生成一系列随机产品:

        public class ProductListGenerator { 

  1. 实现接收一个带有列表大小的int参数的generate()方法。它返回一个包含生成产品列表的List<Product>对象:
        public List<Product> generate (int size) { 

  1. 创建一个对象以返回产品列表:
        List<Product> ret=new ArrayList<Product>(); 

  1. 生成产品列表。将相同的单价分配给所有产品,例如10,以检查程序是否运行良好:
          for (int i=0; i<size; i++){ 
            Product product=new Product(); 
            product.setName("Product "+i); 
            product.setPrice(10); 
            ret.add(product); 
          } 
          return ret; 
        } 

  1. 创建一个名为Task的类。指定它扩展RecursiveAction类:
        public class Task extends RecursiveAction { 

  1. 声明一个名为products的私有List<Product>属性:
        private List<Product> products; 

  1. 声明两个名为firstlast的私有int属性。这些属性将确定任务需要处理的产品的块:
        private int first; 
        private int last; 

  1. 声明一个名为increment的私有double属性来存储产品价格的增量:
        private double increment; 

  1. 实现类的构造函数,用于初始化类的所有属性:
        public Task (List<Product> products, int first, int last,
                     double increment) { 
          this.products=products; 
          this.first=first; 
          this.last=last; 
          this.increment=increment; 
        } 

  1. 实现一个名为compute()的方法,该方法将实现任务的逻辑:
        @Override 
        protected void compute() { 

  1. 如果lastfirst属性之间的差异小于10(任务需要更新少于10个产品),使用updatePrices()方法增加该组产品的价格:
        if (last - first<10) { 
          updatePrices(); 

  1. 如果lastfirst属性之间的差异大于或等于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);   
        } 

  1. 实现updatePrices()方法。此方法更新列表中firstlast属性之间的位置所占据的产品:
        private void updatePrices() { 
          for (int i=first; i<last; i++){ 
            Product product=products.get(i); 
            product.setPrice(product.getPrice()*(1+increment)); 
          } 
        } 

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) { 

  1. 使用ProductListGenerator类创建一个包含10000个产品的列表:
        ProductListGenerator generator=new ProductListGenerator(); 
        List<Product> products=generator.generate(10000); 

  1. 创建一个新的Task对象来更新列表中所有产品的prices。参数first取值为0last参数取值为10000(产品列表的大小):
        Task task=new Task(products, 0, products.size(), 0.20); 

  1. 使用不带参数的构造函数创建一个ForkJoinPool对象:
        ForkJoinPool pool=new ForkJoinPool(); 

  1. 使用execute()方法在池中执行任务:
        pool.execute(task); 

  1. 实现一个每五毫秒显示池进化的代码块,将池的一些参数的值写入控制台,直到任务完成其执行:
        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()); 

  1. 使用shutdown()方法关闭池:
        pool.shutdown(); 

  1. 使用isCompletedNormally()方法检查任务是否无错误地完成,并在这种情况下向控制台写入一条消息:
        if (task.isCompletedNormally()){ 
          System.out.printf("Main: The process has completed
                             normally.\n"); 
        } 

  1. 所有产品在增加后的预期价格是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()); 
          } 
        } 

  1. 写入一条消息以指示程序的最终化:
        System.out.println("Main: End of the program.\n"); 

它是如何工作的...

在这个例子中,你创建了一个ForkJoinPool对象和一个在池中执行的ForkJoinTask类的子类。为了创建ForkJoinPool对象,你使用了不带参数的构造函数,因此它将以默认配置执行。它创建了一个具有与计算机处理器数量相等的线程数量的池。当创建ForkJoinPool对象时,那些线程会被创建并在池中等待,直到有任务到达以供执行。

由于Task类不返回结果,它扩展了RecursiveAction类。在配方中,你使用了推荐的实现任务的架构。如果任务需要更新超过10个产品,它将那个元素集分成两个块,创建两个任务,并将一个块分配给每个任务。你使用了Task类中的firstlast属性来知道这个任务在产品列表中需要更新的位置范围。你使用firstlast属性来仅使用一个产品列表的副本,而不是为每个任务创建不同的列表。

为了执行任务创建的子任务,它调用invokeAll()方法。这是一个同步调用,任务在继续(可能完成)其执行之前等待子任务的最终化。当任务等待其子任务时,执行它的工作线程会获取另一个等待执行的任务并执行它。通过这种行为,fork/join 框架提供了比RunnableCallable对象本身更有效的任务管理。

ForkJoinTask 类的 invokeAll() 方法是 Executor 和 fork/join 框架之间主要区别之一。在 Executor 框架中,所有任务都必须发送到执行器,而在这个例子中,任务包括在池内执行和控制任务的方法。你在扩展了 RecursiveAction 类的 Task 类中使用了 invokeAll() 方法,而 RecursiveAction 类又扩展了 ForkJoinTask 类。

你使用 execute() 方法向池中发送了一个独特的任务来更新所有产品列表。在这种情况下,这是一个异步调用,主线程继续执行。

你使用了 ForkJoinPool 类的一些方法来检查正在运行的任务的状态和演变。该类包括更多可用于此目的的方法。请参阅第九章 监控 fork/join 池 菜单,测试并发应用程序,以获取这些方法的完整列表。

最后,就像使用 Executor 框架一样,你应该使用 shutdown() 方法来结束 ForkJoinPool

以下截图显示了此示例的部分执行过程:

你可以看到任务完成工作,产品价格更新。

还有更多...

ForkJoinPool 类提供了其他方法来执行任务。这些方法如下:

  • execute (Runnable task): 这是示例中使用的 execute() 方法的另一个版本。在这种情况下,你向 ForkJoinPool 类发送一个 Runnable 任务。请注意,ForkJoinPool 类不使用 Runnable 对象的工作窃取算法。它仅与 ForkJoinTask 对象一起使用。

  • invoke(ForkJoinTask<T> task): 虽然 execute() 方法会对 ForkJoinPool 类进行异步调用,正如你在示例中所学,但 invoke() 方法会对 ForkJoinPool 类进行同步调用。这个调用会一直持续到作为参数传递的任务完成执行才会返回。

  • 你还可以使用在 ExecutorService 接口中声明的 invokeAll()invokeAny() 方法。这些方法接收 Callable 对象作为参数。ForkJoinPool 类不使用 Callable 对象的工作窃取算法,因此你最好使用 ThreadPoolExecutor 来执行它们。

ForkJoinTask 类还包括示例中使用的 invokeAll() 方法的其他版本。这些版本如下:

  • invokeAll(ForkJoinTask<?>... tasks): 这个方法版本使用可变数量的参数列表。你可以传递任意数量的 ForkJoinTask 对象作为参数。

  • invokeAll(Collection<T> tasks): 这个方法版本接受一个泛型类型 T 的对象集合(例如,ArrayList 对象、LinkedList 对象或 TreeSet 对象)。这个泛型类型 T 必须是 ForkJoinTask 类或其子类。

虽然ForkJoinPool类是为执行ForkJoinTask对象而设计的,但你也可以直接执行RunnableCallable对象。你还可以使用ForkJoinTask类的adapt()方法,该方法接受一个Callable对象或Runnable对象,并返回一个ForkJoinTask对象以执行该任务。

参见

  • 在第九章的监控 fork/join 池菜谱中,测试并发应用程序

合并任务的结果

Fork/join 框架提供了执行返回结果的任务的能力。这种任务通过RecursiveTask类实现。这个类扩展了ForkJoinTask类,并实现了 Executor 框架提供的Future接口。

在任务内部,你必须使用 Java API 文档中推荐的架构:

    if (problem size > size){ 
      tasks=Divide(task); 
      execute(tasks); 
      joinResults() 
      return result; 
    } else { 
      resolve problem; 
      return result; 
    } 

如果任务需要解决比预定义大小更大的问题,你将问题分解成更多的子任务,并使用 fork/join 框架执行这些子任务。当它们完成执行后,启动任务将获得所有子任务生成的结果,将它们分组,并返回最终结果。最终,当在池中启动的任务完成执行时,你将获得其结果,这实际上是整个问题的最终结果。

在这个菜谱中,你将通过开发一个在文档中查找单词的应用程序来学习如何使用这种问题解决方法,该应用程序将实现以下两种类型的任务:

  • 一个文档任务,将在文档的一组行中搜索一个单词

  • 一行任务,将在文档的一部分中搜索一个单词

所有任务都将返回单词在它们处理的文档部分或行中的出现次数。在这个菜谱中,我们将使用 Java 并发 API 提供的默认 fork/join 池。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为DocumentMock的类。它将生成一个字符串矩阵,以模拟文档:
        public class DocumentMock { 

  1. 创建一个包含一些单词的字符串数组。这个数组将用于生成字符串矩阵:
        private String words[]={"the","hello","goodbye","packt",
                                "java","thread","pool","random",
                                "class","main"}; 

  1. 实现一个generateDocument()方法。该方法接收行数、每行的单词数以及示例将要查找的单词作为参数。它返回一个字符串矩阵:
        public String[][] generateDocument(int numLines, int numWords,
                                           String word){ 

  1. 首先,创建生成文档所需的必要对象——字符串矩阵和一个用于生成随机数的Random对象:
        int counter=0; 
        String document[][]=new String[numLines][numWords]; 
        Random random=new Random(); 

  1. 用字符串填充数组。在每个位置存储在单词数组中随机位置的字符串,并计算程序将在生成的数组中查找的单词出现的次数。你可以使用这个值来检查程序是否正确地完成了其工作:
        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++; 
            } 
          } 
        } 

  1. 写一条包含单词出现次数的消息,并返回生成的矩阵:
        System.out.println("DocumentMock: The word appears "+ counter+"
                            times in the document"); 
        return document; 

  1. 创建一个名为 DocumentTask 的类,并指定它扩展由 Integer 类参数化的 RecursiveTask 类。这个类将实现一个任务,用于计算一组行中单词出现的次数:
        public class DocumentTask extends RecursiveTask<Integer> { 

  1. 声明一个名为 document 的私有 String 矩阵和两个名为 startend 的私有 int 属性。此外,声明一个名为 word 的私有 String 属性:
        private String document[][]; 
        private int start, end; 
        private String word; 

  1. 实现类的构造函数以初始化所有属性:
        public DocumentTask (String document[][], int start, int end,
                             String word){ 
          this.document=document; 
          this.start=start; 
          this.end=end; 
          this.word=word; 
        } 

  1. 实现 compute() 方法。如果 endstart 属性之间的差小于 10,任务通过调用 processLines() 方法计算这些位置之间的行中单词出现的次数:
        @Override 
        protected Integer compute() { 
          Integer result=null; 
          if (end-start<10){ 
            result=processLines(document, start, end, word); 

  1. 否则,将行组分成两个对象,创建两个新的 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); 

  1. 然后,使用 groupResults() 方法将两个任务返回的值相加。最后,返回任务计算出的结果:
          try { 
            result=groupResults(task1.get(),task2.get()); 
          } catch (InterruptedException | ExecutionException e) { 
            e.printStackTrace(); 
          } 
        } 
        return result; 

  1. 实现 processLines() 方法。它接收字符串矩阵、start 属性、end 属性以及任务正在搜索的 word 属性作为参数:
        private Integer processLines(String[][] document, int start,
                                     int end,String word) { 

  1. 对于任务必须处理的每一行,创建一个 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); 
        } 

  1. 使用 invokeAll() 方法执行该列表中的所有任务:
        invokeAll(tasks); 

  1. 将所有这些任务返回的值相加并返回结果:
        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 result; 

  1. 实现 groupResults() 方法。它将两个数字相加并返回结果:
        private Integer groupResults(Integer number1,Integer number2) { 
          Integer result; 
          result=number1+number2; 
          return result; 
        } 

  1. 创建一个名为 LineTask 的类,并指定它扩展由 Integer 类参数化的 RecursiveTask 类。这个类将实现一个任务,用于计算一行中单词出现的次数:
        public class LineTask extends RecursiveTask<Integer>{ 

  1. 声明一个名为 line 的私有 String 数组属性和两个名为 startend 的私有 int 属性。最后,声明一个名为 word 的私有 String 属性:
        private String line[]; 
        private int start, end; 
        private String word; 

  1. 实现类的构造函数以初始化所有属性:
        public LineTask(String line[],int start,int end,String word) { 
          this.line=line; 
          this.start=start; 
          this.end=end; 
          this.word=word; 
        } 

  1. 实现类的 compute() 方法。如果 endstart 属性之间的差小于 100,任务使用 count() 方法在由 startend 属性确定的行片段中搜索单词:
        @Override 
        protected Integer compute() { 
          Integer result=null; 
          if (end-start<100) { 
            result=count(line, start, end, word); 

  1. 否则,将行中的单词组分成两部分,创建两个新的 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); 

  1. 然后,使用 groupResults() 方法将两个任务返回的值相加。最后,返回任务计算出的结果:
          try { 
            result=groupResults(task1.get(),task2.get()); 
          } catch (InterruptedException | ExecutionException e) { 
            e.printStackTrace(); 
          } 
        } 
        return result; 

  1. 实现 count() 方法。它接收包含完整行的字符串数组、start 属性、end 属性以及任务正在搜索的 word 属性作为参数:
        private Integer count(String[] line, int start, int end,
                              String word) { 

  1. 比较存储在startend属性之间的位置上的单词与任务正在搜索的word属性,如果它们相等,则增加counter变量:
        int counter; 
        counter=0; 
        for (int i=start; i<end; i++){ 
          if (line[i].equals(word)){ 
            counter++; 
          } 
        } 

  1. 为了减慢示例的执行速度,将任务休眠10毫秒:
        try { 
          Thread.sleep(10); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 返回counter变量的值:
        return counter; 

  1. 实现groupResults()方法。它将两个数字相加并返回result
        private Integer groupResults(Integer number1,Integer number2) { 
          Integer result; 
          result=number1+number2; 
          return result; 
        } 

  1. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:
        public class Main{ 
          public static void main(String[] args) { 

  1. 使用DocumentMock类创建具有100行和每行1000个单词的Document
        DocumentMock mock=new DocumentMock(); 
        String[][] document=mock.generateDocument(100, 1000, "the"); 

  1. 创建一个新的DocumentTask对象来更新整个文档的产品。start参数取值为0end参数取值为100
        DocumentTask task=new DocumentTask(document, 0, 100, "the"); 

  1. 使用commmonPool()方法获取默认的ForkJoinPool执行器,并使用execute()方法在上面执行任务:
        ForkJoinPool commonPool=ForkJoinPool.commonPool(); 
        commonPool.execute(task); 

  1. 实现一个代码块,显示池的进度信息,每秒将池的一些参数的值写入控制台,直到任务完成执行:
        do { 
          System.out.printf("*************************
                             *****************\n"); 
          System.out.printf("Main: Active Threads: %d\n",
                            commonPool.getActiveThreadCount()); 
          System.out.printf("Main: Task Count: %d\n",
                            commonPool.getQueuedTaskCount()); 
          System.out.printf("Main: Steal Count: %d\n",
                            commonPool.getStealCount()); 
          System.out.printf("***********************************
                             *******\n"); 
          try { 
            TimeUnit.SECONDS.sleep(1); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } while (!task.isDone()); 

  1. 使用shutdown()方法关闭池:
        pool.shutdown(); 

  1. 使用awaitTermination()方法等待任务的最终化:
        try { 
          pool.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 写出文档中单词出现的次数。检查这个数字是否与DocumentMock类写出的数字相同:
        try { 
          System.out.printf("Main: The word appears %d in the
                             document",task.get()); 
        } catch (InterruptedException | ExecutionException e) { 
          e.printStackTrace(); 
        } 

它是如何工作的...

在这个例子中,你实现了两个不同的任务:

  • DocumentTask:此类任务必须处理由startend属性确定的文档的一组行。如果这组行的数量小于10,它将为每一行创建一个LineTask,当它们完成执行后,它将那些任务的结果相加并返回总和的result。如果任务必须处理的行组的数量为10或更大,它将这组行分成两部分,并创建两个DocumentTask对象来处理这些新组。当这些任务完成执行后,任务将它们的结果相加并返回这个总和作为result

  • LineTask:此类任务必须处理文档某一行的一组单词。如果这组单词的数量小于100,任务将直接在该组单词中搜索单词并返回单词出现的次数。否则,它将单词组分成两部分,并创建两个LineTask对象来处理这些组。当这些任务完成执行后,任务将两个任务的结果相加并返回这个总和作为result

Main 类中,你使用了默认的 ForkJoinPool(通过静态方法 commonPool() 获取)并在其中执行了一个 DocumentTask 类,该类需要处理每行 100 行和每行 1000 个单词的文档。此任务将使用其他 DocumentTask 对象和 LineTask 对象来分解问题,当所有任务完成执行后,你可以使用原始任务来获取整个文档中单词出现的总数。由于任务返回结果,它们扩展了 RecursiveTask 类。

要获取 Task 返回的结果,你使用了 get() 方法。此方法声明在 RecursiveTask 类实现的 Future 接口中。

当你执行程序时,你可以比较在控制台写入的第一行和最后一行。第一行是文档生成时计算的单词出现次数,最后一行是 fork/join 任务计算出的相同数字。

更多...

ForkJoinTask 类提供了另一种完成任务执行并返回结果的方法,即 complete() 方法。此方法接受 RecursiveTask 类参数化中使用的对象类型,并在调用 join() 方法时将此对象作为任务的结果返回。建议使用此方法为异步任务提供结果。

由于 RecursiveTask 类实现了 Future 接口,因此 get() 方法还有一个版本:

  • get(long timeout, TimeUnit unit):此版本的 get() 方法,如果任务的结果不可用,将等待指定的时间。如果指定的时间过去后结果仍然不可用,则方法返回一个 null 值。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

参见

  • 本章中 创建 fork/join 池 的配方

  • 第九章中的 监控 fork/join 池 配方,测试并发应用程序

异步运行任务

当你在 ForkJoinPool 中执行 ForkJoinTask 时,你可以以同步或异步的方式进行。当你以同步方式执行时,将任务发送到池中的方法不会返回,直到发送的任务完成执行。当你以异步方式执行时,将任务发送到执行器的那个方法会立即返回,因此任务可以继续执行。

你应该意识到这两种方法之间有一个很大的区别。当你使用同步方法时(例如,使用 invokeAll() 方法),调用这些方法之一的任务(例如,invokeAll() 方法)将暂停,直到它发送到池中的任务完成其执行。这允许 ForkJoinPool 类使用工作窃取算法将新任务分配给执行睡眠任务的工人线程。相反,当你使用异步方法(例如,使用 fork() 方法)时,任务将继续执行,因此 ForkJoinPool 类不能使用工作窃取算法来提高应用程序的性能。在这种情况下,只有当你调用 join()get() 方法等待任务的最终化时,ForkJoinPool 类才能使用该算法。

除了 RecursiveActionRecursiveTask 类之外,Java 8 引入了一个新的 ForkJoinTask 类,它与 CountedCompleter 类一起使用。使用这类任务,你可以在启动任务且没有挂起的子任务时包含一个完成动作。这种机制基于类中包含的一个方法(即 onCompletion() 方法)和挂起任务的计数器。

此计数器默认初始化为零,你可以在需要时以原子方式递增它。通常,你将逐个递增此计数器,正如你启动子任务时那样。最后,当任务完成其执行时,你可以尝试完成任务的执行,并相应地执行 onCompletion() 方法。如果挂起的计数大于零,则递减一个。如果为零,则执行 onCompletion() 方法,然后尝试完成父任务。

在本食谱中,你将学习如何使用 ForkJoinPoolCountedCompleter 类提供的异步方法来管理任务。你将实现一个程序,该程序将在文件夹及其子文件夹中搜索具有特定扩展名的文件。你将要实现的 CountedCompleter 类将处理文件夹的内容。对于该文件夹内的每个子文件夹,它将以异步方式向 ForkJoinPool 类发送一个新的任务。对于该文件夹内的每个文件,任务将检查文件的扩展名,并在继续的情况下将其添加到结果列表中。当一个任务完成时,它将将其子任务的全部结果列表插入其结果任务中。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为 FolderProcessor 的类,并指定它扩展了参数化为 List<String> 类型的 CountedCompleter 类:
        public class FolderProcessor extends
                                CountedCompleter<List<String>> { 

  1. 声明一个名为 path 的私有 String 属性。该属性将存储任务将要处理的文件夹的完整路径:
        private String path; 

  1. 声明一个名为 extension 的私有 String 属性。该属性将存储任务将要查找的文件的扩展名:
        private String extension; 

  1. 声明两个名为tasksresultList的私有List属性。我们将使用第一个来存储从该任务启动的所有子任务,并使用另一个来存储该任务的结果列表:
        private List<FolderProcessor> tasks; 
        private List<String> resultList;      

  1. 为该类实现一个构造函数以初始化其属性及其父类。我们将其声明为protected,因为它只会在内部使用:
        protected FolderProcessor (CountedCompleter<?> completer,
                                   String path, String extension) { 
          super(completer); 
          this.path=path; 
          this.extension=extension; 
        } 

  1. 我们实现其他公共构造函数以供外部使用。由于此构造函数创建的任务不会有父任务,因此我们不将此对象作为参数包含:
        public FolderProcessor (String path, String extension) { 
          this.path=path; 
          this.extension=extension; 
        } 

  1. 实现compute()方法。由于我们的任务的基础类是CountedCompleter类,因此此方法的返回类型为void
        @Override 
        public void compute() { 

  1. 首先,初始化两个列表属性:
        resultList=new ArrayList<>(); 
        tasks=new ArrayList<>(); 

  1. 获取文件夹的内容:
        File file=new File(path); 
        File content[] = file.listFiles(); 

  1. 对于文件夹中的每个元素,如果存在子文件夹,则创建一个新的FolderProcessor对象,并使用fork()方法异步执行它。我们使用类的第一个构造函数,并将当前任务作为新任务的完成者任务传递。我们还使用addToPendingCount()方法增加挂起任务的计数器:
        if (content != null) { 
          for (int i = 0; i < content.length; i++) { 
            if (content[i].isDirectory()) { 
              FolderProcessor task=new FolderProcessor(this,
                              content[i].getAbsolutePath(), extension); 
              task.fork(); 
              addToPendingCount(1); 
              tasks.add(task); 

  1. 否则,使用checkFile()方法比较文件的扩展名与您正在寻找的扩展名,如果它们相等,则将文件的完整路径存储在之前声明的字符串列表中:
          } else { 
            if (checkFile(content[i].getName())){ 
              resultList.add(content[i].getAbsolutePath()); 
            } 
          } 
        } 

  1. 如果FolderProcessor子任务列表中的元素超过50个,向控制台写入一条消息以指示这种情况:
          if (tasks.size()>50) { 
            System.out.printf("%s: %d tasks ran.\n",
                              file.getAbsolutePath(),tasks.size()); 
          } 
        } 

  1. 最后,尝试使用tryComplete()方法完成当前任务:
          tryComplete(); 
        } 

  1. 实现onCompletion()方法。当所有子任务(从当前任务分叉的所有任务)完成执行时,将执行此方法。我们将所有子任务的结果列表添加到当前任务的结果列表中:
        @Override 
        public void onCompletion(CountedCompleter<?> completer) { 
          for (FolderProcessor childTask : tasks) { 
            resultList.addAll(childTask.getResultList()); 
          } 
        } 

  1. 实现checkFile()方法。此方法比较作为参数传递的文件名是否以您正在寻找的extension结尾。如果是,则方法返回true值,否则返回false值:
        private boolean checkFile(String name) { 
          return name.endsWith(extension); 
        } 

  1. 最后,实现getResultList()方法以返回任务的結果列表。此方法的代码非常简单,因此不会包含在内。

  2. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:

        public class Main { 
          public static void main(String[] args) { 

  1. 使用默认构造函数创建ForkJoinPool
        ForkJoinPool pool=new ForkJoinPool(); 

  1. 创建三个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"); 

  1. 使用execute()方法在池中执行三个任务:
        pool.execute(system); 
        pool.execute(apps); 
        pool.execute(documents); 

  1. 在三个任务完成执行之前,每秒向控制台写入有关池状态的信息:
        do { 
          System.out.printf("**********************************
                             ********\n"); 
          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())); 

  1. 使用shutdown()方法关闭ForkJoinPool
        pool.shutdown(); 

  1. 将每个任务生成的结果数量写入控制台:
        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类。每个任务处理一个文件夹的内容。正如你所知,这个内容有以下两种元素:

  • 文件

  • 其他文件夹

如果任务找到一个文件夹,它将创建另一个FolderProcessor对象来处理该文件夹,并使用fork()方法将其发送到池中。此方法将任务发送到将执行它的池,如果它有一个空闲的工作线程,或者它可以创建一个新的线程。该方法立即返回,因此任务可以继续处理文件夹的内容。对于每个文件,任务将其扩展名与其正在寻找的扩展名进行比较,如果它们相等,则将文件的名称添加到results列表中。

一旦任务处理完分配文件夹的所有内容,我们就尝试完成当前任务。正如我们在本食谱的介绍中所解释的,当我们尝试完成一个任务时,CountedCompleter的代码会查找待处理任务计数器的值。如果这个值大于0,它会减少该计数器的值。相反,如果值是0,任务将执行onCompletion()方法,然后尝试完成其父任务。在我们的情况下,当一个任务正在处理一个文件夹并找到一个子文件夹时,它会创建一个新的子任务,使用fork()方法启动该任务,并增加待处理任务的计数器。因此,当一个任务处理完其全部内容时,该任务的待处理任务计数器将等于我们启动的子任务数量。当我们调用tryComplete()方法时,如果当前任务的文件夹有子文件夹,这个调用将减少待处理任务的数量。只有当所有子任务都已完成,它的onCompletion()方法才会被执行。如果当前任务的文件夹没有任何子文件夹,待处理任务的计数器将为零;onComplete()方法将立即被调用,然后它将尝试完成其父任务。这样,我们从上到下创建了一个任务树,这些任务从下到上完成。在onCompletion()方法中,我们处理所有子任务的输出列表,并将它们的元素添加到当前任务的输出列表中。

ForkJoinPool类还允许以异步方式执行任务。你使用了execute()方法将三个初始任务发送到池中。在Main类中,你还使用shutdown()方法关闭了池,并写下了关于其中正在运行的任务的状态和进化的信息。ForkJoinPool类包括更多可用于此目的的方法。请参阅第九章中关于监控 fork/join 池的食谱,测试并发应用程序,以查看这些方法的完整列表。

还有更多...

在这个例子中,我们使用了addToPendingCount()方法来增加待处理任务的计数器,但我们还有其他方法可以用来改变这个计数器的值:

  • setPendingCount(): 此方法设置待处理任务计数器的值。

  • compareAndSetPendingCount(): 此方法接收两个参数。第一个是预期值,第二个是新值。如果待处理任务计数器的值等于预期值,则将其值设置为新的值。

  • decrementPendingCountUnlessZero(): 此方法会减少待处理任务计数器的值,除非它等于零。

CountedCompleter类还包括其他方法来管理任务的完成。以下是最重要的几个:

  • complete(): 此方法独立于待处理任务计数器的值执行onCompletion()方法,并尝试完成其完成者(父)任务。

  • onExceptionalCompletion(): 当调用completeExceptionally()方法或compute()方法抛出Exception时,此方法会被执行。重写此方法以包含处理此类异常的代码。

在这个例子中,你使用了join()方法等待任务的最终化并获取其结果。你也可以使用以下两种get()方法版本之一来完成这个目的:

  • get(long timeout, TimeUnit unit): 此版本的get()方法,如果任务的结果不可用,将等待指定的时间。如果指定的时间过去后结果仍然不可用,则方法返回一个null值。TimeUnit类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

  • join()方法不能被中断。如果你中断了调用join()方法的线程,该方法会抛出InterruptedException异常。

参见

  • 本章中的创建 fork/join 池配方

  • 在第九章的监控 fork/join 池配方中,测试并发应用程序

在任务中抛出异常

Java 中有两种类型的异常:

  • 检查型异常: 这些异常必须在方法的throws子句中指定或在它们内部捕获。例如,IOExceptionClassNotFoundException

  • 非检查型异常: 这些异常不需要指定或捕获。例如,NumberFormatException

ForkJoinTask 类的 compute() 方法中不能抛出任何已检查的异常,因为这个方法在其实现中不包含任何 throws 声明。您必须包含必要的代码来处理已检查的异常。另一方面,您可以抛出(或方法内部使用的任何方法或对象可以抛出)未检查的异常。ForkJoinTaskForkJoinPool 类的行为可能与你预期的不同。程序不会完成执行,你也不会在控制台看到任何关于异常的信息。它只是被默默吞没,就像它没有被抛出一样。只有当你调用初始任务的 get() 方法时,异常才会被抛出。然而,您可以使用 ForkJoinTask 类的一些方法来了解任务是否抛出了异常,以及抛出了什么类型的异常。在本食谱中,您将学习如何获取这些信息。

准备工作

本例的食谱实现使用了 Eclipse IDE。如果您使用 Eclipse 或其他如 NetBeans 之类的 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Task 的类。指定它实现 RecursiveTask 类,并使用 Integer 类进行参数化:
        public class Task extends RecursiveTask<Integer> { 

  1. 声明一个名为 array 的私有 int 数组。它将模拟您在本例中将要处理的数据数组:
        private int array[]; 

  1. 声明两个名为 startend 的私有 int 属性。这些属性将确定任务需要处理的数组元素:
        private int start, end; 

  1. 实现类的构造函数,初始化其属性:
        public Task(int array[], int start, int end){ 
          this.array=array; 
          this.start=start; 
          this.end=end; 
        } 

  1. 实现任务的 compute() 方法。由于您已将 RecursiveTask 类参数化为 Integer 类,因此此方法必须返回一个 Integer 对象。首先,向控制台发送一条消息,包含 startend 属性的值:
        @Override 
        protected Integer compute() { 
          System.out.printf("Task: Start from %d to %d\n",start,end);  

  1. 如果此任务需要处理的元素块(由 startend 属性确定)的大小小于 10,则检查数组中第四个位置的元素(索引号为三)是否在该块中。如果是这样,则抛出 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(); 
          } 

  1. 否则(此任务需要处理的元素块的大小为 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); 
          System.out.printf("Task: Result form %d to %d: %d\n",
                            start,mid,task1.join()); 
          System.out.printf("Task: Result form %d to %d: %d\n",
                            mid,end,task2.join()); 
        } 

  1. 向控制台发送一条消息,指示任务的结束,并写入 startend 属性的值:
        System.out.printf("Task: End form %d to %d\n",start,end); 

  1. 0 作为任务的结果返回:
        return 0; 

  1. 通过创建一个名为 Main 的类并包含一个 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) { 

  1. 创建一个包含 100 个整数数字的数组:
        int array[]=new int[100]; 

  1. 创建一个 Task 对象来处理那个 array
        Task task=new Task(array,0,100); 

  1. 使用默认构造函数创建一个 ForkJoinPool 对象:
        ForkJoinPool pool=new ForkJoinPool(); 

  1. 使用 execute() 方法在池中执行任务:
        pool.execute(task); 

  1. 使用 shutdown() 方法关闭 ForkJoinPool 类:
        pool.shutdown(); 

  1. 使用 awaitTermination() 方法等待任务的最终化。由于你希望无论任务完成所需时间多长都等待其最终化,因此将值 1TimeUnit.DAYS 作为参数传递给此方法:
        try { 
          pool.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 使用 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 对象来处理这些块。否则,它会在数组的第四个位置(索引号为三)查找元素。如果该元素在任务必须处理的块中,它将抛出 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 

这些任务是抛出异常及其父任务的那些任务。所有这些任务都异常完成。当你使用可能抛出异常的 ForkJoinPoolForkJoinTask 对象开发程序时,请考虑这一点,如果你不希望出现这种行为。

以下截图显示了此示例的部分执行:

更多内容...

在此示例中,你使用了 join() 方法等待任务的最终化并获取其结果。你也可以使用以下两种 get() 方法的其中一种来达到此目的:

  • get(): 此版本的 get() 方法在 ForkJoinTask 完成其执行后返回 compute() 方法返回的值,或者它等待直到其最终化。

  • get(long timeout, TimeUnit unit): 此版本的 get() 方法,如果任务的输出不可用,将等待指定的时间。如果指定的时间过去而结果仍然不可用,则方法返回一个 null 值。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

get()方法和join()方法之间有两个主要区别:

  • join()方法不能被中断。如果你中断调用join()方法的线程,该方法会抛出InterruptedException

  • get()方法会返回ExecutionException如果任务抛出任何未检查的异常时,join()方法将返回RuntimeException

如果不是抛出异常,而是使用ForkJoinTask类的completeExceptionally()方法,你可以获得与示例中相同的结果。代码如下:

    Exception e=new Exception("This task throws an Exception: "+
                              "Task from  "+start+" to "+end); 
    completeExceptionally(e); 

参见

  • 本章中创建一个创建 fork/join 池的配方

取消任务

当你在ForkJoinPool类中执行ForkJoinTask对象时,你可以在它们开始执行之前取消它们。ForkJoinTask类提供了cancel()方法用于此目的。当你想要取消一个任务时,你必须注意一些要点,如下所示:

  • ForkJoinPool类不提供任何方法来取消它在池中运行或等待的所有任务

  • 当你取消一个任务时,你不会取消该任务已执行的任务

在这个配方中,你将实现一个取消ForkJoinTask对象的示例。你将查找数组中数字的位置。第一个找到数字的任务将取消其余任务。由于 fork/join 框架不提供此功能,你将实现一个辅助类来完成此取消。

准备中...

本配方的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE 如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为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; 
          } 

  1. 创建一个名为TaskManager的类。我们将使用这个类来存储示例中使用的ForkJoinPool中执行的所有任务。由于ForkJoinPoolForkJoinTask类的限制,你将使用这个类来取消ForkJoinPool类的所有任务:
        public class TaskManager { 

  1. 声明一个使用ForkJoinTask类参数化的对象列表,使用名为ListInteger类参数化:
        private final ConcurrentLinkedDeque<SearchNumberTask> tasks; 

  1. 实现类的构造函数。它初始化任务列表:
        public TaskManager(){ 
          tasks=new ConcurrentLinkedDeque<>(); 
        } 

  1. 实现一个addTask()方法。它将一个ForkJoinTask对象添加到任务列表中:
        public void addTask(ForkJoinTask<Integer> task){ 
          tasks.add(task); 
        } 

  1. 实现一个cancelTasks()方法。它将使用cancel()方法取消存储在列表中的所有ForkJoinTask对象。它接收一个参数,即想要取消其余任务的ForkJoinTask对象。该方法取消所有任务:
        public void cancelTasks(SearchNumberTask cancelTask){ 
          for (SearchNumberTask task  :tasks) { 
            if (task!=cancelTask) { 
              task.cancel(true); 
              task.logCancelMessage(); 
            } 
          } 
        } 

  1. 实现一个名为 SearchNumberTask 的类。指定它扩展了参数化为 Integer 类的 RecursiveTask 类。这个类将在整数数组的元素块中查找一个数字:
        public class SearchNumberTask extends RecursiveTask<Integer> { 

  1. 声明一个名为 numbers 的私有 int 数组:
        private int numbers[]; 

  1. 声明两个名为 startend 的私有 int 属性。这些属性将确定这个任务需要处理的数组元素:
        private int start, end; 

  1. 声明一个名为 number 的私有 int 属性来存储你要查找的数字:
        private int number; 

  1. 声明一个名为 manager 的私有 TaskManager 属性。你将使用这个对象来取消所有任务:
        private TaskManager manager; 

  1. 声明一个私有的 int 常量并将其初始化为 -1。当任务找不到数字时,它将返回这个值:
        private final static int NOT_FOUND=-1; 

  1. 实现类的构造函数以初始化其属性:
        public SearchNumberTask(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; 
        } 

  1. 实现一个名为 compute() 的方法。首先,向控制台写入一条消息,指示 startend 属性的值:
        @Override 
        protected Integer compute() { 
          System.out.println("Task: "+start+":"+end); 

  1. 如果 startend 属性之间的差异大于 10(任务需要处理数组中的超过 10 个元素),调用 launchTasks() 方法将这个任务的工作分成两个子任务:
        int ret; 
        if (end-start>10) { 
          ret=launchTasks(); 

  1. 否则,在调用 lookForNumber() 方法的任务需要处理的数组块中查找数字:
        } else { 
          ret=lookForNumber(); 
        } 

  1. 返回任务的结果:
        return ret; 

  1. 实现一个名为 lookForNumber() 的方法:
        private int lookForNumber() { 

  1. 对于这个任务需要处理的元素块中的所有元素,比较该元素中存储的值与你要查找的数字。如果它们相等,向控制台写入一条消息,在这种情况下,使用 TaskManager 对象的 cancelTasks() 方法取消所有任务,并返回找到数字的元素位置:
        for (int i=start; i<end; i++){ 
          if (numbers[i]==number) { 
            System.out.printf("Task: Number %d found in position %d\n",
                              number,i); 
            manager.cancelTasks(this); 
            return i; 
          } 

  1. 在循环内部,让任务休眠一秒钟:
          try { 
            TimeUnit.SECONDS.sleep(1); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 

  1. 最后,返回 -1 值:
          return NOT_FOUND; 
        } 

  1. 实现一个名为 launchTasks() 的方法。首先,将这个任务需要处理的数字块分成两块,然后创建两个 Task 对象来处理它们:
        private int launchTasks() { 
          int mid=(start+end)/2; 

          Task task1=new Task(numbers,start,mid,number,manager); 
          Task task2=new Task(numbers,mid,end,number,manager); 

  1. 将任务添加到 TaskManager 对象中:
        manager.addTask(task1); 
        manager.addTask(task2); 

  1. 使用 fork() 方法异步执行两个任务:
        task1.fork(); 
        task2.fork(); 

  1. 等待任务的最终化,如果第一个任务的结果不等于 -1,则返回第一个任务的结果,否则返回第二个任务的结果:
        int returnValue; 
        returnValue=task1.join(); 
        if (returnValue!=-1) { 
          return returnValue; 
        } 

        returnValue=task2.join(); 
        return returnValue; 

  1. 实现一个名为 writeCancelMessage() 的方法,在任务被取消时写入一条消息:
        public void logCancelMessage(){ 
          System.out.printf("Task: Canceled task from %d to %d",
                            start,end); 
        } 

  1. 通过创建一个名为 Main 的类并包含一个 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) { 

  1. 使用 ArrayGenerator 类创建一个包含 1000 个数字的数组:
        ArrayGenerator generator=new ArrayGenerator(); 
        int array[]=generator.generateArray(1000); 

  1. 创建一个 TaskManager 对象:
        TaskManager manager=new TaskManager(); 

  1. 使用默认构造函数创建一个 ForkJoinPool 对象:
        ForkJoinPool pool=new ForkJoinPool(); 

  1. 创建一个用于处理之前生成的数组的 Task 对象:
        SearchNumberTask task=new SearchNumberTask (array,0,1000,
                                                    5,manager); 

  1. 使用 execute() 方法在池中异步执行任务:
        pool.execute(task); 

  1. 使用 shutdown() 方法关闭池:
        pool.shutdown(); 

  1. 使用 ForkJoinPool 类的 awaitTermination() 方法等待任务的最终化:
        try { 
          pool.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 向控制台写入一条消息,指示程序的结束:
        System.out.printf("Main: The program has finished\n"); 

它是如何工作的...

ForkJoinTask 类提供了一个 cancel() 方法,允许你在任务尚未执行之前取消它。这是一个非常重要的点。如果任务已经开始执行,调用 cancel() 方法将没有任何效果。该方法接收一个名为 mayInterruptIfRunningBoolean 类型的参数。这个名称可能让你认为,如果你向该方法传递 true 值,即使任务正在运行,任务也会被取消。Java API 文档指定,在 ForkJoinTask 类的默认实现中,此属性没有任何效果。只有当任务尚未开始执行时,才会取消任务。取消任务对已取消任务发送到池中的任务没有影响。它们将继续执行。

Fork/join 框架的一个限制是它不允许取消 ForkJoinPool 中所有的任务。为了克服这个限制,你实现了 TaskManager 类。它存储了所有已发送到池中的任务。它有一个取消它存储的所有任务的方法。如果一个任务因为正在运行或已完成而无法取消,cancel() 方法将返回 false 值,这样你就可以尝试取消所有任务,而不必担心可能的副作用。

在示例中,你实现了一个在数字数组中查找数字的任务。你按照 fork/join 框架的建议将问题分解成更小的子问题。你只对数字的一个出现感兴趣,所以当你找到它时,你取消其他任务。

以下截图显示了此示例执行的一部分:

参见

  • 本章中 创建 fork/join 池 的配方

第六章:并行和响应式流

在本章中,我们将介绍以下食谱:

  • 从不同的源创建流

  • 归约流中的元素

  • 收集流中的元素

  • 对流中的每个元素应用操作

  • 过滤流中的元素

  • 转换流中的元素

  • 排序流中的元素

  • 验证流中元素的条件

  • 使用响应式流的响应式编程

简介

Java 中的是一系列可以按声明性操作(映射、过滤、转换、归约和收集)处理的元素序列,使用lambda 表达式以顺序或并行方式。它在 Java 8 中引入,是该版本最显著的新特性之一,与 lambda 表达式一起。它们改变了你在 Java 中处理大量元素的方式,优化了语言处理这些元素的方式。

流引入了StreamDoubleStreamIntStreamLongStream接口,一些实用类如CollectorsStreamSupport,一些类似函数式的接口如Collector,以及在不同类中的许多方法,例如Collection接口中的stream()parallelStream()方法,或者Files类中的lines()方法。

通过本章的食谱,你将学习如何在你的应用程序中有效地使用流,但在那之前,让我们看看流最重要的特性:

  • 流是一系列数据,而不是数据结构。数据元素由流处理,但不存储在其中。

  • 你可以从不同的源创建流,例如集合(列表、数组等)、文件和字符串,或者通过创建一个提供流元素的类。

  • 你不能访问流中的单个元素。你定义流的源和想要对其元素应用的操作。流操作以函数式方式定义,你可以在中间和终端操作中使用 lambda 表达式来定义你想要执行的操作。

  • 你不能修改流的源。例如,如果你过滤了流中的某些元素,你是在跳过流上的元素,而不是其源。

  • 流定义了两种类型的操作:

  • 中间操作:这些操作总是产生一个包含其结果的新流。它们可以用来转换、过滤和排序流中的元素。

  • 终端操作:这些操作处理流中的所有元素以生成一个结果或副作用。执行后,流不能再使用。

  • 流管道由零个或多个中间操作和一个最终操作组成。

  • 中间操作可以是以下几种:

  • 无状态:处理流中的元素与其他元素无关。例如,根据条件过滤一个元素。

  • 有状态:处理流中的元素依赖于流中的其他元素。例如,对流的元素进行排序。

  • 惰性:中间操作是惰性的。它们不会在终端操作开始执行之前执行。如果 Java 检测到中间操作不会影响操作的最后结果,它可以避免对流的元素或元素集合执行中间操作。

  • Stream可以有无限数量的元素。有一些操作,如limit()findFirst(),可以用来限制在最终计算中使用的元素。由于中间操作是惰性的,无界流可以在有限的时间内完成其执行。

  • 流只能使用一次。正如我们之前提到的,当流的终端操作执行时,流被认为是消耗掉的,不能再使用。如果你需要再次处理相同的数据以生成不同的结果,你必须从相同的源创建一个新的Stream对象。如果你尝试使用已消耗的流,你会得到一个异常。

  • 你可以不费任何额外努力以顺序或并行方式处理流中的元素。你可以多次指定流的执行模式,但只有最后一次将被考虑。你必须小心选择模式。有状态的中间操作不会使用并发的所有可能性。

Java 9 引入了一种新的流类型——响应式流,允许你以异步方式与生产者和消费者通信。本章介绍了九种食谱,将教会你如何创建流并使用它们的所有中间和终端操作以并行和函数式的方式处理大量数据集。

从不同的源创建流

在这个食谱中,你将学习如何从不同的源创建流。你有不同的选项,如下所示:

  • Collection接口的parallelStream()方法

  • Supplier接口

  • 预定义的元素集合

  • File和一个目录

  • 一个数组

  • 随机数生成器

  • 两个不同流的连接

你可以从其他源(将在“更多内容”部分中描述)创建Stream对象,但我们认为这些更有用。

准备工作

本食谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何实现...

在这个食谱中,我们将实现一个示例,你将学习如何从前面描述的源创建流。按照以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用这些类。创建一个名为Person的类,具有六种不同类型的属性:StringintdoubleDate
        public class Person implements Comparable<Person> { 

          private int id; 
          private String firstName; 
          private String lastName; 
          private Date birthDate; 
          private int salary; 
          private double coeficient;

  1. 创建设置和获取这些属性值的setget方法。实现comparteTo()方法来比较两个Person对象。让我们考虑两个人员相同,如果他们有相同的firstName和相同的lastName
          public int compareTo(Person otherPerson) { 
            int compareLastNames = this.getLastName().compareTo
                                        (otherPerson.getLastName()); 
            if (compareLastNames != 0) { 
              return compareLastNames; 
            } else { 
            return this.getFirstName().compareTo
                                        (otherPerson.getFirstName()); 
          } 
        }

  1. 然后,创建一个名为PersonGenerator的类来创建一个随机的Person对象列表。在这个类中实现一个名为generatePersonList()的静态方法,该方法接收要生成的人员的数量,并返回一个包含指定人员数量的List<Person>对象。这里包括这个方法的版本,但你可以自由地更改它:
        public class PersonGenerator { 

          public static List<Person> generatePersonList (int size) { 
            List<Person> ret = new ArrayList<>(); 

            String firstNames[] = {"Mary","Patricia","Linda",
                                   "Barbara","Elizabeth","James",
                                   "John","Robert","Michael",
                                   "William"}; 
            String lastNames[] = {"Smith","Jones","Taylor",
                                  "Williams","Brown","Davies",
                                  "Evans","Wilson","Thomas",
                                  "Roberts"}; 

            Random randomGenerator=new Random(); 
            for (int i=0; i<size; i++) { 
              Person person=new Person(); 
              person.setId(i); 
              person.setFirstName(firstNames[randomGenerator
                                             .nextInt(10)]); 
              person.setLastName(lastNames[randomGenerator
                                           .nextInt(10)]); 
              person.setSalary(randomGenerator.nextInt(100000)); 
              person.setCoeficient(randomGenerator.nextDouble()*10); 
              Calendar calendar=Calendar.getInstance(); 
              calendar.add(Calendar.YEAR, -randomGenerator
                                                 .nextInt(30)); 
              Date birthDate=calendar.getTime(); 
              person.setBirthDate(birthDate); 
              ret.add(person); 
            } 
            return ret; 
          }

  1. 现在,创建一个名为MySupplier的类,并指定它实现了参数化为String类的Supplier接口:
        public class MySupplier implements Supplier<String> {

  1. 声明一个名为counter的私有AtomicInteger属性,并在类的构造函数中初始化它:
        private final AtomicInteger counter; 
        public MySupplier() { 
          counter=new AtomicInteger(0); 
        }

  1. 实现定义在Supplier接口中的get()方法。此方法将返回流的下一个元素:
          @Override 
          public String get() { 
            int value=counter.getAndAdd(1); 
            return "String "+value; 
          } 
        }

  1. 现在,创建一个名为Main的类并在其中实现main()方法:
        public class Main { 
          public static void main(String[] args) {

  1. 首先,我们将从元素列表中创建一个Stream对象。创建一个名为PersonGenerator的类来创建 10,000 个Person对象的列表,并使用List对象的parallelStream()方法创建Stream。然后,使用Stream对象的count()方法获取流的元素数量:
        System.out.printf("From a Collection:\n"); 
        List<Person> persons=PersonGenerator.generatePersonList(10000); 
        Stream<Person> personStream=persons.parallelStream();        
        System.out.printf("Number of persons: %d\n",
                          personStream.count());

  1. 然后,我们将从生成器创建一个Stream。创建MySupplier类的对象。然后,使用Stream类的静态方法generate(),将创建的对象作为参数传递以创建流。最后,使用parallel()方法将创建的流转换为并行流,使用limit()方法获取流的第一个十个元素,并使用forEach()方法打印流的元素:
        System.out.printf("From a Supplier:\n"); 
        Supplier<String> supplier=new MySupplier(); 
        Stream<String> generatorStream=Stream.generate(supplier); 
        generatorStream.parallel().limit(10).forEach(s->
                                           System.out.printf("%s\n",s));

  1. 然后,我们将从一个预定义的元素列表中创建一个流。使用Stream类的静态of()方法来创建Stream。此方法接收一个可变数量的参数。在这种情况下,我们将传递三个String对象。然后,使用流的parallel()方法将其转换为并行流,并使用forEach()方法在控制台打印值:
        System.out.printf("From a predefined set of elements:\n"); 
        Stream<String> elementsStream=Stream.of("Peter","John","Mary"); 
        elementsStream.parallel().forEach(element ->
                                    System.out.printf("%s\n", element));

  1. 现在,我们将创建一个流来读取文件的行。首先,创建一个BufferedReader对象来读取你想要读取的文件。然后,使用BufferedReader类的lines()方法获取一个String对象的流。此流的每个元素都将是从文件中的行。最后,使用parallel()方法获取流的并行版本,并使用count()方法获取Stream的元素数量。我们还需要关闭BufferedReader对象:
        System.out.printf("From a File:\n"); 
        try (BufferedReader br = new BufferedReader(new
                                 FileReader("data\\nursery.data"));) {                                    Stream<String> fileLines = br.lines(); 
          System.out.printf("Number of lines in the file: %d\n\n",
                            fileLines.parallel().count()); 
          System.out.printf("********************************
                             ************************\n"); 
          System.out.printf("\n"); 
          br.close(); 
        } catch (FileNotFoundException e) { 
          e.printStackTrace(); 
        } catch (IOException e) { 
          e.printStackTrace(); 
        }

  1. 现在,我们将创建一个Stream来处理文件夹的内容。首先,使用Files类的list()方法获取包含文件夹内容的Path对象流。然后,使用Stream对象的parallel()方法将其转换为并行流,并使用count()方法来计算其元素数量。最后,在这种情况下,我们必须使用close()方法来关闭 Stream:
        System.out.printf("From a Directory:\n"); 
        try { 
          Stream<Path> directoryContent = Files.list(Paths.get
                                    (System.getProperty("user.home"))); 
          System.out.printf("Number of elements (files and
                             folders):%d\n\n",
                            directoryContent.parallel().count()); 
          directoryContent.close(); 
          System.out.printf("********************************
                             ************************\n"); 
          System.out.printf("\n"); 
        } catch (IOException e) { 
          e.printStackTrace();
        }

  1. 我们接下来要使用的是Array。首先,创建一个字符串数组。然后,使用Arrays类的stream()方法从数组的元素创建一个流。最后,使用parallel()方法将流转换为并行流,并使用forEach()方法将流中的元素打印到控制台:
        System.out.printf("From an Array:\n"); 
        String array[]={"1","2","3","4","5"}; 
        Stream<String> streamFromArray=Arrays.stream(array); 
        streamFromArray.parallel().forEach(s->System.out.printf("%s : ",
                                                                s));

  1. 现在,我们将创建一个随机双精度浮点数流。首先,创建一个Random对象。然后,使用doubles()方法创建一个DoubleStream对象。我们将传递数字10作为该方法的参数,因此我们将创建一个包含十个元素的流。最后,使用parallel()方法将流转换为并行流,使用peek()方法将每个元素写入控制台,使用average()方法计算流中值的平均值,并使用getAsDouble()方法获取由average()方法返回的Optional对象中存储的值:
        Random random = new Random(); 
        DoubleStream doubleStream = random.doubles(10); 
        double doubleStreamAverage = doubleStream.parallel().peek
                                     (d -> System.out.printf("%f :",d))
                                     .average().getAsDouble();

  1. 最后,我们将创建一个连接两个流的流。首先,使用Stream类的of()方法创建两个String对象流。然后,使用Stream类的concat()方法将那些流连接成一个唯一的流。最后,使用Stream类的parallel()方法将流转换为并行流,并使用forEach()方法将所有元素写入控制台:
        System.out.printf("Concatenating streams:\n"); 
        Stream<String> stream1 = Stream.of("1", "2", "3", "4"); 
        Stream<String> stream2 = Stream.of("5", "6", "7", "8"); 
        Stream<String> finalStream = Stream.concat(stream1, stream2); 
        finalStream.parallel().forEach(s -> System.out.printf("%s : ",
                                                              s));

它是如何工作的...

让我们详细看看在这个例子中我们使用的所有创建流的方法:

  • 首先,我们使用了List类的parallelStream()方法。实际上,这个方法是在Collection接口中定义的,所以所有实现这个接口的类,例如ArrayListLinkedListTreeSet类,都实现了这个方法。你可以使用stream()方法创建一个顺序流,或者使用parallelStream()方法创建一个并行流。

  • 然后,我们使用了Supplier接口的一个实现:MySupplier类。该接口提供了get()方法。每次流需要处理一个元素时都会调用这个方法。你可以创建一个包含无限多个元素的流,因此你应该使用一个限制流中元素数量的方法,例如limit()方法。

  • 然后,我们使用了Stream类的of()方法。这是一个接收可变数量参数的静态方法,并返回一个包含这些参数作为元素的Stream

  • 然后,我们使用了BufferedStream类的lines()方法。此方法返回一个流,其中每个元素是从BufferedStream中读取的一行。我们使用此方法来读取文件的所有行,但您也可以使用它与其他类型的BufferedReader一起使用。

  • 然后,我们使用了Files类的list()方法。此方法接收一个表示系统文件夹的Path对象,并返回一个包含该文件夹中元素的Path对象的Stream。您必须考虑到此方法不是递归的,因此如果文件夹有一个或多个子文件夹,它不会处理它们的内容。正如您将在后面的更多内容部分中看到的那样,Files类有其他方法可以用来处理流。

  • 然后,我们使用了Arrays类的stream()方法,它接收一个数组并返回一个包含数组元素的Stream。如果数组是doubleintlong类型,它返回一个DoubleStreamIntStreamLongStream对象。这些都是特殊的流类型,允许您处理此类数字类型。

  • 然后,我们生成了一个包含随机数的流。我们使用了Random类的doubles()方法。我们传递给它我们想要获得的Stream的大小,但您也可以传递给它您想要获得的最低和最高数字。

  • 最后,我们使用了Stream类的concat()方法,它接受两个流并返回一个包含两个流元素的流。

我们还使用了Stream类的一些方法。其中大部分将在稍后的详细描述中介绍,但在这里我们提供它们的基本介绍:

  • count(): 此方法返回Stream中的元素数量。这是一个终端操作,并返回一个long类型的数字。

  • limit(): 此方法接收一个数字作为参数。如果流中的元素少于该数字,它返回一个包含所有元素的流。否则,它返回一个包含指定参数中元素数量的流。这是一个中间操作。

  • forEach(): 此方法允许您指定一个将应用于Stream中每个元素的行动。我们使用这个终端操作将一些信息写入控制台。我们使用 lambda 表达式来完成这个目的。

  • peek(): 这是一个中间操作,允许您对流的每个元素执行一个操作,并返回一个包含相同元素的流。此方法通常用作调试工具。请注意,像所有中间操作一样,这是一个延迟操作,因此它只会在终端操作请求的元素上执行。

  • average(): 这是一个在IntStreamDoubleStreamLongStream流中声明的方法。它返回一个OptionalDouble值。OptionalDouble类表示一个可能具有值或没有值的双精度浮点数。对于空的Stream,它不会生成值。

  • parallel(): 此方法将顺序 Stream 转换为并行流。本例中创建的大多数流都是顺序的,但我们可以使用 Stream 类的此方法将它们转换为并行流。

更多...

Java API 包含其他创建 Stream 对象的方法。在本节中,我们列举了一些:

  • Files 类提供了更多创建流的方法:

    • find(): 此方法返回符合 lambda 表达式中指定条件的文件夹中的文件,或其任何子文件夹中的文件。

    • walk(): 此方法返回一个包含文件夹及其所有子文件夹内容的 Path 对象流。

  • Stream 类还包括其他静态方法,允许你创建流:

    • iterate(): 此方法生成一个流,其元素是通过将一元函数应用于初始元素生成的。流的第一个元素是初始元素,第二个元素是应用函数到初始元素的结果,第三个元素是应用函数到第二个元素的结果,依此类推。
  • 最后,String 类有 chars() 方法。此方法返回一个包含构成 String 的字符值的 IntStream

参见

现在你已经创建了一个流,你必须处理其元素。本章中的所有食谱都为你提供了有关如何处理流元素的信息。

减少流元素

MapReduce 是一种编程模型,用于在分布式环境中使用大量在集群中工作的机器处理非常大的数据集。此编程模型具有以下两个操作:

  • Map: 此操作过滤并转换原始元素,使其更适合减少操作

  • Reduce: 此操作从所有元素生成一个汇总结果,例如数值的总和或平均值

这种编程模型在函数式编程世界中已被广泛使用。在 Java 生态系统中,Apache 软件基金会的 Hadoop 项目提供了对此模型的实现。Stream 类实现了两种不同的减少操作:

  • reduce() 方法的不同版本中实现的纯减少操作,该操作处理元素流以获取一个值

  • collect() 方法的不同版本中实现的可变减少,该操作处理元素流以生成一个可变的数据结构,如 CollectionStringBuilder

在本食谱中,你将学习如何使用 reduce() 方法的不同版本从值流生成结果。正如你可能已经想象的那样,reduce() 方法是 Stream 中的一个终端操作。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 首先,我们将创建一些辅助类,我们将在示例的后续部分使用它们。回顾一下 从不同来源创建流 的配方,并将此示例中的 PersonPersonGenerator 类包含在内。

  2. 然后,创建一个名为 DoubleGenerator 的类。实现一个名为 generateDoubleList() 的方法来生成一个双精度浮点数列表。它接收两个参数,分别是我们要生成的列表的大小和列表中的最大值。它将生成一个随机双精度浮点数列表:

        public class DoubleGenerator { 

          public static List<Double> generateDoubleList(int size,
                                                        int max) { 
            Random random=new Random(); 
            List<Double> numbers=new ArrayList<>(); 

            for (int i=0; i<size; i++) { 
              double value=random.nextDouble()*max; 
              numbers.add(value); 
            } 
            return numbers; 
          }

  1. 实现一个名为 generateStreamFromList() 的方法。此方法接收一个 double 数字列表作为参数,并生成一个包含列表元素的 DoubleStream 流。为此,我们将使用 DoubleStream.Builder 类来构建流:
        public static DoubleStream generateStreamFromList(List<Double>
                                                          list) { 
          DoubleStream.Builder builder=DoubleStream.builder(); 

          for (Double number : list) { 
            builder.add(number); 
          } 
          return builder.build(); 
        }

  1. 创建一个名为 Point 的类,它有两个双精度属性 xy,以及获取和设置其值的方法。这个类的代码非常简单,所以不会包含在内。

  2. 创建一个名为 PointGenerator 的类,其中有一个名为 generatePointList() 的方法。此方法接收你想要生成的列表的大小,并返回一个随机 Point 对象的列表:

        public class PointGenerator { 
          public static List<Point> generatePointList (int size) {

            List<Point> ret = new ArrayList<>(); 
            Random randomGenerator=new Random(); 
            for (int i=0; i<size; i++) { 
              Point point=new Point(); 
              point.setX(randomGenerator.nextDouble()); 
              point.setY(randomGenerator.nextDouble()); 
              ret.add(point); 
            } 
            return ret; 
          } 
        }

  1. 现在创建一个名为 Main 的类,其中包含 main() 方法。首先,我们将使用 DoubleGenerator 类生成一个包含 10,000 个双精度浮点数的 List
        public class Main { 
          public static void main(String args[]) { 

            List<Double> numbers = DoubleGenerator.generateDoubleList
                                                        (10000, 1000);

  1. Stream 类以及专门的 DoubleStreamIntStreamLongStream 类实现了一些专门化的 reduce 操作方法。在这种情况下,我们将使用 DoubleGenerator 类生成一个 DoubleStream,并使用 count()sum()average()max()min() 来获取元素数量、所有元素的总和、所有元素的平均值、流中的最大值和最小值。由于我们只能对流的元素进行一次处理,我们必须为每个操作创建一个新的流。请注意,这些方法仅存在于 DoubleStreamIntStreamLongStream 类中。Stream 类只有 count() 方法。其中一些方法返回一个可选对象。请注意,此对象可能没有任何值,因此在获取值之前应该进行检查:
        DoubleStream doubleStream = DoubleGenerator
                                      .generateStreamFromList(numbers); 
        long numberOfElements = doubleStream.parallel().count(); 
        System.out.printf("The list of numbers has %d elements.\n",
                          numberOfElements); 

        doubleStream = DoubleGenerator.generateStreamFromList(numbers); 
        double sum = doubleStream.parallel().sum(); 
        System.out.printf("Its numbers sum %f.\n", sum); 

        doubleStream = DoubleGenerator.generateStreamFromList(numbers); 
        double average = doubleStream.parallel().average()
                                                   .getAsDouble(); 
        System.out.printf("Its numbers have an average value of %f.\n",
                          average); 

        doubleStream = DoubleGenerator.generateStreamFromList(numbers); 
        double max = doubleStream.parallel().max().getAsDouble(); 
        System.out.printf("The maximum value in the list is %f.\n",
                          max); 

        doubleStream = DoubleGenerator.generateStreamFromList(numbers); 
        double min = doubleStream.parallel().min().getAsDouble(); 
        System.out.printf("The minimum value in the list is %f.\n",
                          min);

  1. 然后,我们将使用 reduce() 方法的第一个版本。此方法接收一个参数,即一个关联的 BinaryOperator,它接收两个相同类型的对象并返回该类型的对象。当操作处理完 Stream 的所有元素时,它返回一个参数化相同类型的 Optional 对象。例如,我们将使用这个版本来计算一个随机 Point 对象坐标的总和:
        List<Point> points=PointGenerator.generatePointList(10000);        
        Optional<Point> point=points.parallelStream().reduce((p1,p2) -> { 
          Point p=new Point(); 
          p.setX(p1.getX()+p2.getX()); 
          p.setY(p1.getY()+p2.getY()); 
          return p; 
        }); 
        System.out.println(point.get().getX()+":"+point.get().getY());

  1. 然后,我们将使用reduce()方法的第二个版本。它与上一个版本类似,但在这个版本中,除了关联的BinaryOperator对象外,它还接收该操作符的标识值(例如,对于总和是0,对于乘积是1)并返回我们正在处理的类型的元素。如果流中没有值,则返回标识值。在这种情况下,我们使用这个版本的reduce()方法来计算我们需要在工资上花费的总金额。我们使用map()方法将每个Person对象转换为int值(它的工资),这样当执行reduce()方法时,我们的Stream对象将具有int值。你将在转换流元素菜谱中了解更多关于map()方法的信息:
        System.out.printf("Reduce, second version\n"); 
        List<Person> persons = PersonGenerator.generatePersonList
                                                               (10000); 
        long totalSalary=persons.parallelStream().map
                         (p -> p.getSalary()).reduce(0, (s1,s2) -> s1+s2); 
        System.out.printf("Total salary: %d\n",totalSalary);

  1. 最后,我们将使用reduce()方法的第三个版本。这个版本在reduce操作的结果类型与流元素类型不同时使用。我们必须提供返回类型的标识值,一个实现BiFunction接口的累加器,它将接收一个返回类型的对象,一个流元素以生成一个返回类型的值,以及一个实现BinaryOperator接口的合并函数,它接收两个返回类型的对象以生成该类型的对象。在这种情况下,我们使用了这个方法版本来计算一个随机人员列表中工资高于 50,000 的人数:
        Integer value=0; 
        value=persons.parallelStream().reduce(value, (n,p) -> { 
          if (p.getSalary() > 50000) { 
            return n+1; 
          } else { 
            return n; 
          } 
        }, (n1,n2) -> n1+n2); 
        System.out.printf("The number of people with a salary bigger
                           that 50,000 is %d\n",value);

它是如何工作的...

在这个例子中,你学习了如何使用 Java 流提供的不同reduce操作。首先,我们使用了DoubleStreamIntStreamLongStream类提供的某些专用reduce操作。这些操作允许你计算流中元素的数量,计算流中所有元素的总和,计算流中元素的平均值,以及计算流中元素的最高和最低值。如果你使用一个泛型Stream,你将只有一个count()方法来计算流中的元素数量。

然后,我们使用了Stream类提供的reduce()方法的三个版本。第一个版本只接收一个参数,一个BinaryOperator。我们指定这个操作符为一个 lambda 表达式,你通常会这样做,但你也可以使用实现BinaryOperator接口的类的对象。这个操作符将接收流中的两个元素,并必须生成一个相同类型的新的元素。例如,我们接收两个Point对象并生成一个新的Point对象。该BinaryOperator实现的操作必须是结合律的,也就是说,以下表达式必须是正确的:

(a op b) op c = a op (b op c)

在这里op是我们的BinaryOperator

这个版本的reduce()方法返回一个Optional对象;Optional是因为如果流中没有元素,将没有返回值,Optional对象将是空的。

reduce()方法的第二个版本接收一个单位值和一个BinaryOperatorBinaryOperator必须与reduce()方法的另一个版本一样具有结合性。对于单位值,它必须是一个true表达式:

单位值 op a = a op 单位值 = a

在这种情况下,reduce()方法返回与流中元素相同类型的元素。如果流中没有元素,将返回单位值。

reduce()方法的最后一个版本用于当我们想要返回与流中元素类型不同的值时。在这种情况下,该方法有三个参数,一个单位值,一个累加器操作符和一个组合器操作符累加器操作符接收返回类型的一个值和流中的一个元素,并生成一个新的返回类型对象。

组合器函数接收两个返回类型的对象来计算一个新的返回类型对象。单位值是返回类型的单位值,它必须验证以下表达式:

组合器(u, 累加器(单位值, t)) == 累加器(u, t)

在这里,u是返回类型的一个对象,t是流中的一个元素。

以下截图显示了示例执行的输出:

还有更多...

我们已经将reduce()方法的全部参数实现为 lambda 表达式。reduce()方法的前两个版本接收一个BinaryOperator,第三个版本接收一个BiFunction和一个BinaryOperator。如果您想重用复杂的操作符,您可以实现一个实现必要接口的类,并使用该类的对象作为这些方法和Stream类其他方法的参数。

参见

  • 本章中的从不同源创建流配方

收集流中的元素

Java 流允许您以顺序或并行方式处理一系列元素。您可以从不同的数据源创建流,如CollectionFileArray,并对其元素应用一系列通常用 lambda 表达式定义的操作。这些操作可以分为两类:

  • 中间操作:这些操作返回其他Stream作为结果,并允许您过滤、转换或排序流中的元素

  • 终端操作:这些操作在处理流中的元素后返回结果

一个流有一个源,零个或多个中间操作,和一个终端操作。两个最重要的终端操作是:

  • 归约操作,它允许你在处理流元素后获得一个唯一的结果。这个结果通常是处理数据的摘要。"减少流元素"菜谱解释了如何在 Java 中使用归约操作。

  • 允许你生成一个数据结构的收集操作,该数据结构包含处理流元素的结果。这也被称为可变归约操作,因为结果是可变的数据结构。

在这个菜谱中,我们将学习如何在 Java 流中使用不同版本的collect()方法和辅助的Collectors类来执行收集操作。

准备工作

这个菜谱的示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE 如 NetBeans,打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 首先,让我们实现一些辅助类,我们将在示例中使用。实现Person类以存储有关个人的基本数据,以及PersonGenerator类以生成随机的人名单。你可以查看从不同来源创建流菜谱以查看这两个类的源代码。

  2. 在这个类中,使用以下代码覆盖toString()方法,它返回人的名字和姓氏:

        @Override 
        public String toString() { 
          return firstName + " " + lastName; 
        }

  1. 然后,创建一个名为Counter的类,具有两个属性:一个名为valueString属性和一个名为counterint属性。生成获取和设置这两个属性值的方法。这个类的源代码非常简单,所以不会包括在内。

  2. 现在,创建一个带有main()方法的Main类。然后,使用PersonGenerator类创建一个随机的Person对象列表:

        public class Main { 

          public static void main(String args[]) { 
            List<Person> persons = PersonGenerator.generatePersonList
                                                                (100);

  1. 我们将要实现的第一个收集操作将生成一个Map,其中的键将是人的名字,值将是具有该名字的所有人的列表。为了实现这一点,我们使用Stream类的collect()方法和Collectors.groupingByConcurrent收集器。然后,我们使用forEach()方法处理映射的所有键(名字),并在控制台打印出具有该键的人数:groupingByConcurrent()方法的参数是一个方法引用。如果我们像在这个例子中一样只调用现有方法,我们可以使用这种机制在 lambda 表达式中使用它。
        Map<String, List<Person>> personsByName = persons
                        .parallelStream().collect(Collectors
                        .groupingByConcurrent(Person::getFirstName)); 
        personsByName.keySet().forEach(key -> { 
          List<Person> listOfPersons = personsByName.get(key); 
          System.out.printf("%s: There are %d persons with that name\n",
                            key, listOfPersons.size());

  1. 我们将要实现的第二个收集操作将连接流中所有人的名字。为了实现这个操作,我们使用Person对象的toString()方法,Stream类的collect()方法,以及Collectors类的joining()方法,该方法将流的所有元素连接起来,并用指定的字符序列分隔:
        String message = persons.parallelStream().map
                  (p -> p.toString()).collect(Collectors.joining(",")); 
        System.out.printf("%s\n", message);

  1. 在我们接下来要实现的下一个收集操作中,我们将把流中的人员分成两组。第一组将包含薪水超过 50,000 的人员,第二组将包含其他人。操作的结果将是一个以Boolean值作为键、人员列表作为值的Map对象。为了实现这一点,我们将使用Stream类的collect()方法和接收作为参数的Collectors类的partitionBy()方法,该方法接收一个Boolean表达式,允许你将流元素分为truefalse。然后我们使用forEach()方法来写入生成的列表中的元素数量:
        Map<Boolean, List<Person>> personsBySalary = persons
                        .parallelStream().collect(Collectors
                        .partitioningBy(p -> p.getSalary() > 50000));

        personsBySalary.keySet().forEach(key -> { 
          List<Person> listOfPersons = personsBySalary.get(key); 
          System.out.printf("%s: %d \n", key, listOfPersons.size()); 
        });

  1. 然后,我们将实现一个收集操作,该操作将生成另一个Map。在这种情况下,键将是人员的名字,值将是具有相同名字的人的姓氏连接在一个String中。为了实现这种行为,我们使用了Stream类的collect()方法和Collectors类的toConcurrentMap()方法。我们将 lambda 表达式作为参数传递给该方法,以获取键,传递 lambda 表达式以获取值,以及传递 lambda 表达式以解决键在最终Map中存在的情况。然后,我们使用forEach()方法处理所有键并写入其关联的值:
        ConcurrentMap<String, String> nameMap = persons
                        .parallelStream().collect(Collectors
                        .toConcurrentMap(p -> p.getFirstName(),
                                         p -> p.getLastName(),
                                         (s1, s2) -> s1 + ", " + s2)); 
        nameMap.forEach((key, value) -> { 
          System.out.printf("%s: %s \n", key, value); 
        });

  1. 到目前为止,在我们实现的collect()方法的所有示例中,我们使用了接收Collector接口实现的该方法的版本。但是,还有另一个版本的collect()方法。使用这个版本的collect()方法,我们将实现一个收集操作,生成一个包含薪水超过 50,000 的人员的List。我们将创建List的表达式(List::new方法)、处理列表和流元素的 lambda 表达式,以及处理两个列表的表达式(List::addAll方法)传递给collect()方法:
        List<Person> highSalaryPeople = persons
                        .parallelStream().collect(
          ArrayList::new, (list, person) -> {  
            if (person.getSalary() > 50000) { 
              list.add(person); 
            } 
          }, 
          ArrayList::addAll 
        ); 
        System.out.printf("High Salary People: %d\n",
                          highSalaryPeople.size());

  1. 最后,我们将实现一个示例,生成一个包含 People 对象列表中出现的第一个名字及其出现次数的 ConcurrentHashMap。我们将使用人的名字作为键,Counter 对象作为值。collect 方法的第一个参数将创建一个新的 ConcurrentHashMap 对象。第二个参数是 BiConsumer 接口的实现,它接收一个 ConcurrentHashMap 和一个 Person 作为参数。首先,我们使用哈希的 computeIfPresent() 方法来增加人员的 Counter,如果人员存在。然后,我们使用哈希的 computeIfAbsent() 方法来插入一个新的人员名字,如果它不存在。collect() 方法的第三个参数是 BiConsumer 接口的实现,它接收两个 ConcurrentHashMap 对象,我们使用 merge() 方法来处理第二个哈希的所有元素,并在它们不存在时将它们插入第一个哈希中,或者在它们存在时增加计数器。
        System.out.printf("Collect, second example\n"); 
        ConcurrentHashMap<String, Counter> peopleNames = persons
                                .parallelStream().collect( 
          ConcurrentHashMap::new, (hash, person) -> { 
            hash.computeIfPresent(person.getFirstName(), (name,
                                                          counter) -> { 
              counter.increment(); 
              return counter; 
            }); 
            hash.computeIfAbsent(person.getFirstName(), name -> { 
              Counter c=new Counter(); 
              c.setValue(name); 
              return c; 
            }); 
          }, 
          (hash1, hash2) -> { 
            hash2.forEach (10, (key, value) -> { 
              hash1.merge(key, value, (v1,v2) -> { 
                v1.setCounter(v1.getCounter()+v2.getCounter()); 
                return v1; 
              }); 
            }); 
          }); 

          peopleNames.forEach((name, counter) -> { 
            System.out.printf("%s: %d\n", name, counter.getCounter()); 
          });

它是如何工作的...

正如我们在本食谱的介绍中提到的,collect() 方法允许你对 Stream 的元素进行可变归约。我们称之为可变归约,因为流最终的结果将是一个可变的数据结构,例如 MapList。Java 并发 API 的 Stream 类提供了两种 collect() 方法的版本。

第一个只接收一个参数,即 Collector 接口的实现。此接口有七个方法,所以你通常不会实现自己的收集器。相反,你将使用实用工具类 Collectors,它有很多方法可以返回用于你的归约操作的可重用 Collector 对象。在我们的示例中,我们使用了 Collectors 类的以下方法:

  • groupingByConcurrent(): 此方法返回一个 Collector 对象,该对象以并发方式对 Stream 的元素进行分组操作,生成 Map 作为结果数据结构。它接收一个表达式作为参数,用于从流元素中获取用于映射的键的值。它生成一个 Map,其中键的类型将是参数表达式返回的类型,值将是流元素的 List

  • joining(): 此方法返回将流元素连接到 StringCollector。你可以指定三个 CharSequence 对象作为元素的分隔符、最终 String 的前缀和后缀。

  • partitioningBy(): 此方法返回与第一个类似的 Collector。它接收一个 Boolean 表达式作为 Stream 的元素,并将流元素组织成两个组:满足表达式的元素和不满足表达式的元素。最终结果将是一个 Map,其键为 Boolean,值为流元素类型的 List

  • toConcurrentMap():此方法返回生成并发ConcurrentMapCollector。它接收三个参数:

    • 从流元素生成键的表达式

    • 从流元素生成值的表达式

    • 当存在两个或更多具有相同键的元素时,从两个值生成值的表达式

Collector有一组Characteristics定义其行为,并且可以为特定的收集器定义或未定义。对我们来说,最重要的是CONCURRENT特性,它表示收集器是否可以以并发方式工作。在这种情况下,我们无法通过仅创建并行流来利用我们的多核处理器。如果我们使用带有Collector的收集操作,我们必须也要考虑到该CollectorCONCURRENT特性的值。只有当以下三个条件都为真时,我们才会有一个并发的归约:

  • Stream是并行的(我们在流中使用了parallelStream()parallel()方法)

  • 收集器具有CONCURRENT特性

  • 要么流是无序的,要么收集器具有UNORDERED特性

在我们的情况下,groupingByConcurrent()toConcurrentMap()返回具有CONCURRENT特性的收集器,而joining()partitionBy()方法返回不具有此类特性的收集器。

然而,还有一个版本的collect()方法可以与并行流一起使用。这个版本的collect()方法接收以下三个参数:

  • 一个生成收集操作最终结果类型的数据结构的供应函数。在并行流中,此函数将被调用与执行操作的线程数量一样多次。

  • 一个累加函数,它接收一个数据结构和流的一个元素,并执行该元素的处理过程。

  • 一个组合函数,它接收两个数据结构并生成一个相同类型且唯一的组合数据结构。

您可以使用 lambda 表达式来实现这些函数,但也可以为供应函数实现Supplier接口或为累加和组合函数实现BiConsumer接口(始终使用适当的数据类型进行参数化)。如果输入和输出参数适当,您还可以使用方法引用(Class::Method)。例如,我们使用了List::new引用作为供应函数,以及List::addAll方法作为组合函数。我们也可以使用List::add方法作为累加函数。还有更多方法可以作为collect()方法的参数使用。

以下截图显示了groupingByConcurrent()操作的输出:

以下截图显示了toConcurrentMap()操作的输出:

还有更多...

Collectors 类有许多更多的方法,这些方法返回可以用于 collect() 方法的 Collector 对象。以下是最有趣的:

  • toList(): 此方法返回将 Stream 的所有元素分组到 List 中的 Collector

  • toCollection(): 此方法返回将 Stream 的所有元素分组到 Collection 中的 Collector。此方法返回一个表达式,该表达式创建 Collection,它将被 Collector 内部使用,并在其执行结束时返回。

  • averagingInt()averagingLong()averagingDouble():这些方法分别返回计算 intlongdouble 值平均值的 Collector。它们接收一个表达式作为参数,该表达式将流元素转换为 intlongdouble。这三个方法返回一个双精度值。

参见

  • 本章中的 从不同来源创建流减少流元素 菜谱

对流中的每个元素应用操作

在这个菜谱中,你将学习如何对流的每个元素应用操作。我们将使用三种方法:两个终端操作 forEach()forEachOrdered(),以及一个中间操作 peek() 方法。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用这些类。创建一个名为 Person 的类,具有人的基本特征。查看 从不同来源创建流 菜谱,以查看此类的源代码。

  2. 由于我们将使用依赖于流元素顺序的方法,我们必须在 Person 类中重写一些方法。首先,我们将重写 compareTo() 方法,该方法比较两个人员。我们将使用 Comparator 接口创建一个静态 Comparator 对象,以使用他们的姓氏和名字比较两个 Person 对象。然后,我们将使用该比较器在 compareTo() 方法中:

        private static Comparator<Person> comparator=Comparator
                                .comparing(Person::getLastName)
                                .thenComparing(Person::getFirstName); 

        @Override 
        public int compareTo(Person otherPerson) { 
          return comparator.compare(this, otherPerson); 
        }

  1. 然后,我们重写了 equals() 方法,该方法确定两个 Person 对象是否相等。正如我们在 compareTo() 方法中所做的那样,我们使用我们之前创建的 Comparator 静态对象。
        @Override 
        public boolean equals(Object object) { 
          return this.compareTo((Person)object)==0; 
        }

  1. 最后,我们重写了 hashCode() 方法,该方法为 Person 对象计算哈希值。在 Java 中,相等的对象必须产生相同的哈希码,因此我们必须重写此方法,并使用 Person 对象的姓氏和名字属性以及 Objects 类的 hash() 方法生成 Person 对象的哈希码:
        public int hashCode() { 
          String sequence=this.getLastName()+this.getFirstName(); 
          return sequence.hashCode(); 
        }

  1. 在此示例中,我们还将使用 从不同来源创建流 菜谱中使用的 PersonGeneratorDoubleGenerator 类。

  2. 现在,创建具有 main() 方法的 Main 类。首先,我们创建一个包含十个随机 Person 对象的 List

        public class Main { 

          public static void main(String[] args) { 
            List<Person> persons=PersonGenerator.generatePersonList(10);

  1. 然后,我们将使用forEach()方法写入生成的列表中所有人员的姓名。forEach()方法接收我们想要应用于每个元素的表达式作为参数。在我们的例子中,我们使用 lambda 表达式将信息写入控制台:
        persons.parallelStream().forEach(p -> { 
           System.out.printf("%s, %s\n", p.getLastName(),
                             p.getFirstName()); 
        });

  1. 然后,你将学习如何以有序的方式对每个元素应用操作。首先,我们使用DoubleGenerator类创建一个随机Double数字列表。然后,我们创建一个并行流,使用sorted()方法对流的元素进行排序,然后使用forEachOrdered()方法以有序的方式将数字写入控制台:
        List<Double> doubles= DoubleGenerator.generateDoubleList(10, 100); 
        System.out.printf("Parallel forEachOrdered() with numbers\n"); 
        doubles.parallelStream().sorted().forEachOrdered(n -> { 
          System.out.printf("%f\n",n); 
        });

  1. 现在,让我们看看如果你对流的元素进行排序但不使用forEachOrdered()方法会发生什么。重复之前的句子,但使用forEach()方法代替:
        System.out.printf("Parallel forEach() after sorted()
                           with numbers\n"); 
        doubles.parallelStream().sorted().forEach(n -> { 
          System.out.printf("%f\n",n); 
        });

  1. 然后,我们将测试forEachOrdered()方法与Person对象流的工作方式:
        persons.parallelStream().sorted().forEachOrdered( p -> { 
          System.out.printf("%s, %s\n", p.getLastName(),
                            p.getFirstName()); 
        });

  1. 最后,让我们测试peek()方法。此方法类似于forEach()方法,但它是一个中间操作。它通常用于日志目的:
        doubles 
          .parallelStream() 
          .peek(d -> System.out.printf("Step 1: Number: %f\n",d)) 
          .peek(d -> System.out.printf("Step 2: Number: %f\n",d)) 
          .forEach(d -> System.out.printf("Final Step: Number: %f\n",d));

它是如何工作的...

在这个菜谱中,你学习了如何使用三种方法来处理流的所有元素并对它们应用操作。这些方法包括:

  • forEach(): 这是一个终端操作,它对Stream中的所有元素应用操作并返回一个空值。它接收作为参数的动作,该动作定义为 lambda 表达式或Consumer接口的实现。对于并行流中的元素应用动作的顺序没有保证。

  • forEachOrdered(): 这是一个终端操作,它按照流的顺序对Stream中的所有元素应用操作,如果流是有序的,并且返回一个空值。你可以在sorted()方法之后使用此方法。你首先使用sorted()方法对流的元素进行排序,然后使用forEachOrdered()方法以有序的方式应用操作。在并行流中,这种行为也是保证的,但它的性能将比无序流的forEach()方法差。

  • peek(): 这是一个中间操作,它返回具有与调用该方法相同的流元素的Stream,并对从流中消耗的所有元素应用指定的操作。应用于元素的操作指定为 lambda 表达式或Consumer接口的实现。请注意,由于中间操作是懒加载的,该操作仅在终端操作执行时才会应用于流消耗的元素。

还有更多...

请注意,如果您使用排序方法,您必须提供一个可以应用于您想要排序的元素或流元素的 Comparator,或者流元素必须实现 Comparable 接口。在我们的例子中,Person 类实现了该接口,并提供了 compareTo() 方法来根据姓名的姓氏和名字对流的元素进行排序。

相关链接

  • 本章中的 从不同来源创建流减少流元素排序流元素 食谱

过滤流中的元素

您将对流应用的最常见操作之一将是过滤操作,该操作选择继续处理的元素。在本食谱中,您将学习 Stream 类提供的不同方法来选择流中的元素。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用。首先,实现存储个人基本属性的 Person 类,以及生成随机 Person 对象列表的 PersonGenerator 类。请参阅食谱 在流的所有元素上应用操作,以查看这两个类的源代码。

  2. 然后,我们将使用 main() 方法实现 Main 类。首先,使用 PersonGenerator 类创建一个包含随机 Person 对象的 List。使用 forEach() 方法打印生成的元素:

        public class Main { 
          public static void main(String[] args) { 
            List<Person> persons=PersonGenerator
                                        .generatePersonList(10); 
            persons.parallelStream().forEach(p-> { 
              System.out.printf("%s, %s\n", p.getLastName(),
                                p.getFirstName()); 
            });

  1. 然后,我们将使用 distinct() 方法消除重复的对象。使用 forEach() 方法写入通过过滤器的元素:
        persons.parallelStream().distinct().forEach(p-> { 
          System.out.printf("%s, %s\n", p.getLastName(),
                            p.getFirstName()); 
        });

  1. 然后,我们将使用数字数组测试 distinct() 方法。创建一个包含重复数字的数字数组。使用 Arrays 类的 asList() 方法将它们转换为 List。使用 parallelStream() 方法创建一个并行流,使用 mapToInt() 方法将流转换为 IntStream 流,使用 distinct() 方法删除重复元素,并最终使用 forEach() 方法将最终元素写入控制台:
        Integer[] numbers={1,3,2,1,2,2,1,3,3,1,1,3,2,1}; 
        Arrays.asList(numbers).parallelStream().mapToInt(n -> n)
                                        .distinct().forEach( n -> { 
          System.out.printf("Number: %d\n", n); 
        });

  1. 现在,我们将使用过滤方法和一个表示该条件的 lambda 表达式作为谓词,获取随机人员列表中薪水低于 3,000 的人员。与其他示例一样,使用 forEach() 方法写入结果元素:
        persons.parallelStream().filter(p -> p.getSalary() < 30000)
                                                .forEach( p -> { 
          System.out.printf("%s, %s\n", p.getLastName(),
                        p.getFirstName()); 
        });

  1. 然后,我们将使用 IntStream 测试 filter() 方法,获取小于两个的数字:
        Arrays.asList(numbers).parallelStream().mapToInt(n -> n)
                                .filter( n -> n<2).forEach(  n-> { 
          System.out.printf("%d\n", n); 
        });

  1. 现在,我们将使用 limit() 方法限制流中的元素数量。例如,从随机人员列表创建一个并行流,使用 mapToDouble() 方法将它们转换为 DoubleStream,并使用 limit() 方法获取前五个元素:
        persons.parallelStream().mapToDouble(p -> p.getSalary())
                                       .sorted().limit(5).forEach(s-> { 
          System.out.printf("Limit: %f\n",s); 
        });

  1. 最后,我们将使用skip()方法来忽略流中的某些元素。从随机的个人列表创建一个并行流,使用mapToDouble()方法将它们转换为DoubleStream,并使用skip()方法忽略前五个元素:
        persons.parallelStream().mapToDouble(p -> p.getSalary())
                                        .sorted().skip(5).forEach(s-> { 
          System.out.printf("Skip: %f\n",s); 
        });

它是如何工作的...

在这个菜谱中,我们使用了四种方法来过滤流中的元素。这些方法是:

  • distinct(): 此方法返回一个包含当前流中不同元素的流,这些元素根据Stream类元素的equals()方法。在我们的例子中,我们使用Person对象和int数字测试了此方法。我们在Person类中实现了equals()hashCode()方法。如果我们不这样做,equals()方法将仅在两个比较对象持有相同的引用时返回true。请注意,此操作是一个有状态的操作,因此它不会在并行流中(如 Java 文档所反映的,'... 在并行计算中,一些包含有状态中间操作的管道可能需要对数据进行多次遍历,或者可能需要缓冲大量数据...')获得良好的性能。

  • filter(): 此方法接收一个Predicate作为参数。此谓词可以表示为一个返回boolean值的 lambda 表达式。filter()方法返回一个包含使Predicate为真的元素的流。

  • limit(): 此方法接收一个int值作为参数,并返回一个不超过指定元素数量的流。此方法的性能也可能很差,尤其是在有序并行流中,特别是当你想要获取的元素数量很大时,因为此方法将返回流中的第一个元素,这将意味着额外的计算。在无序流中不会发生这种情况,因为在这种情况下,返回哪些元素并不重要。

  • skip(): 此方法返回一个流,其中包含在丢弃第一个元素之后的原始流中的元素。要丢弃的元素数量由此方法的参数指定。此方法与limit()方法具有相同的问题。

还有更多...

流类还有其他两个可以用来过滤流元素的方法:

  • dropWhile(): 此方法接收一个Predicate表达式作为参数。它在有序和无序流中有不同的行为。在有序流中,该方法从流中删除匹配谓词的第一个元素。当元素匹配谓词时删除元素。当它找到一个不匹配谓词的元素时,它停止删除元素并返回剩余的流。在无序流中,其行为是非确定性的。它删除匹配谓词的元素子集,但没有指定将删除哪些元素子集。与其他方法一样,它可能在并行有序流中表现不佳。

  • takeWhile():此方法与上一个方法等效,但它保留元素而不是删除它们。

参见

  • 本章中的 从不同来源创建流减少流中的元素收集流中的元素 配方

转换流中的元素

你可以使用流的一些最有用的中间操作来转换流中的元素。这些操作接收一个类的元素并返回另一个类的元素。你甚至可以更改流类型,并从 Stream 生成 IntStreamLongStreamDoubleStream

在这个配方中,你将学习如何使用 Stream 类提供的转换中间操作将元素转换为不同的类。

准备工作

本配方的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他如 NetBeans 之类的 IDE,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用。首先实现 Person 类,该类存储人的基本属性,以及 PersonGenerator 类,该类生成一个随机的 Person 对象的 List。请查看 将操作应用于流的所有元素 的配方,以查看这两个类的源代码。

  2. 创建一个名为 BasicPerson 的类。这个类将有一个名为 nameString 属性和一个名为 agelong 属性。创建获取和设置这两个属性值的方法。由于这个类的源代码非常简单,所以不会在这里包含。

  3. 创建另一个辅助类,命名为 FileGenerator。这个类将有一个名为 generateFile() 的方法,该方法接收模拟文件中的行数,并返回其内容作为一个 ListString

        public class FileGenerator { 
          public static List<String> generateFile(int size) { 
            List<String> file=new ArrayList<>(); 
            for (int i=0; i<size; i++) { 
              file.add("Lorem ipsum dolor sit amet,
                        consectetur adipiscing elit. Morbi lobortis
                        cursus venenatis. Mauris tempus elit ut 
                        malesuada luctus. Interdum et malesuada fames
                        ac ante ipsum primis in faucibus. Phasellus
                        laoreet sapien eu pulvinar rhoncus. Integer vel
                        ultricies leo. Donec vel sagittis nibh.
                        Maecenas eu quam non est hendrerit pu"); 
            } 
            return file; 
          } 
        }

  1. 然后,创建具有 main() 方法的 Main 类。首先,使用 PersonGenerator 类创建一个随机 Person 对象的列表:
        public class Main { 

          public static void main(String[] args) { 

            // Create list of persons 
            List<Person> persons = PersonGenerator.generatePersonList(100);

  1. 然后,我们将使用 mapToDouble() 方法将 Person 对象的流转换为 DoubleStream 的双精度值流。使用 parallelStream() 方法创建一个并行流,然后使用 mapToDouble() 方法,将一个接收 Person 对象并返回其薪资(一个双精度数字)的 lambda 表达式作为参数传递。然后使用 distinct() 方法获取唯一值,并使用 forEach() 方法将它们写入控制台。我们还可以使用 count() 方法获取写入的不同元素的数量:
        DoubleStream ds = persons.parallelStream().mapToDouble
                                                (p -> p.getSalary()); 
        ds.distinct().forEach(d -> { 
          System.out.printf("Salary: %f\n", d); 
        }); 
        ds = persons.parallelStream().mapToDouble(p -> p.getSalary()); 
        long size = ds.distinct().count(); 
        System.out.printf("Size: %d\n", size);

  1. 现在,我们将使用parallelStream()方法创建流并将Person对象转换为BasicPerson对象。使用map()方法转换对象。此方法接收一个 lambda 表达式作为参数,该表达式接收一个Person对象,创建一个新的BasicPerson对象,并设置其属性值。然后,我们使用forEach()方法写入BasicPerson对象的属性值:
        List<BasicPerson> basicPersons = persons.parallelStream().map
                                                                (p -> { 
          BasicPerson bp = new BasicPerson(); 
          bp.setName(p.getFirstName() + " " + p.getLastName()); 
          bp.setAge(getAge(p.getBirthDate())); 
          return bp; 
        }).collect(Collectors.toList()); 

        basicPersons.forEach(bp -> { 
          System.out.printf("%s: %d\n", bp.getName(), bp.getAge()); 
        });

  1. 最后,我们将学习如何管理中间操作返回Stream的情况。在这种情况下,我们将处理一个StreamStream,但我们可以使用flatMap()方法将这些Stream对象连接成一个唯一的Stream。使用FileGenerator类生成包含 100 个元素的List<String>。然后,使用parallelStream()方法创建一个并行流。我们将使用split()方法分割每一行以获取其单词,然后使用Stream类的of()方法将结果Array转换为Stream。如果我们使用map()方法,我们正在生成一个StreamStream,但使用flatMap()方法我们将得到一个包含整个 List 中所有单词的唯一StreamString对象。然后,我们使用filter()方法获取长度大于零的单词,使用sorted()方法对流进行排序,并使用groupingByConcurrent()方法将其收集到Map中,其中键是单词,值是每个单词在流中出现的次数:
        List<String> file = FileGenerator.generateFile(100); 
        Map<String, Long> wordCount = file.parallelStream()
          .flatMap(line -> Stream.of(line.split("[ ,.]"))) 
          .filter(w -> w.length() > 0).sorted()
          .collect(Collectors.groupingByConcurrent(e -> e, Collectors
            .counting())); 

        wordCount.forEach((k, v) -> { 
          System.out.printf("%s: %d\n", k, v); 
        });

  1. 最后,我们必须实现之前在代码中使用的getAge()方法。此方法接收一个Person对象的出生日期并返回其年龄:
        private static long getAge(Date birthDate) { 
          LocalDate start = birthDate.toInstant()
                        .atZone(ZoneId.systemDefault()).toLocalDate(); 
          LocalDate now = LocalDate.now(); 
          long ret = ChronoUnit.YEARS.between(start, now); 
          return ret; 
        }

它是如何工作的...

在这个菜谱中,你学习了如何使用中间操作和表达式在源类型和目标类型之间进行转换来转换流中的元素。在我们的示例中,我们使用了三种不同的方法:

  • mapToDouble():我们使用此方法将对象Stream转换为具有双数值元素的DoubleStream。此方法接收一个 lambda 表达式或ToDoubleFunction接口的实现作为参数。此表达式接收Stream的一个元素并必须返回一个双值。

  • map():当我们需要将Stream的元素转换为不同的类时,我们可以使用此方法。例如,在我们的情况下,我们将Person类转换为BasicPerson类。此方法接收一个 lambda 表达式或Function接口的实现作为参数。此表达式必须创建新对象并初始化其属性。

  • flatMap(): 这个方法在更复杂的情况下非常有用,当你需要处理一个Stream对象流,并希望将它们转换为一个唯一的Stream时。这个方法接收一个 lambda 表达式或Function接口的实现作为map()函数的参数,但在这个情况下,这个表达式必须返回一个Stream对象。flatMap()方法将自动将这些流连接成一个唯一的Stream

更多内容...

Stream类提供了其他方法来转换Stream的元素:

  • mapToInt()mapToLong():这些方法与mapToDouble()方法相同,但分别生成IntStreamLongStream对象。

  • flatMapToDouble()flatMapToInt()flatMapToLong():这些方法与flatMap()方法相同,但分别与DoubleStreamIntStreamLongStream一起工作。

相关内容

  • 本章中的从不同来源创建流减少流元素收集流元素菜谱

对流元素进行排序

你还希望对Stream执行另一个典型操作,那就是对其元素进行排序。例如,你可能希望按名称、邮政编码或其他任何数值对Stream的元素进行排序。

使用流时,我们还有其他考虑,所谓的遭遇顺序。一些流可能有一个定义好的遭遇顺序(这取决于Stream的来源)。一些操作使用流元素的遭遇顺序,例如limit()skip()等。这使得这些方法的并行计算性能不佳。在这些情况下,你可以通过删除排序约束来加速这些方法的执行。

在这个菜谱中,你将学习如何对Stream的元素进行排序,以及如何在不需要Stream的遭遇顺序的情况下删除排序约束。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用这些类。首先,实现Person类,它存储一个人的基本属性,以及PersonGenerator类,它生成一个随机的Person对象列表。请查看菜谱对流的全部元素执行操作以查看这两个类的源代码。

  2. 现在,实现Main类中的main()方法。首先,我们将创建一个int数字的Array。然后,我们将从这个数组中使用parallelStream()方法创建并行流,使用sorted()方法对数组元素进行排序,并使用forEachOrdered()方法以有序方式写入元素。请注意,此操作不会使用我们多核处理器的全部功能,因为它必须按指定顺序写入元素:

        public class Main { 
          public static void main(String args[]) { 
            int[] numbers={9,8,7,6,5,4,3,2,1,2,3,4,5,6,7,8,9}; 
            Arrays.stream(numbers).parallel().sorted().forEachOrdered
                                                                (n -> { 
            System.out.printf("%d\n", n); 
          });

  1. 现在,让我们用Person对象的Stream来尝试相同的原理。使用PersonGenerator类创建一个包含 10 个随机Person对象的列表,并使用相同的方法sorted()forEachOrdered()来查看人员是如何有序写入的:
        List<Person> persons=PersonGenerator.generatePersonList(10); 
        persons.parallelStream().sorted().forEachOrdered(p -> { 
          System.out.printf("%s, %s\n",p.getLastName(),p.getFirstName()); 
        });

  1. 最后,我们将通过使用unordered()方法来了解如何消除数据结构的遭遇顺序。首先,我们将从我们的随机Person对象List中创建TreeSet。我们使用TreeSet是因为它内部排序元素。然后,我们创建一个循环来重复操作十次,看看有序和无序操作之间的差异:
        TreeSet<Person> personSet=new TreeSet<>(persons); 
        for (int i=0; i<10; i++) {

  1. 然后,我们使用stream()方法从PersonSet创建流,将其转换为并行流,使用limit()方法获取第一个元素,并返回Person对象,将其收集到列表中并获取第一个元素:
        Person person= personSet.stream().parallel().limit(1)
                                .collect(Collectors.toList()).get(0); 
        System.out.printf("%s %s\n", person.getFirstName(),
                          person.getLastName());

  1. 现在,我们执行相同的操作,但在stream()parallel()方法之间使用unordered()方法来移除有序约束:
        person=personSet.stream().unordered().parallel().limit(1)
                                .collect(Collectors.toList()).get(0); 
        System.out.printf("%s %s\n", person.getFirstName(),
                          person.getLastName());

它是如何工作的...

Stream对象可能根据其来源和之前应用的中间操作而具有遭遇顺序。这种遭遇顺序对元素必须按顺序处理的方法施加了限制。例如,如果您在带有遭遇顺序的Stream中使用limit()skip()方法,它们将根据该遭遇顺序获取并忽略第一个元素。还有其他操作,如forEach()方法,不考虑遭遇顺序。如果您对具有遭遇顺序的流应用相同的操作,结果始终相同。如果流没有遭遇顺序,结果可能会有所不同。

当您使用顺序流时,遭遇顺序对应用程序的性能没有影响,但与并行流相比,它可能会产生很大影响。根据操作,可能需要多次处理Stream的元素或将大量数据存储在缓冲区中。在这种情况下,使用unordered()方法消除遭遇顺序,就像我们在本食谱中所做的那样,将显著提高应用程序的性能。

另一方面,sorted()方法对Stream的元素进行排序。如果你使用这个方法,Stream的元素必须实现Comparable接口。否则,你可以传递一个Comparator作为参数,该参数将被用来排序元素。如果你使用这个方法,你正在创建一个有序流,所以之前解释的所有关于具有遇到顺序的流的事情都适用于结果流。

最后,forEach()方法不考虑流的遇到顺序。如果你想考虑这个遇到顺序,比如说,在排序后写入流的元素顺序,你可以使用forEachOrdered()方法。

以下截图显示了示例的部分输出:

图片

你可以看到,当你调用从TreeSet生成的并行流的limit(1)方法时,你总是获得相同的结果,因为 Stream API 尊重该结构的遇到顺序。但是,当我们包含对unordered()方法的调用时,遇到顺序不被考虑,获得的结果应该会变化,就像这个例子一样。

更多...

当你使用unordered()方法时,你并没有执行任何内部改变数据结构中元素顺序的代码。你只是在删除一个在其他方法中会被考虑的条件。使用unordered()方法的流的结果可能与没有使用该方法相同流的结果相等。它的使用可能会在可能为并行流提供不同的处理结果时产生后果。例如,如果你尝试使用List中的Person对象而不是personSetTreeSet来运行我们的示例,你将在两种情况下都获得相同的结果。

正如我们之前提到的,unordered()方法的主要目的是删除限制并行流性能的约束。

参见

  • 本章中关于从不同来源创建流、减少流元素收集流元素的食谱

验证流元素的条件

Stream类提供的一个有趣选项是检查流元素是否满足条件。这个功能是由返回Boolean值的终端操作提供的。

在这个食谱中,你将学习哪些方法提供了Stream类来检查流元素的条件,以及如何使用它们。

准备工作

这个食谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 首先,我们将实现一些辅助类,我们将在示例中使用。首先,实现Person类,它存储一个人的基本属性,以及PersonGenerator类,它生成一个随机的Person对象列表。请查看菜谱Apply an action to all the elements of a stream以查看这两个类的源代码。

  2. 然后,创建带有main()方法的Main类。首先,我们将使用PersonGenerator类创建一个随机Person对象的List

        public class Main { 
          public static void main(String[] args) { 
            List<Person> persons=PersonGenerator.generatePersonList(10);

  1. 然后,计算薪水字段的最高和最低值,以验证所有计算都是正确的。我们使用两个流进行计算,第一个使用map()max()方法,第二个使用mapToInt()min()方法:
        int maxSalary = persons.parallelStream().map(p -> p.getSalary())
                                        .max(Integer::compare).get(); 
        int minSalary = persons.parallelStream().mapToInt(p -> p
                                        .getSalary()).min().getAsInt(); 
        System.out.printf("Salaries are between %d and %d\n", minSalary,
                          maxSalary);

  1. 现在,我们将测试一些条件。首先,让我们使用allMatch()方法和相应的 lambda 表达式来验证所有生成的Person对象都有一个大于零的薪水:
        boolean condition; 
        condition=persons.parallelStream().allMatch
                                              (p -> p.getSalary() > 0); 
        System.out.printf("Salary > 0: %b\n", condition);

  1. 我们重复条件以测试所有薪水是否大于 10,000 和 30,000。
        condition=persons.parallelStream().allMatch
                                          (p -> p.getSalary() > 10000); 
        System.out.printf("Salary > 10000: %b\n",condition); 
        condition=persons.parallelStream().allMatch
                                          (p -> p.getSalary() > 30000); 
        System.out.printf("Salary > 30000: %b\n",condition);

  1. 然后,我们将使用anyMatch()方法来测试是否有人的薪水大于 50,000 和 100,000:
        condition=persons.parallelStream().anyMatch
                                         (p -> p.getSalary() > 50000); 
        System.out.printf("Any with salary > 50000: %b\n",condition); 
        condition=persons.parallelStream().anyMatch
                                         (p -> p.getSalary() > 100000); 
        System.out.printf("Any with salary > 100000: %b\n",condition);

  1. 为了完成这个测试块,我们使用noneMatch()方法来验证没有人的薪水超过 100,000。
        condition=persons.parallelStream().noneMatch
                                         (p -> p.getSalary() > 100000); 
        System.out.printf("None with salary > 100000: %b\n",condition);

  1. 之后,我们使用findAny()方法来获取Person对象流中的一个随机元素:
        Person person = persons.parallelStream().findAny().get(); 
        System.out.printf("Any: %s %s: %d\n", person.getFirstName(),
                          person.getLastName(), person.getSalary());

  1. 然后,我们使用findFirst()方法来获取Person对象流中的第一个元素:
        person = persons.parallelStream().findFirst().get(); 
        System.out.printf("First: %s %s: %d\n", person.getFirstName(),
                          person.getLastName(), person.getSalary());

  1. 最后,我们使用sorted()方法按薪水对流进行排序,传递以 lambda 表达式表达的Comparator,并使用findFirst()方法获取,在这种情况下,薪水最低的Person对象:
        person = persons.parallelStream().sorted((p1,p2) -> {  
          return p1.getSalary() - p2.getSalary(); 
        }).findFirst().get(); 
        System.out.printf("First Sorted: %s %s: %d\n",
                          person.getFirstName(), person.getLastName(),
                          person.getSalary());

它是如何工作的...

在这个菜谱中,我们使用了三种不同的方法来验证 Stream 元素的条件:

  • allMatch():这个方法是一个终端操作,它接收一个作为参数的Predicate接口的实现,该实现以 lambda 表达式或实现它的对象的形式表达,并返回一个Boolean值。如果PredicateStream的所有元素都是真的,则返回true,否则返回false

  • anyMatch():这个方法是一个终端操作,它接收一个作为参数的Predicate接口的实现,该实现以 lambda 表达式或实现它的对象的形式表达,并返回一个Boolean值。如果PredicateStream中至少一个元素为真,则返回true,否则返回false

  • noneMatch():这个方法是一个终端操作,它接收一个作为参数的Predicate,该Predicate以 lambda 表达式或接口实现的形式表达,并返回一个Boolean值。如果Predicate对 Stream 的所有元素都是假的,则返回true,否则返回false

我们还使用了两种方法来获取Stream的元素:

  • findAny(): 此方法是一个终端操作,不接收参数,并返回一个用Stream元素的类参数化的Optional对象,包含Stream的某个元素。此方法返回的元素没有保证。如果Stream没有元素,返回的Optional对象将是一个空的。

  • findFirst(): 此方法是一个终端操作,不接收参数,并返回一个用Stream元素的类参数化的Optional。如果流有一个确定的遭遇顺序,它将返回Stream的第一个元素;如果没有遭遇顺序,它将返回任何元素。如果Stream没有元素,返回的Optional将是一个空的。

还有更多...

在这个食谱中,我们使用了 Java API 提供的一个接口和一个类。Predicate接口是一个函数式接口,通常用 lambda 表达式表示。这个表达式将接收一个Stream的元素并返回一个Boolean值。如果你想实现一个实现此接口的类,你只需要实现接收参数类型对象的test()方法并返回一个Boolean值。该接口定义了更多方法,但它们都有默认实现。

Stream的终端操作可能返回或不返回值时,使用Optional类。这样,Java 保证操作始终返回一个值,即Optional对象,该对象可能包含我们使用get()方法获取的值,也可能是一个空对象,我们可以使用isPresent()方法检查该条件。如果你使用get()方法与一个空的Optional对象,将抛出NoSuchElementException

参见

  • 本章中关于从不同来源创建流减少流元素收集流元素的食谱

响应式流编程

响应式流(www.reactive-streams.org/)定义了一种机制,以提供具有非阻塞背压的异步流处理。

响应式流基于以下三个元素:

  • 信息发布者

  • 一个或多个该信息的订阅者

  • 发布者和消费者之间的订阅

响应式流规范确定了这些类之间应该如何根据以下规则相互作用:

  • 发布者将添加想要被通知的订阅者

  • 订阅者在被添加到发布者时收到通知

  • 订阅者以异步方式从发布者请求一个或多个元素,也就是说,订阅者请求元素并继续执行。

  • 当发布者有要发布的元素时,它会将其发送给所有请求元素的订阅者

正如我们之前提到的,所有这些通信都是异步的,因此我们可以利用我们多核处理器的全部功能。

Java 9 包含了三个接口,即Flow.PublisherFlow.SubscriberFlow.Subscription,以及一个实用类SubmissionPublisher类,以允许我们实现响应式流应用程序。在这个食谱中,你将学习如何使用所有这些元素来实现一个基本的响应式流应用程序。

准备工作

这个食谱的示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Item的类,它将代表从发布者发送到订阅者的信息项。这个类有两个String属性,分别命名为titlecontent,以及获取和设置它们值的get()set()方法。它的源代码非常简单,所以这里不会包含它。

  2. 然后,创建一个名为Consumer1的类,并指定它实现了参数化为Item类的Subscriber接口。我们必须实现四个方法。首先,我们实现onComplete()方法。它只是向控制台发送一条消息:

        public class Consumer1 implements Flow.Subscriber<Item> { 

          @Override 
          public void onComplete() { 
            System.out.printf("%s: Consumer 1: Completed\n",
                              Thread.currentThread().getName()); 

          }

  1. 然后,我们实现onError()方法。它只是将关于错误的信息写入控制台:
        @Override 
        public void onError(Throwable exception) { 
          System.out.printf("%s: Consumer 1: Error\n",
                            Thread.currentThread().getName()); 
          exception.printStackTrace(System.err); 
        }

  1. 然后,我们实现onNext()方法。它只是将关于接收到的项目的信息写入控制台:
        @Override 
        public void onNext(Item item) { 
          System.out.printf("%s: Consumer 1: Item received\n",
                            Thread.currentThread().getName()); 
          System.out.printf("%s: Consumer 1: %s\n",
                            Thread.currentThread().getName(),
                            item.getTitle()); 
          System.out.printf("%s: Consumer 1: %s\n",
                            Thread.currentThread().getName(),
                            item.getContent()); 
        }

  1. 最后,我们实现onSubscribe()方法。它只是向控制台写入一条消息,并不使用Subscription对象的request()方法请求任何项目:
        @Override 
        public void onSubscribe(Flow.Subscription subscription) { 
          System.out.printf("%s: Consumer 1: Subscription received\n",
                            Thread.currentThread().getName()); 
          System.out.printf("%s: Consumer 1: No Items requested\n",
                            Thread.currentThread().getName()); 
        }

  1. 现在,是Consumer2类的时间了。指定它也实现了Subscriber接口,并使用Item类进行参数化。在这种情况下,我们有一个私有的Subscription属性来存储订阅对象。onComplete()onError()方法与Consumer1类中的方法等效:
        public class Consumer2 implements Flow.Subscriber<Item> { 

          private Subscription subscription; 

          @Override 
          public void onComplete() { 
            System.out.printf("%s: Consumer 2: Completed\n",
                              Thread.currentThread().getName());      
          }

          @Override 
          public void onError(Throwable exception) { 
            System.out.printf("%s: Consumer 2: Error\n",
                              Thread.currentThread().getName()); 
            exception.printStackTrace(System.err); 
          }

  1. onNext()方法还有一行额外的代码来请求另一个元素:
        @Override 
        public void onNext(Item item) { 
          System.out.printf("%s: Consumer 2: Item received\n",
                            Thread.currentThread().getName()); 
          System.out.printf("%s: Consumer 2: %s\n",
                            Thread.currentThread().getName(),
                            item.getTitle()); 
          System.out.printf("%s: Consumer 2: %s\n",
                            Thread.currentThread().getName(),
                            item.getContent()); 
          subscription.request(1); 
        }

  1. onSubscribe()方法还有一行额外的代码来请求第一个元素:
        @Override 
        public void onSubscribe(Flow.Subscription subscription) { 
          System.out.printf("%s: Consumer 2: Subscription received\n",
                            Thread.currentThread().getName()); 
          this.subscription=subscription; 
          subscription.request(1); 
        }

  1. 现在,实现一个名为Consumer3的类,并指定它实现了参数化为Item类的Subscriber接口。onComplete()onError()方法与之前类的那些方法等效:
        public class Consumer3 implements Flow.Subscriber<Item> { 

          @Override 
          public void onComplete() { 
            System.out.printf("%s: Consumer 3: Completed\n",
                              Thread.currentThread().getName()); 

          }


          @Override 
          public void onError(Throwable exception) { 
            System.out.printf("%s: Consumer 3: Error\n",
                              Thread.currentThread().getName()); 
            exception.printStackTrace(System.err); 
          }

  1. 在这种情况下,onNext()方法将关于项目的信息写入控制台,但不请求任何元素:
        @Override 
        public void onNext(Item item) { 
          System.out.printf("%s: Consumer 3: Item received\n",
                            Thread.currentThread().getName()); 
          System.out.printf("%s: Consumer 3: %s\n",
                            Thread.currentThread().getName(),
                            item.getTitle()); 
          System.out.printf("%s: Consumer 3: %s\n",
                            Thread.currentThread().getName(),
                            item.getContent()); 
        }

  1. onSubscribe()方法中,我们请求三个项目:
        @Override 
        public void onSubscribe(Flow.Subscription subscription) { 
          System.out.printf("%s: Consumer 3: Subscription received\n",
                            Thread.currentThread().getName()); 
          System.out.printf("%s: Consumer 3: Requested three items\n",
                            Thread.currentThread().getName()); 
          subscription.request(3); 
        }

  1. 最后,实现Main类中的main()方法。首先,创建三个消费者,每个类一个:
        public class Main { 
          public static void main(String[] args) { 

            Consumer1 consumer1=new Consumer1(); 
            Consumer2 consumer2=new Consumer2(); 
            Consumer3 consumer3=new Consumer3();

  1. 现在,创建一个参数化为Item类的SubmissionPublisher对象,并使用subscribe()方法添加三个消费者:
        SubmissionPublisher<Item> publisher=new SubmissionPublisher<>(); 

        publisher.subscribe(consumer1); 
        publisher.subscribe(consumer2); 
        publisher.subscribe(consumer3);

  1. 现在,创建十个Item对象,并使用SubmissionPublisher对象的submit()方法发布它们。每个项目之间等待一秒钟:
        for (int i=0; i<10; i++) { 
          Item item =new Item(); 
          item.setTitle("Item "+i); 
          item.setContent("This is the item "+i); 
          publisher.submit(item); 
          try { 
            TimeUnit.SECONDS.sleep(1); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 最后,使用close()方法关闭发布者:
            publisher.close(); 
          } 
        }

它是如何工作的...

反应式流的主要目标是提供一个机制,以非阻塞背压异步处理数据流。我们希望信息接收者优化他们的资源。由于机制是异步的,接收者不需要使用他们的资源来寻找新元素。当有新元素到来时,它们会被调用。非阻塞背压允许接收者在准备好的时候才消费新元素,这样他们可以使用一个有界队列来存储传入的元素,而不会被新元素的生产者饱和。

Java 中的反应式流基于三个接口:

  • Flow.Publisher:此接口只有一个方法:

    • subscribe():此方法接收一个Subscriber对象作为参数。发布者应该在发布Item时考虑这个订阅者。
  • Flow.Subscriber:此接口有四个方法:

    • onComplete():当Publisher完成其执行时,将调用此方法。

    • onError():当有必须通知订阅者的错误时,将调用此方法。

    • onNext():当Publisher有一个新元素时,将调用此方法。

    • onSubscribe():当发布者使用subscribe()方法添加订阅者时,将调用此方法。

  • Flow.Subscription:此接口有一个方法:

    • request():此方法由Subscriber用于从发布者请求一个元素。

请注意,这些只是接口,你可以按自己的意愿实现它们并使用它们。假设的流程如下:

  1. 有人调用Publishersubscribe()方法,并发送一个Subscriber

  2. Publisher创建一个Subscription对象并将其发送到SubscriberonSubscribe()方法。

  3. Subscriber使用Subscriptionrequest()方法向Publisher请求元素。

  4. 当发布者有要发布的元素时,它会将它们发送给所有请求元素的Subscribers,调用它们的onNext()方法。

  5. 当发布者结束其执行时,它会调用订阅者的onComplete()方法。

Java API 提供了实现Publisher接口并实现此行为的SubmissionPublisher类。

以下截图显示了示例的输出,你可以看到反应式流的预期行为:

图片

三个Subscriber对象接收它们的Subscription。由于Consumer1没有请求任何Item,所以它不会收到。Consumer3请求了三个,所以在示例的输出中,你会看到它将接收这三个Item对象。最后,Consumer2对象将接收十个Item对象以及关于Publisher执行结束的通知。

还有更多...

在使用反应式流时,还有一个额外的接口需要使用,那就是Flow.Processor接口,它将Flow.PublisherFlow.Subscriber接口组合在一起。其主要目的是作为一个元素,位于发布者和订阅者之间,将第一个产生的元素转换成第二个可以处理的格式。在一个链中可以拥有多个处理器,这样其中一个处理器的输出就可以被下一个处理器处理。

Java 还定义了一个Flow类,它包括了之前解释过的四个接口。

第七章:并发集合

在本章中,我们将涵盖以下主题:

  • 使用非阻塞线程安全的双端队列

  • 使用线程安全的阻塞双端队列

  • 使用按优先级排序的线程安全队列

  • 使用带有延迟元素的线程安全列表

  • 使用线程安全的可导航映射

  • 使用线程安全的 HashMap

  • 使用原子变量

  • 使用原子数组

  • 使用volatile关键字

  • 使用变量句柄

简介

数据结构是编程的基本元素。几乎每个程序都使用一种或多种数据结构来存储和管理数据。Java API 提供了Java 集合框架。它包含接口、类和算法,实现了许多不同的数据结构,您可以在程序中使用。

当您需要在并发程序中处理数据集合时,您必须非常小心地选择实现方式。大多数集合类不适用于并发应用程序,因为它们无法控制对它们数据的并发访问。如果一个并发任务共享一个无法与其他并发任务一起工作的数据结构,您可能会遇到数据不一致错误,这将影响程序的操作。这类数据结构的一个例子是ArrayList类。

Java 提供了数据收集过程,您可以在并发程序中使用而不会出现任何问题或不一致。基本上,Java 提供了两种类型的集合用于并发应用程序:

  • 阻塞集合:这类集合包括添加和删除数据操作。如果操作不能立即完成,因为集合已满或为空,发起调用的线程将阻塞,直到操作可以执行。

  • 非阻塞集合:这类集合也包括添加和删除数据操作。但在此情况下,如果操作不能立即完成,它将返回一个null值或抛出异常;发起调用的线程将不会在此处阻塞。

通过本章中的食谱,您将学习如何在并发应用程序中使用一些 Java 集合。这些包括:

  • 使用ConcurrentLinkedDeque类的非阻塞双端队列

  • 使用LinkedBlockingDeque类的阻塞双端队列

  • 用于数据生产者和消费者的阻塞队列,使用LinkedTransferQueue

  • 使用PriorityBlockingQueue类按优先级排序的阻塞队列

  • 使用带有延迟元素的阻塞队列,使用DelayQueue

  • 使用ConcurrentSkipListMap类的非阻塞可导航映射

  • 使用ConcurrentHashMap类的非阻塞哈希表

  • 使用AtomicLongAtomicIntegerArray类进行原子变量

  • 使用volatile关键字标记的字段存储的变量

  • 在单个类的字段上执行原子操作,使用变量句柄。

使用非阻塞线程安全的双端队列

“列表”被称为最基本的数据集合。它包含不确定数量的元素,你可以从任何位置添加、读取或移除一个元素。并发列表允许各种线程同时向列表添加或移除元素,而不会产生任何数据不一致错误。类似于列表,我们还有双端队列。双端队列是一种类似于队列的数据结构,但在双端队列中,你可以从前面(头部)或后面(尾部)添加或移除元素。

在这个菜谱中,你将学习如何在并发程序中使用非阻塞双端队列。非阻塞双端队列提供操作,如果立即不执行(例如,你想从列表中获取一个元素,但列表为空),则抛出异常或返回null值,具体取决于操作。Java 7 引入了ConcurrentLinkedDeque类,该类实现了一个非阻塞的并发双端队列。

我们将实现以下两个不同任务的示例:

  • 向双端队列添加数千个元素

  • 从双端队列中移除数据

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为AddTask的类并指定它实现Runnable接口:
        public class AddTask implements Runnable {

  1. 声明一个名为list的私有ConcurrentLinkedDeque属性,由String类参数化:
        private final ConcurrentLinkedDeque<String> list;

  1. 实现类的构造函数以初始化其属性:
        public AddTask(ConcurrentLinkedDeque<String> list) { 
         this.list=list; 
        }

  1. 实现类的run()方法。该方法将有一个 5000 次的循环。在每个循环中,我们将从双端队列中取出第一个和最后一个元素,因此我们将取出总共 10,000 个元素:
        @Override 
        public void run() { 
          String name=Thread.currentThread().getName(); 
          for (int i=0; i<10000; i++){ 
            list.add(name+": Element "+i); 
          } 
        }

  1. 创建一个名为PollTask的类并指定它实现Runnable接口:
        public class PollTask implements Runnable {

  1. 声明一个名为list的私有ConcurrentLinkedDeque属性,由String类参数化:
        private final ConcurrentLinkedDeque<String> list;

  1. 实现类的构造函数以初始化其属性:
        public PollTask(ConcurrentLinkedDeque<String> list) { 
          this.list=list; 
        }

  1. 实现类的run()方法。它通过循环以 5,000 步取出双端队列中的 10,000 个元素,每步移除两个元素:
        @Override 
        public void run() { 
          for (int i=0; i<5000; i++) { 
            list.pollFirst(); 
            list.pollLast(); 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建一个由String类参数化的名为listConcurrentLinkedDeque对象:
        ConcurrentLinkedDeque<String> list=new ConcurrentLinkedDeque<>();

  1. 创建一个名为threads的 100 个Thread对象数组:
        Thread threads[]=new Thread[100];

  1. 创建 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);

  1. 使用join()方法等待线程的完成:
        for (int i=0; i<threads.length; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 在控制台输出列表的大小:
        System.out.printf("Main: Size of the List: %d\n",list.size());

  1. 创建 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);

  1. 使用join()方法等待线程的最终化:
        for (int i=0; i<threads.length; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 在控制台输出列表的大小:
        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 值。当所有任务完成后,你在控制台输出了双端队列的元素数量。那时,列表中的元素数量为零。请注意,ConcurrentLinkedDeque 数据结构不允许你添加 null 值。

要写入双端队列中的元素数量,你使用了 size() 方法。你必须考虑到,此方法可能返回一个非实际值,特别是当你使用它在有线程向列表添加或从列表删除数据时。该方法必须遍历整个双端队列来计数元素,并且列表的内容可能会因为此操作而改变。只有在你使用它们而没有线程修改双端队列时,你才能保证返回的结果是正确的。

更多...

ConcurrentLinkedDeque 类提供了更多方法来从双端队列中获取元素:

  • getFirst()getLast(): 这些方法分别从双端队列中返回第一个和最后一个元素。它们不会从双端队列中移除返回的元素。如果双端队列为空,它们会抛出 NoSuchElementExcpetion 异常。

  • peek(), peekFirst(), 和 peekLast(): 这些方法分别返回双端队列的第一个和最后一个元素。它们不会从双端队列中移除返回的元素。如果双端队列为空,它们返回一个 null 值。

  • remove(), removeFirst(), 和 removeLast(): 这些方法分别返回双端队列的第一个和最后一个元素。它们也会移除返回的元素。如果双端队列为空,它们会抛出 NoSuchElementException 异常。

使用阻塞线程安全的双端队列

最基本的集合被称为列表。列表具有无限数量的元素,你可以从任何位置添加、读取或删除一个元素。并发列表允许各种线程同时向列表添加或删除元素,而不会产生任何数据不一致性。与列表类似,我们还有双端队列。双端队列是一种类似于队列的数据结构,但在双端队列中,你可以从前面(头部)或后面(尾部)添加或删除元素。

在此菜谱中,你将学习如何在你的并发程序中使用阻塞双端队列。阻塞双端队列与非阻塞双端队列之间的主要区别在于,阻塞双端队列具有插入和删除元素的方法,如果由于列表已满或为空而无法立即执行,则这些方法将阻塞调用线程,直到操作可以执行。Java 包括实现了阻塞双端队列的LinkedBlockingDeque类。

你将实现以下两个任务的示例:

  • 一个向双端队列添加数千个元素的操作

  • 一个从同一列表大量删除数据的操作

准备工作

此菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse

或者使用不同的 IDE,例如 NetBeans,打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤描述实现示例:

  1. 创建一个名为Client的类并指定它实现Runnable接口:
        public class Client implements Runnable{

  1. 声明一个名为requestList的私有LinkedBlockingDeque属性,该属性由String类参数化:
        private final LinkedBlockingDeque<String> requestList;

  1. 实现类的构造函数以初始化其属性:
        public Client (LinkedBlockingDeque<String> requestList) { 
          this.requestList=requestList; 
        }

  1. 实现该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 added: %s at %s.\n",request,
                                new Date()); 
            } 
            try { 
              TimeUnit.SECONDS.sleep(2); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
          System.out.printf("Client: End.\n"); 
        }

  1. 通过创建一个名为Main的类并添加main()方法来创建示例的主类:
        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 声明并创建一个名为listLinkedBlockingDeque,该双端队列由String类参数化,并指定固定大小为三:
        LinkedBlockingDeque<String> list=new LinkedBlockingDeque<>(3);

  1. 创建并启动一个Thread对象以执行客户端任务:
        Client client=new Client(list); 
        Thread thread=new Thread(client); 
        thread.start();

  1. 每 300 毫秒使用列表对象的take()方法从列表中获取三个String对象。重复此循环五次。将字符串写入控制台:
        for (int i=0; i<5 ; i++) { 
          for (int j=0; j<3; j++) { 
            String request=list.take(); 
            System.out.printf("Main: Removed: %s at %s. Size: %d\n",
                              request,new Date(), list.size()); 
          } 
          TimeUnit.MILLISECONDS.sleep(300); 
        }

  1. 编写一条消息以指示程序结束:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

在此菜谱中,你使用了由String类参数化的LinkedBlockingDeque,以处理非阻塞的并发数据双端队列。

Client类使用put()方法将字符串插入到双端队列中。如果双端队列已满(因为你已使用固定容量创建它),则该方法将阻塞其线程的执行,直到列表中有空余空间。

Main类使用take()方法从双端队列中获取字符串。如果双端队列为空,则该方法将阻塞其线程的执行,直到双端队列中有元素。

在本例中使用的 LinkedBlockingDeque 类的这两种方法如果在被阻塞时被中断,可能会抛出一个 InterruptedException 异常。因此,你必须包含必要的代码来捕获这个异常。

更多内容...

LinkedBlockingDeque 类还提供了插入和从双端队列获取元素的方法,这些方法不是阻塞的,而是抛出异常或返回 null 值。这些方法如下:

  • takeFirst()takeLast():这些方法分别返回双端队列的第一个和最后一个元素。它们会从双端队列中移除返回的元素。如果双端队列为空,它们会阻塞线程,直到双端队列中有元素为止。

  • getFirst()getLast():这些方法分别返回双端队列的第一个和最后一个元素。它们不会从双端队列中移除返回的元素。如果双端队列为空,它们抛出一个 NoSuchElementException 异常。

  • peek()peekFirst()peekLast()peekFirst()peekLast() 方法分别返回双端队列的第一个和最后一个元素。它们不会从双端队列中移除返回的元素。如果双端队列为空,它们返回一个 null 值。

  • poll()pollFirst()pollLast()pollFirst()pollLast() 方法分别返回双端队列的第一个和最后一个元素。它们会从双端队列中移除返回的元素。如果列表为空,它们返回一个 null 值。

  • add()addFirst()addLast()addFirst()addLast() 方法分别将元素添加到第一个和最后一个位置。如果双端队列已满(使用固定容量创建),它们会抛出一个 IllegalStateException 异常。

参见

  • 本章中使用的 使用非阻塞线程安全的双端队列 烹饪配方

使用按优先级排序的阻塞线程安全队列

当你与数据结构一起工作时,你可能通常会感到需要有一个有序队列。Java 提供了具有此功能的 PriorityBlockingQueue

你想要添加到 PriorityBlockingQueue 中的所有元素都必须实现 Comparable 接口;或者,你可以在队列的构造函数中包含 Comparator。此接口有一个名为 compareTo() 的方法,它接收相同类型的对象。因此,你有两个对象要比较:一个是执行方法的对象,另一个是作为参数接收的对象。如果本地对象小于参数,该方法必须返回一个小于零的数字。如果本地对象大于参数,它应该返回一个大于零的数字。如果两个对象都相等,数字必须为零。

当你在 PriorityBlockingQueue 中插入一个元素时,PriorityBlockingQueue 使用 compareTo() 方法来确定插入元素的位置。较大的元素将根据 compareTo() 方法是队列的尾部还是头部。

PriorityBlockingQueue 的另一个重要特性是它是一个 阻塞数据结构。它具有方法,如果无法立即执行操作,将阻塞线程,直到它们能够执行。

在本食谱中,您将通过实现一个示例来学习如何使用 PriorityBlockingQueue 类,在该示例中,您将在同一个列表中存储具有不同优先级的大量事件,以检查队列是否按您希望的方式排序。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他不同的 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

或打开不同的 IDE,例如 NetBeans,创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Event 的类,并指定它实现由 Event 类参数化的 Comparable 接口:
        public class Event implements Comparable<Event> {

  1. 声明一个名为 thread 的私有 int 属性,用于存储创建事件的线程数:
        private final int thread;

  1. 声明一个名为 priority 的私有 int 属性,用于存储事件的优先级:
        private final int priority;

  1. 实现类的构造函数以初始化其属性:
        public Event(int thread, int priority){ 
          this.thread=thread; 
          this.priority=priority; 
        }

  1. 实现返回线程属性值的 getThread() 方法:
        public int getThread() { 
          return thread; 
        }

  1. 实现返回优先级属性值的 getPriority() 方法:
        public int getPriority() { 
          return priority; 
        }

  1. 实现比较方法 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; 
          } 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 声明一个名为 id 的私有 int 属性,用于存储标识任务的数字:
        private final int id;

  1. 声明一个名为 queue 的私有 PriorityBlockingQueue 属性,参数化为 Event 类,用于存储任务生成的事件:
        private final PriorityBlockingQueue<Event> queue;

  1. 实现类的构造函数以初始化其属性:
        public Task(int id, PriorityBlockingQueue<Event> queue) { 
          this.id=id; 
          this.queue=queue; 
        }

  1. 实现 run() 方法。在队列中存储 1,000 个事件,使用其 ID 来标识创建事件的任务,并为每个事件分配一个从 1 到 1000 的不同优先级。使用 add() 方法将事件存储在队列中:
        @Override 
        public void run() { 
          for (int i=0; i<1000; i++){ 
            Event event=new Event(id,i); 
            queue.add(event); 
          } 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main{ 
          public static void main(String[] args) {

  1. 创建一个名为 queuePriorityBlockingQueue 对象,参数化为 Event 类:
        PriorityBlockingQueue<Event> queue=new PriorityBlockingQueue<>();

  1. 创建一个包含五个 Thread 对象的数组以存储将执行五个任务的线程:
        Thread taskThreads[]=new Thread[5];

  1. 创建五个 Task 对象。将线程存储在之前创建的数组中:
        for (int i=0; i<taskThreads.length; i++){ 
          Task task=new Task(i,queue); 
          taskThreads[i]=new Thread(task); 
        }

  1. 启动之前创建的五个线程:
        for (int i=0; i<taskThreads.length ; i++) { 
          taskThreads[i].start(); 
        }

  1. 使用 join() 方法等待五个线程的最终化:
        for (int i=0; i<taskThreads.length ; i++) { 
          try { 
            taskThreads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 在控制台中写入队列的实际大小和存储在其中的事件。使用 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()); 
        }

  1. 向控制台写入队列的最终大小信息:
        System.out.printf("Main: Queue Size: %d\n",queue.size()); 
        System.out.printf("Main: End of the program\n");

它是如何工作的...

在这个例子中,你使用PriorityBlockingQueue实现了Event对象的优先队列。如介绍中所述,存储在PriorityBlockingQueue中的所有元素都必须实现Comparable接口或向队列的构造函数提供Comparator对象。在这种情况下,你采用了第一种方法,因此在Event类中实现了compareTo()方法。

所有事件都有一个优先级属性。优先级值较高的元素将是队列中的第一个元素。当你实现compareTo()方法时,如果执行此方法的事件的优先级高于作为参数传递的事件的优先级,它返回-1作为结果。在另一种情况下,如果执行此方法的事件的优先级低于作为参数传递的事件的优先级,它返回1作为结果。如果两个对象具有相同的优先级,compareTo()方法返回0。在这种情况下,PriorityBlockingQueue类不保证元素的顺序。

我们实现了Task类,以便将Event对象添加到优先队列中。每个任务对象使用add()方法将 1,000 个事件添加到队列中,优先级在0999之间。

Main类的main()方法创建了五个Task对象,并在相应的线程中执行它们。当所有线程完成执行后,你将所有元素写入控制台。要从队列中获取元素,我们使用了poll()方法。此方法返回并移除队列中的第一个元素。

以下截图显示了程序执行输出的一部分:

图片

你可以看到队列有 5,000 个元素,以及第一个元素具有最大的优先级值。

还有更多...

PriorityBlockingQueue类还有其他一些有趣的方法。以下是一些方法的描述:

  • clear(): 此方法移除队列中的所有元素。

  • take(): 此方法返回并移除队列的第一个元素。如果队列是空的,它将阻塞其线程,直到队列有元素。

  • put(E e): 这是用于参数化PriorityBlockingQueue类的类。它将作为参数传递的元素插入到队列中。

  • peek(): 此方法返回队列的第一个元素,但不移除它。

参见

  • 本章中的使用阻塞线程安全的双端队列配方

使用带有延迟元素的线程安全列表

Java API 提供的一个有趣的数据结构,你可以在并发应用程序中使用,是在 DelayQueue 类中实现的。在这个类中,你可以存储具有激活日期的元素。返回或从队列中提取元素的方法将忽略这些元素,其数据将在未来出现。它们对这些方法不可见。为了获得这种行为,你想要存储在 DelayQueue 类中的元素需要实现 Delayed 接口。此接口允许你处理延迟对象。此接口有一个 getDelay() 方法,它返回元素激活的时间。此接口强制你实现以下两个方法:

  • compareTo(Delayed o): Delayed 接口扩展了 Comparable 接口。如果执行此方法的对象具有比作为参数传递的对象更小的延迟,则此方法将返回一个小于零的值。如果执行此方法的对象具有比作为参数传递的对象更大的延迟,则返回一个大于零的值。如果两个对象具有相同的延迟,则返回零。

  • getDelay(TimeUnit unit): 此方法必须返回直到激活日期剩余的时间,以单位为单位,由单位参数指定。TimeUnit 类是一个枚举,具有以下常量:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

在本例中,你将学习如何通过在其中存储具有不同激活日期的一些事件来使用 DelaydQueue 类。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse

或者使用不同的 IDE,例如 NetBeans,打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 Event 的类,并指定它实现 Delayed 接口:
        public class Event implements Delayed {

  1. 声明一个名为 startDate 的私有 Date 属性:
        private final Date startDate;

  1. 实现类的构造函数以初始化其属性:
        public Event (Date startDate) { 
          this.startDate=startDate; 
        }

  1. 实现 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; 
        }

  1. 实现 getDelay() 方法。返回对象开始日期与实际日期在 TimeUnit 中的差值,该值作为参数接收:
        public long getDelay(TimeUnit unit) {   
          Date now=new Date(); 
          long diff=startDate.getTime()-now.getTime(); 
          return unit.convert(diff,TimeUnit.MILLISECONDS); 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 声明一个名为 id 的私有 int 属性,用于存储一个标识此任务的数字:
        private final int id;

  1. 声明一个名为 queue 的私有 DelayQueue 属性,该属性由 Event 类参数化:
        private final DelayQueue<Event> queue;

  1. 实现类的构造函数以初始化其属性:
        public Task(int id, DelayQueue<Event> queue) { 
          this.id=id; 
          this.queue=queue; 
        }

  1. 实现 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);

  1. 使用 add() 方法在队列中存储 100 个事件:
          for (int i=0; i<100; i++) { 
            Event event=new Event(delay); 
            queue.add(event); 
          }   
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 创建一个由 Event 类参数化的 DelayQueue 对象:
        DelayQueue<Event> queue=new DelayQueue<>();

  1. 创建一个包含五个 Thread 对象的数组以存储你将要执行的任务:
        Thread threads[]=new Thread[5];

  1. 创建具有不同 ID 的五个 Task 对象:
        for (int i=0; i<threads.length; i++){ 
          Task task=new Task(i+1, queue); 
          threads[i]=new Thread(task); 
        }

  1. 启动之前创建的所有五个任务:
        for (int i=0; i<threads.length; i++) { 
          threads[i].start(); 
        }

  1. 使用 join() 方法等待线程的最终化:
        for (int i=0; i<threads.length; i++) { 
          threads[i].join(); 
        }

  1. 将队列中存储的事件写入控制台。当队列的大小大于零时,使用 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 对象存储在 DelayQueue 类中。

getDelay() 方法返回激活日期和实际日期之间的纳秒数。这两个日期都是 Date 类的对象。你使用了 getTime() 方法,它返回一个转换为毫秒的日期。然后,你将此值转换为 TimeUnit,它作为参数接收。DelayQueue 类在纳秒级别工作,但此时对你来说是透明的。

如果执行该方法的对象的延迟小于作为参数传递的对象的延迟,则 compareTo() 方法返回一个小于零的值。如果执行该方法的对象的延迟大于作为参数传递的对象的延迟,则返回一个大于零的值。如果两个延迟都相等,则返回 0

你还实现了 Task 类。这个类有一个名为 idinteger 属性。当 Task 对象被执行时,它将等于任务 ID 的秒数添加到实际日期,这指的是 DelayQueue 类中由该任务存储的事件的激活日期。每个 Task 对象使用 add() 方法在队列中存储 100 个事件。

最后,在 Main 类的 main() 方法中,你创建了五个 Task 对象,并在相应的线程中执行它们。当这些线程完成执行后,你使用控制台中的 poll() 方法写入所有事件。此方法检索并移除队列中的第一个元素。如果没有活动元素,则返回 null 值。你调用 poll() 方法,如果它返回 Event 类,则增加一个计数器。当它返回 null 值时,你在控制台中写入计数器的值,并将线程休眠半秒钟以等待更多活动事件。当你获得了队列中存储的 500 个事件后,程序执行结束。

以下截图显示了程序执行的部分输出:

你可以看到当程序被激活时,它只获得了 100 个事件。

你必须非常小心地使用size()方法。它返回列表中包括活动和非活动元素的总元素数。

更多...

DelayQueue类还有其他有趣的方法,如下所示:

  • clear(): 此方法移除队列中的所有元素。

  • offer(E e): 在这里,E代表用于参数化DelayQueue类的类。此方法将作为参数传递的元素插入到队列中。

  • peek(): 此方法检索但不移除队列的第一个元素。

  • take(): 此方法检索并移除队列的第一个元素。如果没有活动元素,执行此方法的线程将被阻塞,直到线程获得一些活动元素。

参见

  • 本章中的使用阻塞线程安全的队列配方

使用线程安全的可导航映射

ConcurrentNavigableMap是一个接口,它定义了 Java API 提供的有趣的数据结构,你可以在你的并发程序中使用这些数据结构。实现ConcurrentNavigableMap接口的类将元素存储在两部分:

  • 一个,用于唯一标识一个元素

  • 定义元素的其余数据,称为

Java API 还提供了一个实现ConcurrentSkipListMap的类,这是一个实现具有ConcurrentNavigableMap接口行为的非阻塞列表的接口。内部,它使用跳表来存储数据。跳表是一种基于并行列表的数据结构,它允许我们获得与二叉树相关的效率。你可以在en.wikipedia.org/wiki/Skip_list上获取更多关于跳表的信息。有了它,你可以获得一个排序的数据结构,而不是一个排序的列表,并且具有更好的插入、搜索或删除元素的访问时间。

跳表(Skip List)是由威廉·普(William Pugh)在 1990 年提出的。

当你向映射中插入一个元素时,映射使用键来排序它们;因此,所有元素都将排序。键必须实现Comparable接口,或者你必须向映射的构造函数提供一个Comparator类。该类还提供了获取映射子映射的方法,以及返回具体元素的方法。

在这个配方中,你将学习如何使用ConcurrentSkipListMap类来实现联系人映射。

准备工作

本配方的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等不同的 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Contact的类:
        public class Contact {

  1. 声明两个名为namephone的私有String属性:
        private final String name; 
        private final String phone;

  1. 实现类的构造函数以初始化其属性:
        public Contact(String name, String phone) { 
          this.name=name; 
          this.phone=phone; 
        }

  1. 实现返回namephone属性值的方法:
        public String getName() { 
          return name; 
        } 

        public String getPhone() { 
          return phone; 
        }

  1. 创建一个名为Task的类并指定它实现Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为 map 的私有 ConcurrentSkipListMap 属性,由 StringContact 类参数化:
        private final ConcurrentSkipListMap<String, Contact> map;

  1. 声明一个名为 id 的私有 String 属性来存储当前任务的 ID:
        private final String id;

  1. 实现类的构造函数以存储其属性:
        public Task (ConcurrentSkipListMap<String, Contact> map,String id){ 
          this.id=id; 
          this.map=map; 
        }

  1. 实现 run() 方法。它使用任务的 ID 和递增的数字来创建 Contact 对象,并将 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); 
          }     
        }

  1. 通过创建一个名为 Main 的类并将 main() 方法添加到其中来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个由 StringConctact 类参数化的名为 mapConcurrentSkipListMap 对象:
        ConcurrentSkipListMap<String, Contact> map = new
                                          ConcurrentSkipListMap<>();

  1. 创建一个用于存储所有即将执行的 Task 对象的 26 个 Thread 对象数组:
        Thread threads[]=new Thread[26]; 
        int counter=0;

  1. 创建并启动 26 个 task 对象,并将一个大写字母分配给每个任务的 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++; 
        }

  1. 使用 join() 方法等待线程的最终化:
        for (Thread thread : threads){ 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 使用 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());

  1. 使用 lastEntry() 方法获取地图的最后一个条目。将其数据写入控制台:
        element=map.lastEntry(); 
        contact=element.getValue(); 
        System.out.printf("Main: Last Entry: %s: %s\n", contact.getName(),
                          contact.getPhone());

  1. 使用 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() 方法创建了 26 个 Task 对象,使用 A 到 Z 之间的字母作为 ID。然后,你使用了一些方法从地图中获取数据。firstEntry() 方法返回一个包含地图第一个元素的 Map.Entry 对象。此方法不会从地图中删除元素。该对象包含键和元素。要获取元素,你调用了 getValue() 方法。你可以使用 getKey() 方法来获取该元素的键。

lastEntry() 方法返回一个包含地图最后一个元素的 Map.Entry 对象。subMap() 方法返回包含地图部分元素的 ConcurrentNavigableMap 对象,在这种情况下,键在 A1996B1002 之间的元素。你使用了 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): 如果指定的键存在于映射中,此方法将替换与该键关联的值。

参见

  • 本章中的使用非阻塞线程安全的队列菜谱

使用线程安全的 HashMap

哈希表是一种数据结构,允许你将键映射到值。内部通常使用数组来存储元素,并使用哈希函数来计算元素在数组中的位置,使用其键。这种数据结构的主要优点是,插入、删除和搜索操作在这里都非常快,因此在需要执行大量搜索操作的情况下非常有用。

Java API 通过MapConcurrentMap接口提供了不同的哈希表实现。ConcurrentMap接口为所有操作提供了线程安全和原子保证,因此你可以在并发应用程序中使用它们。ConcurrentHashMap类实现了ConcurrentMap接口,并添加了一些接口中定义的方法。此类支持以下功能:

  • 读取操作的全并发性

  • 插入和删除操作的高预期并发性

这两个元素(类和接口)都是在 Java 版本 5 中引入的,但在版本 8 中,开发了许多类似于流 API 提供的方法。

在这个菜谱中,你将学习如何在你的应用程序中使用ConcurrentHashMap类以及它提供的重要方法。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为Operation的类,具有三个属性:一个名为userString属性,一个名为operationString属性,以及一个名为timeDate属性。添加获取和设置属性值的 方法。这个类的代码非常简单,所以不会在这里展示。

  2. 创建一个名为HashFiller的类。指定它实现Runnable接口:

        public class HashFiller implements Runnable {

  1. 声明一个名为userHash的私有ConcurrentHashMap属性。哈希表的键将是一个String类型,其值将是Operation对象的ConcurrentLinkedDeque对象。实现类的构造函数以初始化属性:
        private ConcurrentHashMap<String, ConcurrentLinkedDeque<Operation>>
                userHash; 

        public HashFiller(ConcurrentHashMap<String, ConcurrentLinkedDeque
                          <Operation>> userHash) { 
          this.userHash = userHash; 
        }

  1. 实现run()方法。我们将用 100 个随机的Operation对象填充ConcurrentHashMap。首先,生成随机数据,然后使用addOperationToHash()方法将对象插入到哈希表中:
        @Override 
        public void run() { 

          Random randomGenerator = new Random(); 
          for (int i = 0; i < 100; i++) { 
            Operation operation = new Operation(); 
            String user = "USER" + randomGenerator.nextInt(100); 
            operation.setUser(user); 
            String action = "OP" + randomGenerator.nextInt(10); 
            operation.setOperation(action); 
            operation.setTime(new Date()); 

            addOperationToHash(userHash, operation); 
          } 
        }

  1. 实现addOperationToHash()方法。它接收哈希表和要添加的操作作为参数。映射中的键将是分配给操作的用户。我们使用computeIfAbsent()方法来获取与键关联的ConcurrentLinkedDeque对象。如果键存在,此方法返回与它关联的值。如果不存在,它执行传递给此方法的 lambda 表达式来生成值并将其与键关联。在这种情况下,我们生成一个新的ConcurrentLinkedDeque对象。最后,将操作插入到队列中:
        private void addOperationToHash(ConcurrentHashMap<String,
                                        ConcurrentLinkedDeque<Operation>>
                                        userHash, Operation operation) { 

          ConcurrentLinkedDeque<Operation> opList = userHash
                                   .computeIfAbsent(operation.getUser(),
                                   user -> new ConcurrentLinkedDeque<>()); 

          opList.add(operation); 
        }

  1. 现在实现Main类并包含main()方法。首先,声明一个ConcurrentHashMap对象和一个HashFiller任务:
        ConcurrentHashMap<String, ConcurrentLinkedDeque<Operation>>
          userHash = new ConcurrentHashMap<>(); 
        HashFiller hashFiller = new HashFiller(userHash);

  1. 使用HashFiller类执行 10 个线程,并使用join()方法等待它们的最终化:
        Thread[] threads = new Thread[10]; 
        for (int i = 0; i < 10; i++) { 
          threads[i] = new Thread(hashFiller); 
          threads[i].start(); 
        } 

        for (int i = 0; i < 10; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 现在,提取ConcurrentHashMap的信息。首先,使用size()方法提取其中存储的元素数量。然后,使用forEach()方法对存储在哈希表中的所有元素应用一个操作。第一个参数是并行度阈值。这是使操作以并发方式执行所需的最小元素数量。我们指定了值 10,哈希表有 100 个元素,因此操作将以并行方式执行。Lambda 表达式接收两个参数:键和值。打印存储为值的ConcurrentLinkedDeque的键和大小:
        System.out.printf("Size: %d\n", userHash.size()); 

        userHash.forEach(10, (user, list) -> { 
          System.out.printf("%s: %s: %d\n", Thread.currentThread()
                            .getName(), user, list.size()); 
        });

  1. 然后,使用forEachEntry()方法。这与前面的方法类似,但 lambda 表达式接收一个Entry对象作为参数,而不是接收两个参数。您可以使用此条目对象来获取键和值:
        userHash.forEachEntry(10, entry -> { 
          System.out.printf("%s: %s: %d\n", Thread.currentThread()
                            .getName(), entry.getKey(), 
          entry.getValue().size()); 
        });

  1. 然后,使用search()方法来查找满足指定搜索函数的第一个元素。在我们的例子中,我们搜索操作码以 1 结尾的操作。与forEach()方法类似,我们指定一个并行度阈值:
        Operation op = userHash.search(10, (user, list) -> { 
          for (Operation operation : list) { 
            if (operation.getOperation().endsWith("1")) { 
              return operation; 
            } 
          } 
          return null; 
        }); 

        System.out.printf("The operation we have found is: %s, %s, %s,\n",
                          op.getUser(), op.getOperation(), op.getTime());

  1. 再次使用search()方法,但这次,使用它来查找拥有超过 10 个操作的用戶:
        ConcurrentLinkedDeque<Operation> operations = userHash.search(10,
                                                          (user, list) -> { 
          if (list.size() > 10) { 
            return list; 
          } 
          return null; 
        }); 

        System.out.printf("The user we have found is: %s: %d operations\n",
                          operations.getFirst().getUser(),
                          operations.size());

  1. 最后,使用reduce()方法计算存储在哈希表中的操作总数:
            int totalSize = userHash.reduce(10, (user, list) -> { 
              return list.size(); 
            }, (n1, n2) -> { 
              return n1 + n2; 
            }); 

            System.out.printf("The total size is: %d\n", totalSize); 
          } 
        }

它是如何工作的...

在这个菜谱中,我们实现了一个使用ConcurrentHashMap来存储用户执行的操作信息的应用程序。内部,哈希表使用Operation类的用户属性作为键,使用ConcurrentLinkedDeque(一个非阻塞的并发列表)作为其值来存储与该用户关联的所有操作。

首先,我们使用 10 个不同的线程使用一些随机数据填充了哈希。为此,我们实现了HashFiller任务。这些任务的最大问题是当你需要在哈希表中插入一个键时会发生什么。如果有两个线程同时想要添加相同的键,你可能会丢失一个线程插入的数据,并出现数据竞争条件。为了解决这个问题,我们使用了computeIfAbsent()方法。

此方法接收一个键和一个可以表示为 lambda 表达式的Function接口实现;键和实现作为参数接收。如果键存在,该方法返回与键关联的值。如果不存在,该方法将执行指定的Function对象,并将Function返回的键和值添加到 HashMap 中。在我们的情况下,键不存在,因此我们创建了一个新的ConcurrentLinkedDeque类实例。此方法的主要优势是它是原子执行的;因此,如果另一个线程尝试执行相同的操作,它将被阻塞,直到此操作完成。

然后,在main()方法中,我们使用了ConcurrentHashMap的其他方法来处理存储在哈希中的信息。我们使用了以下方法:

  • forEach():此方法接收一个可以表示为 lambda 表达式的BiConsumer接口实现;它作为参数接收。此表达式的其他两个参数代表我们正在处理的元素的键和值。此方法将表达式应用于存储在ConcurrentHashMap中的所有元素。

  • forEachEntry():此方法与上一个方法等效,但这里的表达式是Consumer接口的实现。它接收一个Entry对象作为参数,该对象存储我们正在处理的条目的键和值。这是表达相同功能的一种方式。

  • search():此方法接收可以表示为 lambda 表达式的BiFunction接口实现;它作为参数接收。此函数还接收我们正在处理的ConcurrentHashMap对象的条目的键和值作为参数。它返回BiFunction返回的第一个非空值。

  • reduce():此方法接收两个BiFunction接口,以将ConcurrentHashMap的元素减少到唯一值。这允许您使用ConcurrentHashMap的元素实现MapReduce操作。第一个BiFunction接口允许您将元素的键和值转换为唯一值,第二个BiFunction接口允许您聚合两个不同元素的值。

到目前为止所描述的所有方法都有一个名为 parallelismThreshold 的第一个参数。此参数被描述为 ...执行此操作所需的(估计)元素数量...,也就是说,如果 ConcurrentHashMap 的元素少于参数中指定的值,则方法以顺序方式执行。相反(如在我们的情况下),方法以并行方式执行。

还有更多...

ConcurrentHashMap 包含比上一节中指定的更多方法。以下列表中我们列举了一些:

  • forEachKey()forEachValue(): 这些方法与 forEach() 方法类似,但在此情况下,表达式分别处理存储在 ConcurrentHashMap 中的键和值。

  • searchEntries(), searchKeys(), 和 searchValues(): 这些方法与之前解释的 search() 方法类似。然而,在这种情况下,作为参数传递的表达式接收一个 Entry 对象、一个键或存储在 ConcurrentHashMap 中的元素的值。

  • reduceEntries(), reduceKeys(), 和 reduceValues(): 这些方法与之前解释的 reduce() 方法类似。然而,在这种情况下,作为参数传递的表达式接收一个 Entry 对象、一个键或存储在 ConcurrentHashMap 中的元素的值。

  • reduceXXXToDouble(), reduceXXXToLong(), 和 reduceXXXToInt(): 这些方法允许您通过生成 doublelongint 值来对 ConcurrentHashMap 的元素进行归约。

  • computeIfPresent(): 此方法补充了 computeIfAbsent() 方法。在这种情况下,它接收一个键和一个 BiFunction 接口的实现,该接口可以用 lambda 表达式表示。如果键存在于 HashMap 中,该方法将表达式应用于计算键的新值。BiFunction 接口接收键和该键的实际值作为参数,并返回新值。

  • merge(): 此方法接收一个键、值和 BiFunction 接口的实现,该接口可以用 lambda 表达式表示;它们作为参数接收。如果键不存在于 ConcurrentHashMap 中,则将其插入并关联值参数。如果它存在,则执行 BiFunction 来计算与键关联的新值。BiFunction 接口接收键及其实际值作为参数,并返回与键关联的新值。

  • getOrDefault(): 此方法接收一个键和一个默认值作为参数。如果键存在于 ConcurrentHashMap 中,它返回其关联的值。否则,它返回默认值。

参见

  • 本章中 使用线程安全的可导航映射 的食谱

  • 第六章 “减少流元素”的食谱,并行和响应式流

使用原子变量

原子变量是在 Java 5 版本中引入的,以提供对单个变量的原子操作。当您使用普通变量时,您在 Java 中实现的每个操作都会转换为 JVM 可理解的 Java 字节码的多个指令。例如,当您将值赋给变量时,您在 Java 中只使用一个指令;然而,当您编译此程序时,它被转换为 JVM 语言的多种指令。当您与多个线程共享变量时,这可能导致数据不一致错误。

为了避免这些问题,Java 引入了原子变量。当一个线程正在使用原子变量执行操作,并且如果有其他线程想要对同一个变量执行操作,类的实现包括一个机制来检查操作是否是原子执行的。基本上,操作获取变量的值,即变量的旧值。

  1. 您获取变量的值,即变量的旧值。

  2. 您在一个时间变量中更改变量的值,这是变量的新值。

  3. 如果旧值等于变量的实际值,则用新值替换旧值。如果另一个线程改变了变量的值,旧值可能与实际值不同。

其中一些变量,例如LongAccumulator类,接收一个作为参数的操作,该操作可以在其某些方法内部执行。这些操作必须没有副作用,因为它们可能在每次值更新时多次执行。

原子变量不使用锁或其他同步机制来保护对其值的访问。它们的所有操作都基于比较和设置。保证几个线程可以同时使用一个原子变量,而不会产生数据不一致错误;此外,它们简化了实现。

Java 8 添加了四个新的原子类。首先我们有LongAdderDoubleAdder类;它们存储频繁由不同线程更新的longdouble值。您可以使用AtomicLong类获得与LongAdder类相同的功能,但前者提供了更好的性能。其他两个类是LongAccumulatorDoubleAccumulator。这些类与前面的类类似,但在这里,您必须在构造函数中指定两个参数:

  • 计数器的初始值。

  • 一个 LongBinaryOperatorDoubleBinaryOperator,可以表示为 lambda 表达式。这个表达式接收变量的旧值和要应用的增量,并返回变量的新值。

在本食谱中,您将学习如何使用实现银行账户和两个不同任务的原子变量:一个向账户添加资金,另一个从账户中减去资金。您将在示例实现中使用 AtomicLong 类。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为 Account 的类来模拟银行账户:
        public class Account {

  1. 声明一个名为 balance 的私有 AtomicLong 属性来存储账户的余额。此外,声明一个名为 operations 的私有 LongAdder 属性和一个名为 commission 的私有 DoubleAccumulator 属性:
        private final AtomicLong balance; 
        private final LongAdder operations; 
        private final DoubleAccumulator commission;

  1. 实现类的构造函数以初始化其属性。对于 DoubleAccumulator 类,身份值是 0,我们通过将参数传递的增量乘以 0.2 来更新实际值:
        public Account() { 
          balance = new AtomicLong(); 
          operations = new LongAdder(); 
          commission = new DoubleAccumulator((x,y)-> x+y*0.2, 0); 
        }

  1. 实现获取三个属性值的方法:
        public long getBalance() { 
          return balance.get(); 
        } 
        public long getOperations() { 
          return operations.longValue(); 
        } 
        public double getCommission() { 
          return commission.get(); 
        }

  1. 实现一个名为 setBalance() 的方法来设置余额属性值。我们还需要使用 reset() 方法来初始化操作和佣金属性:
        public void setBalance(long balance) { 
          this.balance.set(balance); 
          operations.reset(); 
          commission.reset(); 
        }

  1. 实现一个名为 addAmount() 的方法来增加 balance 属性的值。此外,使用 LongAdder 类的 increment() 方法增加 operations 属性的值,并通过 accumulate() 方法增加一个单位来将金额值的 20%添加到 commission 对象:
        public void addAmount(long amount) { 
          this.balance.getAndAdd(amount); 
          this.operations.increment(); 
          this.commission.accumulate(amount);
        }

  1. 实现一个名为 substractAmount() 的方法来减少 balance 属性的值。与 addAmount() 方法类似,我们修改 operationscommission 属性的值:
        public void subtractAmount(long amount) { 
          this.balance.getAndAdd(-amount); 
          this.operations.increment(); 
          this.commission.accumulate(amount);
        }

  1. 创建一个名为 Company 的类并指定它实现 Runnable 接口。这个类将模拟公司所做的支付:
        public class Company implements Runnable {

  1. 声明一个名为 account 的私有 Account 属性:
        private final Account account;

  1. 实现类的构造函数以初始化其属性:
        public Company(Account account) { 
          this.account=account; 
        }

  1. 实现任务的 run() 方法。使用账户的 addAmount() 方法在其余额中增加 10 次,每次增加 1,000:
        @Override 
        public void run() { 
          for (int i=0; i<10; i++){ 
            account.addAmount(1000); 
          } 
        }

  1. 创建一个名为 Bank 的类并指定它实现 Runnable 接口。这个类将模拟从账户中取款:
        public class Bank implements Runnable {

  1. 声明一个名为 account 的私有 Account 属性:
        private final Account account;

  1. 实现类的构造函数以初始化其属性:
        public Bank(Account account) { 
          this.account=account; 
        }

  1. 实现任务的 run() 方法。使用账户的 subtractAmount() 方法从其余额中减少 10 次,每次减少 1,000:
        @Override 
        public void run() { 
          for (int i=0; i<10; i++){ 
            account.subtractAmount(1000); 
          } 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个 Account 对象并将其余额设置为 1000
        Account  account=new Account(); 
        account.setBalance(1000);

  1. 创建一个新的 Company 任务和一个线程来执行它:
        Company company=new Company(account);
        Thread companyThread=new Thread(company);

  1. 创建一个新的 Bank 任务和一个线程来执行它:
        Bank bank=new Bank(account);
        Thread bankThread=new Thread(bank);

  1. 在控制台写入账户的初始余额:
        System.out.printf("Account : Initial Balance: %d\n",
                          account.getBalance());

  1. 启动线程:
        companyThread.start(); 
        bankThread.start();

  1. 使用 join() 方法等待线程最终化,并在控制台写入最终余额、操作次数和账户的累计佣金:
        try { 
          companyThread.join(); 
          bankThread.join(); 
          System.out.printf("Account : Final Balance: %d\n",
                            account.getBalance()); 
          System.out.printf("Account : Number of Operations: %d\n",
                            account.getOperations().intValue()); 
          System.out.printf("Account : Accumulated commisions: %f\n",
                            account.getCommission().doubleValue()); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

它是如何工作的...

这个例子中的关键是 Account 类。在这个类中,我们声明了一个名为 balanceAtomicLong 变量来存储账户的余额,一个名为 operationsLongAdder 变量来存储我们对账户进行的操作次数,以及一个名为 commissionDoubleAccumulator 变量来存储操作的佣金值。在 commission 对象的构造函数中,我们指定了值将按表达式 0.2*y 增加的。通过这种方式,我们想要指定我们将变量的实际值增加其与 0.2 的乘积以及传递给 accumulate() 方法的参数值。

为了实现返回 balance 属性值的 getBalance() 方法,我们使用了 AtomicLong 类的 get() 方法。为了实现返回操作次数的 getOperations() 方法,我们使用了 longValue() 方法。为了实现设置 balance 属性值的 setBalance() 方法,我们使用了 AtomicLong 类的 set() 方法。

为了实现向账户余额添加金额的 addAmount() 方法,我们使用了 AtomicLong 类的 getAndAdd() 方法,该方法返回值并按参数指定的值增加它。我们还使用了 LongAdder 类的 increment() 方法,该方法将变量的值增加 1,以及 DoubleAccumulator 类的 accumulate() 方法来按指定表达式增加 commission 属性的值。请注意,尽管 addAmount() 方法调用了三个原子操作,但它作为一个整体不是原子的。

最后,为了实现减少 balance 属性值的 subtractAmount() 方法,我们使用了 getAndAdd() 方法。我们还包含了调用 LongAdderDoubleAccumulator 类的 increment()accumulate() 方法。

然后,我们实现了两个不同的任务:

  • Company 类模拟了一个公司,它会增加账户的余额。这个类的每个任务都会进行 10 次每次增加 1,000 的操作。

  • Bank 类模拟了一个银行,其中银行账户的所有者可以取出其资金。这个类的每个任务都会进行 10 次每次减去 1,000 的操作。

Main 类中,您创建了一个具有 1,000 元余额的 Account 对象。然后,您执行了银行任务和公司任务,因此账户的最终余额与初始余额相同。

当您执行程序时,您将看到最终余额与初始余额相同。以下截图显示了此示例的执行输出:

图片

更多内容...

如介绍中所述,Java 中还有其他原子类。AtomicBooleanAtomicIntegerAtomicReference 是原子类的其他示例。

LongAdder 类提供了以下其他有趣的方法:

  • add(): 通过指定为参数的值增加内部计数器的值

  • decrement(): 通过一个减少内部计数器

  • reset(): 将内部值重置为零

您还可以使用类似于 LongAdderDoubleAdder 类,但它没有 increment()decrement() 方法,内部计数器是一个 double 值。

您还可以使用类似于 DoubleAccumulatorLongAccumulator 类,但内部计数器是一个 long 值。

参见

  • 在 第二章 中同步方法基本线程同步

使用原子数组

假设您需要实现一个由多个线程共享一个或多个对象的并发应用程序。在这种情况下,您必须使用同步机制(如锁或 synchronized 关键字)来保护对其属性的访问,以避免数据不一致错误。

这些机制存在以下问题:

  • 死锁:当线程阻塞等待其他线程锁定的锁时,该线程永远不会释放它,这种情况发生。这种情况会阻塞程序,因此它永远不会完成。

  • 如果只有一个线程访问共享对象,它必须执行获取和释放锁所需的代码。

为了在此情况下提供更好的性能,开发了 比较并交换操作。此操作通过以下三个步骤实现变量的值修改:

  1. 您获取变量的值,即变量的旧值。

  2. 您在一个临时变量中更改变量的值,该临时变量是变量的新值。

  3. 如果旧值等于变量的实际值,则用新值替换旧值。如果另一个线程已更改它,则旧值可能不同于实际值。

使用此机制,您不需要使用同步机制,因此可以避免死锁并获得更好的性能。此机制也有其缺点。操作必须没有任何副作用,因为它们可能会在具有高度竞争资源的 livelocks 中重试;与标准锁相比,它们也难以监控性能。

Java 在原子变量中实现了这种机制。这些变量提供了compareAndSet()方法,这是比较并交换操作的实现,以及基于它的其他方法。

Java 还引入了原子数组,它为integerlong数字的数组提供了原子操作。在本例中,你将学习如何使用AtomicIntegerArray类来处理原子数组。请注意,如果你使用AtomicInteger[],它不是一个线程安全的对象。单个AtomicInteger对象是线程安全的,但数组作为数据结构不是。

准备工作

本例的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Incrementer的类并指定它实现Runnable接口:
        public class Incrementer implements Runnable {

  1. 声明一个名为vector的私有AtomicIntegerArray属性,用于存储integer数字数组:
        private final AtomicIntegerArray vector;

  1. 实现类的构造函数以初始化其属性:
        public Incrementer(AtomicIntegerArray vector) { 
          this.vector=vector; 
        }

  1. 实现该run()方法。使用getAndIncrement()方法增加数组中所有元素的值:
        @Override 
        public void run() { 
          for (int i=0; i<vector.length(); i++){ 
            vector.getAndIncrement(i); 
          } 
        }

  1. 创建一个名为Decrementer的类并指定它实现Runnable接口:
        public class Decrementer implements Runnable {

  1. 声明一个名为vector的私有AtomicIntegerArray属性,用于存储integer数字数组:
        private AtomicIntegerArray vector;

  1. 实现类的构造函数以初始化其属性:
        public Decrementer(AtomicIntegerArray vector) { 
          this.vector=vector; 
        }

  1. 实现该run()方法。使用getAndDecrement()方法减少数组中所有元素的值:
        @Override 
        public void run() { 
          for (int i=0; i<vector.length(); i++) { 
            vector.getAndDecrement(i); 
          }   
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 声明一个名为THREADS的常量并将值100分配给它。创建一个包含 1,000 个元素的AtomicIntegerArray对象:
        final int THREADS=100; 
        AtomicIntegerArray vector=new AtomicIntegerArray(1000);

  1. 创建一个名为Incrementer的任务来处理之前创建的原子数组:
        Incrementer incrementer=new Incrementer(vector);

  1. 创建一个Decrementer任务来处理之前创建的原子数组:
        Decrementer decrementer=new Decrementer(vector);

  1. 创建两个数组来存储 100 个Thread对象:
        Thread threadIncrementer[]=new Thread[THREADS]; 
        Thread threadDecrementer[]=new Thread[THREADS];

  1. 创建并启动 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(); 
        }

  1. 使用join()方法等待线程的最终化:
        for (int i=0; i<100; i++) { 
          try { 
            threadIncrementer[i].join(); 
            threadDecrementer[i].join();

          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 在控制台,输出与零不同的原子数组元素。使用get()方法获取原子数组的元素:
        int errors=0;  
        for (int i=0; i<vector.length(); i++) { 
          if (vector.get(i)!=0) { 
            System.out.println("Vector["+i+"] : "+vector.get(i)); 
            errors++; 
          } 
        } 
        if (errors==0) { 
          System.out.printf("No errors found\n"); 
        }

  1. 在控制台输出一条消息,指示示例的最终化:
        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): 建立由参数指定的数组位置的值。

参见

  • 本章中 使用原子变量 的配方

使用 volatile 关键字

几乎每个应用程序都会读取和写入计算机的主内存中的数据。出于性能原因,这些操作不是直接在内存中执行的。CPU 有一个缓存内存系统,因此应用程序将数据写入缓存,然后数据从缓存移动到主内存。

在多线程应用程序中,并发线程在不同的 CPU 或 CPU 内部的内核中运行。当一个线程修改存储在内存中的变量时,修改是在它运行的缓存或 CPU 或内核中进行的。然而,没有保证这种修改何时会达到主内存。如果另一个线程想要读取数据的值,它可能无法读取修改后的值,因为它不在计算机的主内存中。

为了解决这个问题(还有其他解决方案,例如synchronized关键字),Java 语言包括volatile关键字。这是一个修饰符,允许你指定变量必须始终从主内存中读取和存储,而不是你的 CPU 缓存。当其他线程需要看到变量的实际值时,你应该使用 volatile 关键字;然而,访问该变量的顺序并不重要。在这种情况下,volatile 关键字将为你提供更好的性能,因为它不需要获取任何监视器或锁来访问变量。相反,如果访问变量的顺序很重要,你必须使用另一种同步机制。

在这个配方中,你将学习如何使用 volatile 关键字及其使用效果。

准备工作

本配方示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或不同的 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为Flag的类,其中有一个名为flag的公共Boolean属性,初始化为true值:
        public class Flag { 
          public boolean flag=true; 
        }

  1. 创建一个名为VolatileFlag的类,并有一个名为flag的公共布尔属性,初始化为true值。我们在这个属性的声明中添加了volatile修饰符:
        public class VolatileFlag { 
          public volatile boolean flag=true; 
        }

  1. 创建一个名为Task的类,并指定它实现Runnable接口。它有一个私有的Flag属性和一个用于初始化它的构造函数:
        public class Task implements Runnable { 
          private Flag flag; 
          public Task(Flag flag) { 
            this.flag = flag; 
          }

  1. 实现这个任务的run()方法。当flag属性的值为true时,它将增加一个int变量的值。然后,写入变量的最终值:
        @Override 
        public void run() { 
          int i = 0; 

          while (flag.flag) { 
            i++; 
          } 
          System.out.printf("VolatileTask: Stopped %d - %s\n", i,
                            new Date()); 
        }

  1. 创建一个名为VolatileTask的类,并指定它实现Runnable接口。它有一个私有的VolatileFlag属性和一个用于初始化它的构造函数:
        public class VolatileTask implements Runnable { 

          private VolatileFlag flag; 
          public VolatileTask(VolatileFlag flag) { 
            this.flag = flag; 
          }

  1. 实现这个任务的run()方法。它与Task类中的方法相同,所以这里不会包括它:

  2. 实现带有main()方法的Main类。首先,创建四个VolatileFlagFlagVolatileTaskTask类的对象:

        public class Main { 

          public static void main(String[] args) { 
            VolatileFlag volatileFlag=new VolatileFlag(); 
            Flag flag=new Flag(); 

            VolatileTask vt=new VolatileTask(volatileFlag); 
            Task t=new Task(flag);

  1. 然后,创建两个线程来执行任务,启动它们,并让主线程休眠一秒钟:
        Thread thread=new Thread(vt); 
        thread.start(); 
        thread=new Thread(t); 
        thread.start(); 

        try { 
          TimeUnit.SECONDS.sleep(1); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 然后,将volatileFlag变量的值更改以停止volatileTask的执行,并让主线程休眠一秒钟:
        System.out.printf("Main: Going to stop volatile task: %s\n",
                          new Date()); 
        volatileFlag.flag=false; 
        System.out.printf("Main: Volatile task stoped: %s\n", new Date()); 

        try { 
          TimeUnit.SECONDS.sleep(1); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 最后,将task对象的值更改以停止任务的执行,并让主线程休眠一秒钟:
        System.out.printf("Main: Going to stop task: %s\n", new Date()); 
        flag.flag=false; 
        System.out.printf("Main: Volatile stop flag changed: %s\n",
                          new Date()); 

        try { 
          TimeUnit.SECONDS.sleep(1); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

它是如何工作的...

以下截图显示了示例的输出:

图片

应用程序没有完成其执行,因为task线程还没有完成。当我们更改volatileFlag的值——因为它的flag属性被标记为volatile——新的值会被写入主内存,并且VolatileTask会立即访问这个值并完成其执行。相反,当你更改flag对象的值——因为它的flag属性没有被标记为 volatile——新的值会被存储在主线程的缓存中,任务对象看不到这个新值并且永远不会结束其执行。volatile关键字之所以重要,不仅是因为它要求写入被刷新,而且还因为它确保读取不会被缓存,并且它们从主内存中获取最新的值。这非常重要,而且经常被忽视。

考虑到volatile关键字保证了修改会被写入主内存,但它的相反情况并不总是成立。例如,如果你与一个由多个线程共享的非 volatile 整数值工作,并且进行了很多修改,你可能能够看到其他线程所做的修改,因为它们被写入主内存。然而,没有保证这些更改从缓存传递到主内存。

更多...

当共享变量的值只被一个线程修改时,volatile关键字才能很好地工作。如果变量被多个线程修改,volatile关键字不能保护你免受可能的数据竞争条件的影响。它也不使操作,如+-,原子化。例如,对 volatile 变量的++操作不是线程安全的。

自 Java 5 以来,Java 内存模型通过volatile关键字建立了 happens-before 保证。这一事实有两个影响:

  • 当你修改一个 volatile 变量时,它的值会被发送到主内存。同一线程之前修改的所有变量的值也会被发送。

  • 编译器不能为了优化目的重新排序修改 volatile 变量的句子。它可以重新排序之前的操作和之后的操作,但不能重新排序 volatile 变量的修改。这些修改之前发生的变化将对这些指令可见。

参见

  • 本章中的使用原子变量使用原子数组菜谱

使用变量句柄

可变句柄是 Java 9 的一个新特性,它允许你获取一个变量的类型化引用(属性、静态字段或数组元素),以便以不同的模式访问它。例如,你可以通过允许对变量的原子访问来保护在并发应用程序中对这个变量的访问。到目前为止,你只能通过原子变量获得这种行为,但现在,你可以使用可变句柄来获得相同的功能,而不需要使用任何同步机制。可变句柄还允许你获取变量的附加访问模式。

在这个菜谱中,你将学习如何获取和使用可变句柄,以及使用它所获得的益处。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等不同的 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Account的类,有两个名为amountunsafeAmount的公共 double 属性。实现构造函数以初始化其值:
        public class Account { 
          public double amount; 
          public double unsafeAmount; 

          public Account() { 
            this.amount=0; 
            this.unsafeAmount=0; 
          } 
        }

  1. 创建一个名为Decrementer的类,并指定它实现Runnable接口。它有一个名为Account的私有属性,在类的构造函数中初始化:
        public class Decrementer implements Runnable { 

          private Account account; 
          public Decrementer(Account account) { 
            this.account = account; 
          }

  1. 实现方法run()。这个方法将在amountunsafeAmount属性上执行 10,000 次递减操作。要修改amount属性的值,使用VarHandle。通过使用MethodHandles类的lookup()方法获取它,然后使用getAndAdd()方法修改属性的值。要修改unsafeAmount属性,使用=运算符:
        @Override 
        public void run() { 
          VarHandle handler; 
          try { 
            handler = MethodHandles.lookup().in(Account.class)
                        .findVarHandle(Account.class, "amount",
                        double.class); 
            for (int i = 0; i < 10000; i++) { 
              handler.getAndAdd(account, -100); 
              account.unsafeAmount -= 100; 
            } 
          } catch (NoSuchFieldException | IllegalAccessException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现一个名为Incrementer的类。这将与Decrementer类等效,但它会增加账户的值。这个类的源代码将不会在这里包括。

  2. 最后,实现Main类中的main()方法。首先,创建一个account对象:

        public class Main { 
          public static void main(String[] args) { 
            Account account = new Account();

  1. 然后,创建一个线程来执行Incrementer任务,并创建一个线程来执行Decrementer任务。启动它们并使用join()方法等待它们的最终化:
        Thread threadIncrementer = new Thread(new Incrementer(account)); 
        Thread threadDecrementer = new Thread(new Decrementer(account)); 

        threadIncrementer.start(); 
        threadDecrementer.start(); 

        try { 
          threadIncrementer.join(); 
          threadDecrementer.join(); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 最后,在控制台中写入amountunsafeAmount属性的值:
            System.out.printf("Safe amount: %f\n", account.amount); 
            System.out.printf("Unsafe amount: %f\n", account.unsafeAmount); 

          } 
        }

它是如何工作的...

以下截图显示了应用程序执行的结果:

图片

当你执行相同数量的增加和减少操作时,两种情况下的预期结果都是0。我们通过amount属性获得这个结果,因为我们使用VarHandle访问它时,我们保证了对其修改的原子访问。另一方面,unsafeAmount没有预期的值。对这个值的访问没有得到保护,我们有一个数据竞争条件。

要使用变量句柄,首先我们必须使用MethodHandles类的lookup()方法来获取它,然后是in()方法,然后是findVarHandle()方法。lookup()方法返回一个Lookup对象,in()方法返回指定类的Lookup对象——在我们的例子中是Account类,而findVarHandle()生成我们想要访问的属性的VarHandle

一旦我们有了VarHandle对象,我们就可以使用不同的方法来使用不同的访问模式。在这个例子中,我们使用了getAndAdd()方法。这个方法保证了增加属性值的原子访问。我们向它们传递我们想要访问的对象和增加的值。

下一节提供了有关不同访问模式和每种情况下可以使用的方法的更多信息。

还有更多...

你有四种不同的访问类型来访问带有变量句柄的变量:

  • 读取模式:这是用来获取变量的读取访问模式。你可以使用以下方法:

    • get(): 以声明为非volatile的方式读取变量的值

    • getVolatile(): 以声明为volatile的方式读取变量的值

    • getAcquire(): 读取变量的值并保证在优化目的的指令之前,不会对修改或访问此变量的指令进行重排

    • getOpaque(): 读取变量的值并保证当前线程的指令不会被重排;不对其他线程提供保证

  • 写入模式:这是用来获取变量的写入访问模式。你可以使用set()setVolatile()setRelease()setOpaque()方法。它们与之前的方法等效,但具有写入访问。

  • 原子访问模式:这是用来获取与原子变量提供的功能类似的功能,例如比较和获取变量的值。你可以使用以下方法:

    • compareAndSet(): 如果传递给参数的预期值等于变量的当前值,则将变量的值更改为易失性变量声明的值

    • weakCompareAndSet()weakCompareAndSetVolatile(): 如果传递给参数的预期值等于变量的当前值,则可能以原子方式更改变量的值,分别作为非易失性或易失性变量声明

  • 数值更新访问模式:这是以原子方式修改数值。

参见

  • 本章中的使用原子变量使用原子数组配方

第八章:自定义并发类

在本章中,我们将涵盖以下主题:

  • 自定义 ThreadPoolExecutor 类

  • 实现基于优先级的 Executor 类

  • 实现 ThreadFactory 接口以生成自定义线程

  • 在 Executor 对象中使用我们的 ThreadFactory

  • 自定义在计划线程池中运行的任务

  • 实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程

  • 自定义在 fork/join 框架中运行的任务

  • 实现自定义锁类

  • 实现基于优先级的传输队列

  • 实现您自己的原子对象

  • 实现您自己的流生成器

  • 实现您自己的异步流

简介

Java 并发 API 提供了许多接口和类来实现并发应用程序。它们提供了低级机制,例如Thread类、RunnableCallable接口,或者synchronized关键字。它们还提供了高级机制,例如Executor框架和 Java 7 版本中添加的 fork/join 框架,或者 Java 8 中添加的Stream框架,以处理大量数据集。尽管如此,您可能会发现自己正在开发一个程序,其中 Java API 的默认配置和/或实现不符合您的需求。

在这种情况下,您可能需要根据 Java 提供的实现实现您自己的自定义并发工具。基本上,您可以:

  • 实现一个接口以提供该接口定义的功能,例如ThreadFactory接口。

  • 覆盖一个类的一些方法以适应您的需求。例如,覆盖Phaser类的onAdvance()方法,默认情况下该方法不执行任何有用的操作,并应被覆盖以提供一些功能。

通过本章的食谱,您将学习如何在不从头设计并发框架的情况下更改一些 Java 并发 API 类的行为。您可以将这些食谱作为实现您自己的自定义的起点。

自定义 ThreadPoolExecutor 类

Executor框架是一种机制,允许您将线程创建与其执行分离。它基于ExecutorExecutorService接口,ThreadPoolExecutor类实现了这两个接口。它有一个内部线程池,并提供方法允许您发送两种类型的任务并在池线程中执行它们。这些任务是:

  • 实现Runnable接口的任务不返回结果

  • 实现返回结果的Callable接口的任务

在这两种情况下,您只需将任务发送给执行器。执行器使用其池中的线程之一或创建一个新的线程来执行这些任务。它还决定任务执行的时刻。

在这个菜谱中,你将学习如何重写 ThreadPoolExecutor 类的一些方法来计算你将在执行器中执行的任务的执行时间,并在执行器完成执行时在控制台统计信息中写入关于执行器的内容:

准备就绪

本菜谱的示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目:

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 MyExecutor 的类,该类扩展了 ThreadPoolExecutor 类:
        public class MyExecutor extends ThreadPoolExecutor {

  1. 声明一个名为 startTimes 的私有 ConcurrentHashMap 属性,该属性由 StringDate 类参数化:
        private final ConcurrentHashMap<Runnable, Date> startTimes;

  1. 实现类的构造函数。使用 super 关键字调用父类的构造函数并初始化 startTime 属性:
        public MyExecutor(int corePoolSize, int maximumPoolSize,
                          long keepAliveTime, TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue);
          startTimes=new ConcurrentHashMap<>();
        }

  1. 重写 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(); 
        }

  1. 重写 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(); 
        }

  1. 重写 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(r, new Date()); 
        }

  1. 重写 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(r); 
              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(); 
            } 
          } 
        }

  1. 创建一个名为 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(); 
          } 

        }

  1. 通过创建一个名为 Main 的类并实现一个 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个名为 myExecutorMyExecutor 对象:
        MyExecutor myExecutor=new MyExecutor(4, 8, 1000,
                                  TimeUnit.MILLISECONDS,
                                  new LinkedBlockingDeque<Runnable>());

  1. 创建一个由 String 类参数化的 Future 对象列表,用于存储您将要发送给执行器的任务的输出结果:
        List<Future<String>> results=new ArrayList<>();

  1. 提交 10 个 Task 对象:
        for (int i=0; i<10; i++) { 
          SleepTwoSecondsTask task=new SleepTwoSecondsTask(); 
          Future<String> result=myExecutor.submit(task); 
          results.add(result); 
        }

  1. 使用 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(); 
          } 
        }

  1. 使用 shutdown() 方法完成执行器的执行:
        myExecutor.shutdown();

  1. 使用 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(); 
          } 
        }

  1. 使用 awaitTermination() 方法等待执行器的完成:
        try { 
          myExecutor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 输出一条消息,表明程序执行的结束:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

在这个示例中,我们通过扩展ThreadPoolExecutor类并重写其四个方法来实现自定义执行器。beforeExecute()afterExecute()方法用于计算任务的执行时间。beforeExecute()方法在任务执行之前执行;在这种情况下,我们使用HashMap来存储任务的开始日期。afterExecute()方法在任务执行之后执行。你可以从HashMap中获取已完成的任务的startTime,然后计算实际日期和startTime之间的差异以获取任务的执行时间。你还重写了shutdown()shutdownNow()方法,将执行器中执行的任务的统计信息写入控制台。这些任务包括:

  • 使用getCompletedTaskCount()方法获取已执行的任务

  • 使用getActiveCount()方法获取当前正在运行的任务

  • 使用执行器存储待处理任务的阻塞队列的size()方法获取待处理任务

实现Callable接口的SleepTwoSecondsTask类将其执行线程休眠 2 秒,而Main类,你向其中发送 10 个任务到你的执行器,使用它和其他类来演示其功能。

执行程序,你将看到程序如何显示每个正在运行的任务的时间跨度以及调用shutdown()方法时的执行器统计信息。

相关内容

  • 在第四章的创建线程执行器并控制其拒绝的任务示例中,线程执行器

  • 本章中的在 Executor 对象中使用我们的 ThreadFactory示例

实现基于优先级的执行器类

在 Java 并发 API 的第一个版本中,你必须创建和运行你应用程序的所有线程。在 Java 5 版本中,随着 Executor 框架的出现,引入了一种新的并发任务执行机制。

使用 Executor 框架,你只需要实现你的任务并将它们发送到执行器。执行器负责创建和执行执行你的任务的线程。

在内部,执行器使用阻塞队列来存储待处理任务。这些任务按照到达执行器的顺序存储。一个可能的替代方案是使用优先队列来存储新任务。这样,如果一个具有高优先级的任务到达执行器,它将在所有其他已经等待但优先级相对较低的其他线程之前执行。

在这个示例中,你将学习如何调整一个执行器,该执行器将使用优先队列来存储你发送给执行器执行的任务。

准备工作

本示例的代码是在 Eclipse IDE 中实现的。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按以下步骤实现示例:

  1. 创建一个名为MyPriorityTask的类,该类实现了RunnableComparable接口,并使用MyPriorityTask类接口进行参数化:
        public class MyPriorityTask implements Runnable,
                              Comparable<MyPriorityTask> {

  1. 声明一个名为priority的私有int属性:
        private int priority;

  1. 声明一个名为name的私有String属性:
        private String name;

  1. 实现类的构造函数以初始化其属性:
        public MyPriorityTask(String name, int priority) { 
          this.name=name; 
          this.priority=priority; 
        }

  1. 实现一个方法来返回优先级属性值:
        public int getPriority(){ 
          return priority; 
        }

  1. 实现声明在Comparable接口中的compareTo()方法。它接收一个MyPriorityTask对象作为参数,并比较两个对象的优先级:当前对象和参数对象。你让优先级更高的任务先于优先级低的任务执行:
        @Override 
        public int compareTo(MyPriorityTask o) { 
          return Integer.compare(o.getPriority(), this.getPriority()); 
        }

  1. 实现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(); 
            Thread.currentThread().interrupt(); 
          } 
        }

  1. 通过创建一个名为Main的类并实现一个main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个名为executorThreadPoolExecutor对象。使用参数化由Runnable接口的PriorityBlockingQueue作为此执行器将用于存储其挂起任务的队列:
        ThreadPoolExecutor executor=new ThreadPoolExecutor(4,4,1,
                                    TimeUnit.SECONDS,
                                    new PriorityBlockingQueue<Runnable>());

  1. 使用循环计数器作为任务优先级的依据,向执行器发送 10 个任务。使用execute()方法将任务发送到执行器:
        for (int i=0; i<10; i++){ 
          MyPriorityTask task=new MyPriorityTask ("Task "+i,i); 
          executor.execute(task); 
        }

  1. 将当前线程休眠 1 秒:
        try { 
          TimeUnit.SECONDS.sleep(1); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用循环计数器作为任务优先级的依据,向执行器发送 10 个额外的任务。使用execute()方法将任务发送到执行器:
        for (int i=10; i<20; i++) { 
          MyPriorityTask task=new MyPriorityTask ("Task "+i,i); 
          executor.execute(task);       
        }

  1. 使用shutdown()方法关闭执行器:
        executor.shutdown();

  1. 使用awaitTermination()方法等待执行器的最终化:
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 在控制台写入一条消息,指示程序的最终化:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

将普通执行器转换为基于优先级的执行器很简单。你只需要传递一个参数化由Runnable接口的PriorityBlockingQueue对象作为参数。但与执行器一起,你应该知道存储在优先队列中的所有对象都必须实现Comparable接口。

你实现了实现了Runnable接口的MyPriorityTask类,该类将作为任务执行,并实现了Comparable接口以便存储在优先队列中。这个类有一个Priority属性,用于存储任务的优先级。如果一个任务的这个属性值更高,它将更早被执行。compareTo()方法决定了任务在优先队列中的顺序。在Main类中,你向执行器发送了 20 个不同优先级的任务。你首先发送给执行器的任务将是首先被执行的任务。由于执行器空闲等待任务,所以一旦任务到达,它就会立即执行。你创建了具有四个执行线程的执行器,所以前四个任务将是首先被执行的。然后,其余的任务将根据它们的优先级执行。

以下截图显示了此示例的一次执行:

图片

还有更多...

您可以配置Executor以使用任何实现BlockingQueue接口的实现。一个有趣的实现是DelayQueue。此类用于存储具有延迟激活的元素。它提供仅返回活动对象的方法。您可以使用此类来实现自己的ScheduledThreadPoolExecutor类版本。

参见

  • 在第四章,“线程执行器”中的创建线程执行器并控制其拒绝的任务食谱

  • 本章的自定义 ThreadPoolExecutor 类食谱

  • 在第七章,“并发集合”中的使用按优先级排序的阻塞线程安全队列食谱

实现 ThreadFactory 接口以生成自定义线程

工厂模式是面向对象编程世界中广泛使用的设计模式。它是一种创建型模式,其目标是开发一个具有创建一个或多个类对象使命的类。然后,当我们想要创建这些类中的一个对象时,我们使用工厂而不是使用 new 运算符。

使用这个工厂,我们将对象的创建集中化,从而获得易于更改创建对象类别或创建这些对象的方式的优势,考虑到我们在使用有限资源创建对象时的限制。例如,我们只能拥有具有易于生成对象创建统计数据的能力的N个此类对象。

Java 提供了ThreadFactory接口来实现Thread对象工厂。Java 并发 API 的一些高级实用工具,如Executor框架或 fork/join 框架,使用线程工厂来创建线程。Java 并发 API 中工厂模式的另一个例子是Executors类。它提供了许多创建不同类型的Executor对象的方法。在本食谱中,您将通过添加新功能来扩展Thread类,并将实现一个线程工厂类来生成此类线程。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为MyThread的类,该类扩展了Thread类:
        public class MyThread extends Thread {

  1. 声明三个名为creationDatestartDatefinishDate的私有Date属性:
        private final Date creationDate; 
        private Date startDate; 
        private Date finishDate;

  1. 实现类的构造函数。它接收要执行的名称和Runnable对象作为参数。初始化线程的创建日期:
        public MyThread(Runnable target, String name ){ 
          super(target,name); 
          creationDate = new Date(); 
        }

  1. 实现 run()方法。存储线程的起始日期,调用父类的 run()方法,并存储执行完成日期:
        @Override 
        public void run() { 
          setStartDate(); 
          super.run(); 
          setFinishDate(); 
        }

  1. 实现一个方法来设置startDate属性值:
        public synchronized void setStartDate() { 
          startDate=new Date(); 
        }

  1. 实现一个方法来设置finishDate属性值:
        public synchronized void setFinishDate() { 
          finishDate=new Date(); 
        }

  1. 实现一个名为 getExecutionTime() 的方法,该方法计算线程的执行时间,即开始和结束日期之间的差异:
        public synchronized long getExecutionTime() { 
          return finishDate.getTime()-startDate.getTime(); 
        }

  1. 重写 toString() 方法以返回线程的创建日期和执行时间:
        @Override 
        public synchronized 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(); 
        }

  1. 创建一个名为 MyThreadFactory 的类,该类实现了 ThreadFactory 接口:
        public class MyThreadFactory implements ThreadFactory {

  1. 声明一个名为 counter 的私有 AtomicInteger 属性:
        private AtomicInteger counter;

  1. 声明一个名为 prefix 的私有 String 属性:
        private String prefix;

  1. 实现类的构造函数以初始化其属性:
        public MyThreadFactory (String prefix) { 
          this.prefix=prefix; 
          counter=new AtomicInteger(1); 
        }

  1. 实现 newThread() 方法。创建一个 MyThread 对象并增加 counter 属性:
        @Override 
        public Thread newThread(Runnable r) { 
          MyThread myThread=new MyThread(r,prefix+"-"+counter
                                                  .getAndIncrement()); 
          return myThread; 
        }

  1. 创建一个名为 MyTask 的类,该类实现了 Runnable 接口。实现 run() 方法。将当前线程休眠 2 秒:
        public class MyTask implements Runnable { 
          @Override 
          public void run() { 
            try { 
              TimeUnit.SECONDS.sleep(2); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 通过创建一个名为 Main 的类并实现 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 创建一个 MyThreadFactory 对象:
        MyThreadFactory myFactory=new MyThreadFactory
                                               ("MyThreadFactory");

  1. 创建一个 Task 对象:
        MyTask task=new MyTask();

  1. 使用工厂的 newThread() 方法创建一个 MyThread 对象来执行任务:
        Thread thread=myFactory.newThread(task);

  1. 启动线程并等待其最终化:
        thread.start(); 
        thread.join();

  1. 使用 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 对象。

为了检查这两个类,你实现了 MyTask 类,该类实现了 Runnable 对象。这是由 MyThread 对象管理的线程要执行的任务。一个 MyTask 实例将其执行线程休眠 2 秒。

在示例的主方法中,你使用 MyThreadFactory 工厂创建了一个 MyThread 对象来执行 Task 对象。如果你执行程序,你将看到一条消息,其中包含执行线程的开始日期和执行时间。以下截图显示了此示例生成的输出:

还有更多...

Java 并发 API 提供了Executors类来生成线程执行器,通常是ThreadPoolExecutor类的对象。您也可以使用此类通过defaultThreadFactory()方法获取ThreadFactory接口的最基本实现。此方法生成的工厂将生成属于同一ThreadGroup对象的基本Thread对象。您可以在程序中出于任何目的使用ThreadFactory接口,而不仅仅是与 Executor 框架相关。

在 Executor 对象中使用我们的 ThreadFactory

在前面的食谱中,我们介绍了工厂模式,并提供了如何实现实现ThreadFactory接口的线程工厂的示例。

Executor 框架是一种机制,允许您分离线程创建和执行。它基于ExecutorExecutorService接口以及实现这两个接口的ThreadPoolExecutor类。它有一个内部线程池,并提供允许您将两种类型的任务发送到池中执行的方法。这两种类型任务如下:

  • 实现Runnable接口的类,用于实现不返回结果的任务

  • 实现Callable接口的类,用于实现返回结果的任务

内部,Executor框架使用ThreadFactory接口来创建它使用的线程,以生成新的线程。在本食谱中,您将学习如何实现自己的线程类,一个线程工厂来创建此类线程,以及如何在 Executor 中使用此工厂,以便 Executor 执行您的线程。

准备工作

阅读前面的食谱并实现其示例。

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或另一个 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. MyThreadMyThreadFactoryMyTask类复制到项目中。它们是在实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程食谱中实现的。您将在本示例中使用它们。

  2. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:

        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 创建一个名为threadFactoryMyThreadFactory对象:
        MyThreadFactory threadFactory=new MyThreadFactory
                                                  ("MyThreadFactory");

  1. 使用Executors类的newCachedThreadPool()方法创建一个新的Executor对象。将之前创建的工厂对象作为参数传递。新的Executor对象将使用此工厂创建必要的线程,因此它将执行MyThread线程:
        ExecutorService executor=Executors.newCachedThreadPool
                                                     (threadFactory);

  1. 创建一个新的Task对象,并使用submit()方法将其发送到执行器:
        MyTask task=new MyTask(); 
        executor.submit(task);

  1. 使用shutdown()方法关闭执行器:
        executor.shutdown();

  1. 使用awaitTermination()方法等待执行器的最终化:
        executor.awaitTermination(1, TimeUnit.DAYS);

  1. 写一条消息以指示程序结束:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

在前一个配方中的 How it works... 部分中,你有一个关于 MyThreadMyThreadFactoryMyTask 类如何工作的详细解释。

在示例的 main() 方法中,你使用 Executors 类的 newCachedThreadPool() 方法创建了一个 Executor 对象。你将之前创建的工厂对象作为参数传递,因此创建的 Executor 对象将使用该工厂创建所需的线程并执行 MyThread 类的线程。

执行程序,你将看到一条包含线程开始日期和执行时间的消息。以下截图显示了此示例生成的输出:

参见

  • 本章中 实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程 的配方

自定义在计划线程池中运行的任务

计划线程池是 Executor 框架的基本线程池的扩展,允许你安排在一段时间后执行的任务的执行。它由 ScheduledThreadPoolExecutor 类实现,并允许执行以下两种类型的任务:

  • 延迟任务:这类任务在一段时间后只执行一次

  • 周期性任务:这类任务在延迟后执行,然后定期执行

延迟任务可以执行 CallableRunnable 对象,但周期性任务只能执行 Runnable 对象。由计划池执行的所有任务都是 RunnableScheduledFuture 接口的一个实现。在本配方中,你将学习如何实现自己的 RunnableScheduledFuture 接口实现以执行延迟和周期性任务。

准备工作

本配方的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到...

按照以下步骤实现示例:

  1. 创建一个名为 MyScheduledTask 的类,该类由一个名为 V 的泛型类型参数化。它扩展了 FutureTask 类并实现了 RunnableScheduledFuture 接口:
        public class MyScheduledTask<V> extends FutureTask<V>
                                implements RunnableScheduledFuture<V> {

  1. 声明一个名为 task 的私有 RunnableScheduledFuture 属性:
        private RunnableScheduledFuture<V> task;

  1. 声明一个名为 executor 的私有 ScheduledThreadPoolExecutor 类:
        private ScheduledThreadPoolExecutor executor;

  1. 声明一个名为 period 的私有 long 属性:
        private long period;

  1. 声明一个名为 startDate 的私有 long 属性:
        private long startDate;

  1. 实现类的构造函数。它接收将要由任务执行的 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; 
        }

  1. 实现一个getDelay()方法。如果任务是周期性的并且startDate属性具有非零值,则计算返回值作为startDate属性和实际日期之间的差异。否则,返回存储在任务属性中的原始任务的延迟。别忘了你必须以传递的时间单位返回结果:
        @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); 
            } 
          } 
        }

  1. 实现一个compareTo()方法。调用原始任务的compareTo()方法:
        @Override 
        public int compareTo(Delayed o) { 
          return task.compareTo(o); 
        }

  1. 实现一个isPeriodic()方法。调用原始任务的isPeriodic()方法:
        @Override 
        public boolean isPeriodic() { 
          return task.isPeriodic(); 
        }

  1. 实现一个run()方法。如果是周期性任务,你必须使用下一个执行任务的开始日期更新其startDate属性。计算它为实际日期和周期的总和。然后,再次将任务添加到ScheduledThreadPoolExecutor对象的队列中:
        @Override 
        public void run() { 
          if (isPeriodic() && (!executor.isShutdown())) { 
            Date now=new Date(); 
            startDate=now.getTime()+period; 
            executor.getQueue().add(this); 
          }

  1. 使用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()); 
        }

  1. 实现一个setPeriod()方法来设置此任务的周期:
        public void setPeriod(long period) { 
          this.period=period; 
        }

  1. 创建一个名为MyScheduledThreadPoolExecutor的类来实现一个执行MyScheduledTask任务的ScheduledThreadPoolExecutor对象。指定这个类扩展ScheduledThreadPoolExecutor类:
        public class MyScheduledThreadPoolExecutor extends
                                          ScheduledThreadPoolExecutor {

  1. 实现一个仅调用其父类构造函数的类构造函数:
        public MyScheduledThreadPoolExecutor(int corePoolSize) { 
          super(corePoolSize); 
        }

  1. 实现一个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; 
        }

  1. 覆盖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; 
        }

  1. 创建一个实现Runnable接口的名为Task的类:
        public class Task implements Runnable {

  1. 实现一个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"); 
        }

  1. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) throws Exception{

  1. 创建一个名为executorMyScheduledThreadPoolExecutor对象。使用4作为参数以在池中有两个线程:
        MyScheduledThreadPoolExecutor executor=new
                                 MyScheduledThreadPoolExecutor(4);

  1. 创建一个名为taskTask对象。在控制台写入实际日期:
        Task task=new Task(); 
        System.out.printf("Main: %s\n",new Date());

  1. 使用schedule()方法将延迟任务发送到执行器。任务将在延迟 1 秒后执行:
        executor.schedule(task, 1, TimeUnit.SECONDS);

  1. 将主线程休眠3秒:
        TimeUnit.SECONDS.sleep(3);

  1. 创建另一个Task对象。再次在控制台打印实际日期:
        task=new Task(); 
        System.out.printf("Main: %s\n",new Date());

  1. 使用scheduleAtFixedRate()方法将周期性任务发送到执行器。任务将在延迟 1 秒后执行,然后每 3 秒执行一次:
        executor.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);

  1. 将主线程休眠 10 秒:
        TimeUnit.SECONDS.sleep(10);

  1. 使用 shutdown() 方法关闭执行器。使用 awaitTermination() 方法等待执行器的最终化:
        executor.shutdown(); 
        executor.awaitTermination(1, TimeUnit.DAYS);

  1. 在控制台写入一条消息,指示程序的结束:
        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 值。第三个参数是新的对象将要替换的池中的任务,最后一个是将要执行任务的执行器。在这种情况下,你使用 this 关键字来引用创建任务的执行器。

MyScheduledTask 类可以执行延迟和周期性任务。你实现了两个方法,包含了执行这两种任务所需的所有逻辑。它们是 getDelay()run() 方法。

getDelay() 方法由计划执行器调用,以确定是否需要执行一个任务。这个方法在延迟和周期性任务中的行为会发生变化。如前所述,MyScheduledClass 类的构造函数接收将要执行 Runnable 对象的原始 ScheduledRunnableFuture 对象,并将其存储为类的属性,以便访问其方法和数据。当我们执行一个延迟任务时,getDelay() 方法返回原始任务的延迟;然而,在周期性任务的情况下,getDelay() 方法返回 startDate 属性和实际日期之间的差异。

run() 方法是执行任务的方法。周期性任务的一个特殊性是,如果您想再次执行任务,必须将任务的下一次执行放入执行器的队列中作为一个新任务。因此,如果您正在执行周期性任务,您需要设置 startDate 属性的值,并将其添加到任务的实际执行日期和周期中,然后将任务再次存储在执行器的队列中。startDate 属性存储了任务下一次执行开始的时间。然后,您使用 FutureTask 类提供的 runAndReset() 方法执行任务。在延迟任务的情况下,您不需要将它们放入执行器的队列中,因为它们只能执行一次。

您还必须考虑执行器是否已关闭。如果是这样,您不需要再次将周期性任务存储在执行器的队列中。

最后,您在 MyScheduledThreadPoolExecutor 类中重写了 scheduleAtFixedRate() 方法。我们之前提到,对于周期性任务,您需要使用任务的周期来设置 startDate 属性的值,但您还没有初始化这个周期。您必须重写这个接收该周期作为参数的方法;这样做是为了将其传递给 MyScheduledTask 类,以便它可以使用它。

示例完整地包含了实现 Runnable 接口的 Task 类,它是计划执行器中执行的任务。示例的主类创建了一个 MyScheduledThreadPoolExecutor 执行器,并将以下两个任务发送给它:

  • 一个延迟任务,将在实际日期后 1 秒执行

  • 一个周期性任务,将在实际日期后的第一次执行 1 秒,然后每 3 秒执行一次

以下截图显示了此示例的部分执行情况。您可以检查两种任务是否正确执行:

还有更多...

ScheduledThreadPoolExecutor 类提供了一个接收 Callable 对象作为参数的 decorateTask() 方法的另一个版本,而不是 Runnable 对象。

参见

  • 在 第四章 的 线程执行器 部分中,在执行器中延迟执行任务在执行器中周期性执行任务 的食谱

实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程

Java 9 最有趣的功能之一是 fork/join 框架。它是对 ExecutorExecutorService 接口的实现,允许您执行 CallableRunnable 任务,而无需管理执行它们的线程。

此执行器面向执行可以分解为更小部分的任务。其主要组件如下:

  • 这是一种特殊类型的任务,由 ForkJoinTask 类实现。

  • 它提供了两个操作来将任务分割成子任务(fork 操作)以及等待这些子任务的最终化(join 操作)。

  • 这是一个算法,称为工作窃取算法,它优化了线程池中线程的使用。当任务等待其子任务时,执行它的线程被用来执行另一个线程。

Fork/Join 框架的主类是 ForkJoinPool 类。内部,它有两个以下元素:

  • 一队列等待执行的任务

  • 一组执行任务的线程池

ForkJoinWorkerThread 类为 Thread 类添加了新方法,例如当线程创建时执行的 onStart() 方法以及用于清理线程使用的资源的 onTermination() 方法。ForkJoinPool 类使用 ForkJoinWorkerThreadFactory 接口的一个实现来创建它所使用的工作线程。

在这个菜谱中,你将学习如何实现一个用于 ForkJoinPool 类的自定义工作线程,以及如何使用它,通过扩展 ForkJoinPool 类并实现 ForkJoinWorkerThreadFactory 接口来创建工厂。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 MyWorkerThread 的类,该类扩展了 ForkJoinWorkerThread 类:
        public class MyWorkerThread extends ForkJoinWorkerThread {

  1. 声明并创建一个由 Integer 类参数化的私有 ThreadLocal 属性 taskCounter
        private final static ThreadLocal<Integer> taskCounter=
                                         new ThreadLocal<Integer>();

  1. 实现类的构造函数:
        protected MyWorkerThread(ForkJoinPool pool) { 
          super(pool); 
        }

  1. 重写 onStart() 方法。在其父类中调用该方法,向控制台打印一条消息,并将此线程的 taskCounter 属性值设置为零:
        @Override 
        protected void onStart() { 
          super.onStart(); 
          System.out.printf("MyWorkerThread %d: Initializing task
                             counter.\n", getId()); 
          taskCounter.set(0); 
        }

  1. 重写 onTermination() 方法。将此线程的 taskCounter 属性值写入控制台:
        @Override 
        protected void onTermination(Throwable exception) { 
          System.out.printf("MyWorkerThread %d: %d\n",
                            getId(),taskCounter.get()); 
          super.onTermination(exception); 
        }

  1. 实现 addTask() 方法。增加 taskCounter 属性的值:
        public void addTask(){ 
          taskCounter.set(taskCounter.get() + 1);; 
        }

  1. 创建一个名为 MyWorkerThreadFactory 的类,该类实现了 ForkJoinWorkerThreadFactory 接口。实现 newThread() 方法。创建并返回一个 MyWorkerThread 对象:
        public class MyWorkerThreadFactory implements
                       ForkJoinWorkerThreadFactory { 
          @Override 
          public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 
            return new MyWorkerThread(pool); 
          } 

        }

  1. 创建一个名为 MyRecursiveTask 的类,该类扩展了由 Integer 类参数化的 RecursiveTask 类:
        public class MyRecursiveTask extends RecursiveTask<Integer> {

  1. 声明一个名为 array 的私有 int 数组:
        private int array[];

  1. 声明两个名为 startend 的私有 int 属性:
        private int start, end;

  1. 实现类的构造函数,初始化其属性:
        public Task(int array[],int start, int end) { 
          this.array=array; 
          this.start=start; 
          this.end=end; 
        }

  1. 实现一个 compute() 方法来计算数组中起始位置和结束位置之间的所有元素的总和。首先,将执行任务的线程转换为 MyWorkerThread 对象,并使用 addTask() 方法增加该线程的任务计数器:
        @Override 
        protected Integer compute() { 
          Integer ret; 
          MyWorkerThread thread=(MyWorkerThread)Thread.currentThread(); 
          thread.addTask();

  1. 如果数组中起始位置和结束位置之间的差异大于 100 个元素,我们计算中间位置并创建两个新的MyRecursiveTask任务来分别处理第一部分和第二部分。如果差异等于或小于 100,我们计算起始位置和结束位置之间所有元素的总和:
        if (end-start>100) { 
          int mid=(start+end)/2; 
          MyRecursiveTask task1=new MyRecursiveTask(array,start,mid); 
          MyRecursiveTask task2=new MyRecursiveTask(array,mid,end); 
          invokeAll(task1,task2); 
          ret=addResults(task1,task2); 
        } else { 
          int add=0; 
          for (int i=start; i<end; i++) { 
            add+=array[i]; 
          } 
          ret=add; 
        }

  1. 让线程休眠 10 毫秒,并返回任务的执行结果:
          try { 
            TimeUnit.MILLISECONDS.sleep(10); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
          return ret; 
        }

  1. 实现一个名为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; 
          }

  1. 通过创建一个名为Main的类并实现一个main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 创建一个名为factoryMyWorkerThreadFactory对象:
        MyWorkerThreadFactory factory=new MyWorkerThreadFactory();

  1. 创建一个名为poolForkJoinPool对象。将之前创建的factory对象传递给构造函数:
        ForkJoinPool pool=new ForkJoinPool(4, factory, null, false);

  1. 创建一个包含 100,000 个整数的数组。初始化所有元素为 1:
        int array[]=new int[100000]; 
        for (int i=0; i<array.length; i++){ 
          array[i]=1; 
        }

  1. 创建一个新的task对象来计算数组中所有元素的总和:
        MyRecursiveTask task=new MyRecursiveTask(array,0,array.length);

  1. 使用execute()方法将任务发送到线程池:
        pool.execute(task);

  1. 使用join()方法等待任务的结束:
        task.join();

  1. 使用shutdown()方法关闭线程池:
        pool.shutdown();

  1. 使用awaitTermination()方法等待执行器的最终化:
        pool.awaitTermination(1, TimeUnit.DAYS);

  1. 使用get()方法将任务的执行结果写入控制台:
        System.out.printf("Main: Result: %d\n",task.get());

  1. 在控制台写入一条消息,表明示例结束:
        System.out.printf("Main: End of the program\n");

它是如何工作的...

Fork/Join 框架使用的线程被称为工作线程。Java 包括一个名为ForkJoinWorkerThread的类,它扩展了Thread类并实现了 Fork/Join 框架使用的工作线程。

在这个示例中,你实现了MyWorkerThread类,该类扩展了ForkJoinWorkerThread类并重写了ForkJoinWorkerThread类的两个方法。你的目标是实现每个工作线程中的任务计数器,以便你可以知道一个工作线程执行了多少个任务。你使用ThreadLocal属性实现了计数器。这样,每个线程都会以对你,即程序员来说透明的方式拥有自己的计数器。

你重写了ForkJoinWorkerThread类的onStart()方法来初始化任务计数器。当工作线程开始执行时,会调用此方法。你还重写了onTermination()方法,将任务计数器的值打印到控制台。当工作线程完成执行时,会调用此方法。此外,你在MyWorkerThread类中实现了一个方法。addTask()方法增加每个线程的任务计数器。

ForkJoinPool 类,与 Java 并发 API 中的所有执行器一样,使用工厂创建其线程。因此,如果你想在 ForkJoinPool 类中使用 MyWorkerThread 线程,你必须实现你的线程工厂。对于 fork/join 框架,这个工厂必须实现 ForkJoinPool.ForkJoinWorkerThreadFactory 类。你为此实现了 MyWorkerThreadFactory 类。这个类只有一个方法,用于创建一个新的 MyWorkerThread 对象。

最后,你只需要使用你创建的工厂初始化一个 ForkJoinPool 类。你在 Main 类中这样做,使用 ForkJoinPool 类的构造函数。

以下截图显示了程序的部分输出:

图片

你可以看到 ForkJoinPool 对象如何执行了四个工作线程,以及每个线程执行了多少个任务。

更多...

请注意,当线程正常完成或抛出异常时,ForkJoinWorkerThread 类提供的 onTermination() 方法会被调用。该方法接收一个 Throwable 对象作为参数。如果参数为 null 值,工作线程正常完成;然而,如果参数有值,线程会抛出异常。你必须包含必要的代码来处理这种情况。

参见

  • 在 第五章 的 创建 fork/join 池 菜谱中,Fork/Join 框架

  • 在 第一章 的 通过工厂创建线程 菜谱中,线程管理

自定义在 fork/join 框架中运行的任务

Executor 框架将任务创建和执行分离。使用它,你只需要实现 Runnable 对象并使用一个 Executor 对象。你只需要将 Runnable 任务发送给执行器,它会创建、管理和最终化执行这些任务所需的线程。

Java 9 在 fork/join 框架中提供了一种特殊的执行器类型(在 Java 7 中引入)。这个框架旨在解决可以使用分治技术分解成更小任务的问题。在任务内部,你必须检查你想要解决的问题的大小;如果它大于设定的阈值,你将问题分解成两个或更多任务,并使用框架执行它们。如果问题的大小小于设定的阈值,你直接在任务中解决问题;可选地,它返回一个结果。fork/join 框架实现了工作窃取算法,这提高了这类问题的整体性能。

Fork/Join 框架的 main 类是 ForkJoinPool 类。内部,它包含以下两个元素:

  • 等待执行的任务队列

  • 执行任务的线程池

默认情况下,由ForkJoinPool类执行的任务是ForkJoinTask类的对象。您也可以将RunnableCallable对象发送到ForkJoinPool类,但它们无法充分利用分叉/合并框架的所有优点。通常,您会将ForkJoinTask类的两个子类之一发送到ForkJoinPool对象:

  • RecursiveAction:如果您的任务不返回结果

  • RecursiveTask:如果您的任务返回结果

在这个菜谱中,您将学习如何通过实现一个扩展ForkJoinTask类的任务来为分叉/合并框架实现自己的任务。您将要实现的任务将测量并写入其执行时间,以便您可以控制其演变。您还可以实现自己的分叉/合并任务来写入日志信息,获取任务中使用的资源,或后处理任务的输出结果。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为MyWorkerTask的类,并指定它扩展由Void类型参数化的ForkJoinTask类:
        public abstract class MyWorkerTask extends ForkJoinTask<Void> {

  1. 声明一个名为name的私有String属性来存储任务的名称:
        private String name;

  1. 实现类的构造函数以初始化其属性:
        public MyWorkerTask(String name) { 
          this.name=name; 
        }

  1. 实现返回nullgetRawResult()方法。这是ForkJoinTask类的抽象方法之一。由于MyWorkerTask任务不会返回任何结果,因此此方法必须返回null
        @Override 
        public Void getRawResult() { 
          return null; 
        }

  1. 实现setRawResult()方法。这是ForkJoinTask类的另一个抽象方法。由于MyWorkerTask任务不会返回任何结果,因此请留空此方法的主体:
        @Override 
        protected void setRawResult(Void value) { 

        }

  1. 实现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; 
        }

  1. 实现返回任务名称的getName()方法:
        public String getName(){ 
          return name; 
        }

  1. 声明抽象方法compute()。如前所述,此方法将实现任务的逻辑,并且必须由MyWorkerTask类的子类实现:
        protected abstract void compute();

  1. 创建一个名为Task的类,该类扩展了MyWorkerTask类:
        public class Task extends MyWorkerTask {

  1. 声明一个名为array的私有int值数组:
        private int array[];

  1. 实现类的构造函数以初始化其属性:
        public Task(String name, int array[], int start, int end){ 
          super(name); 
          this.array=array; 
          this.start=start; 
          this.end=end; 
        }

  1. 实现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);

  1. 如果元素块少于 100 个元素,使用for循环增加所有元素:
        } else { 
        for (int i=start; i<end; i++) { 
          array[i]++; 
        }

  1. 最后,让执行任务的线程休眠 50 毫秒:
            try { 
              Thread.sleep(50); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 接下来,通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 创建一个包含 10,000 个元素的int数组:
        int array[]=new int[10000];

  1. 创建一个名为poolForkJoinPool对象:
        ForkJoinPool pool=new ForkJoinPool();

  1. 创建一个 Task 对象来增加数组中所有元素。构造函数的参数将 Task 作为任务名称,数组对象,以及值 0 和 10000 给出,表示此任务必须处理整个数组:
        Task task=new Task("Task",array,0,array.length);

  1. 使用 execute() 方法将任务发送到池中:
        pool.invoke(task);

  1. 使用 shutdown() 方法关闭 pool
        pool.shutdown();

  1. 在控制台中写入一条消息,表示程序结束:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

在这个菜谱中,你实现了扩展 ForkJoinTask 类的 MyWorkerTask 类。这是你自己的基类,用于实现可以在 ForkJoinPool 执行器中执行的任务,并且可以利用执行器的所有优点,因为它是一个工作窃取算法。此类相当于 RecursiveActionRecursiveTask 类。

当你扩展 ForkJoinTask 类时,你必须实现以下三个方法:

  • setRawResult(): 此方法用于设置任务的结果。由于你的任务不返回任何结果,请留空此方法。

  • getRawResult(): 此方法用于返回任务的结果。由于你的任务不返回任何结果,此方法返回 null。

  • exec(): 此方法实现了任务的逻辑。在这种情况下,你将逻辑委托给了抽象的 compute() 方法(作为 RecursiveActionRecursiveTask 类)。然而,在 exec() 方法中,你测量了方法的执行时间,并将其写入控制台。

最后,在示例的主类中,你创建了一个包含 10,000 个元素的数组,一个 ForkJoinPool 执行器,以及一个 Task 对象来处理整个数组。执行程序,你会看到执行的不同任务如何在控制台中写入它们的执行时间。

参见

  • 第五章中的 创建 fork/join 池 菜谱

  • 本章的 实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程 菜谱

实现自定义锁类

锁是 Java 并发 API 提供的基本同步机制之一。它们允许程序员保护代码的关键部分,以便一次只有一个线程可以执行该代码块。它提供了以下两个操作:

  • lock(): 当你想访问关键部分时调用此操作。如果有其他线程正在运行此关键部分,其他线程将被阻塞,直到它们被锁唤醒以获取访问关键部分的权限。

  • unlock(): 你在关键部分结束时调用此操作,以允许其他线程访问它。

在 Java 并发 API 中,锁在 Lock 接口中声明,并在某些类中实现,例如 ReentrantLock 类。

在这个菜谱中,你将学习如何通过实现一个实现了 Lock 接口的类来创建自己的 Lock 对象,该对象可以用于保护关键部分。

准备中

此菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 MyAbstractQueuedSynchronizer 的类,它扩展了 AbstractQueuedSynchronizer 类:
        public class MyAbstractQueuedSynchronizer extends
                                        AbstractQueuedSynchronizer {

  1. 声明一个名为 state 的私有 AtomicInteger 属性:
        private final AtomicInteger state;

  1. 实现类的构造函数,以初始化其属性:
        public MyAbstractQueuedSynchronizer() { 
          state=new AtomicInteger(0); 
        }

  1. 实现一个名为 tryAcquire() 的方法。此方法尝试将状态变量的值从零更改为一。如果成功,则返回 true 值;否则,返回 false
        @Override 
        protected boolean tryAcquire(int arg) { 
          return state.compareAndSet(0, 1); 
        }

  1. 实现一个名为 tryRelease() 的方法。此方法尝试将状态变量的值从一更改为零。如果成功,则返回 true;否则,返回 false
        @Override 
        protected boolean tryRelease(int arg) { 
          return state.compareAndSet(1, 0); 
        }

  1. 创建一个名为 MyLock 的类,并指定它实现 Lock 接口:
        public class MyLock implements Lock{

  1. 声明一个名为 sync 的私有 AbstractQueuedSynchronizer 属性:
        private final AbstractQueuedSynchronizer sync;

  1. 实现类的构造函数,以使用一个新的 MyAbstractQueueSynchronizer 对象初始化 sync 属性:
        public MyLock() { 
          sync=new MyAbstractQueuedSynchronizer(); 
        }

  1. 实现一个名为 lock() 的方法。调用 sync 对象的 acquire() 方法:
        @Override 
        public void lock() { 
          sync.acquire(1); 
        }

  1. 实现一个名为 lockInterruptibly() 的方法。调用 sync 对象的 acquireInterruptibly() 方法:
        @Override 
        public void lockInterruptibly() throws InterruptedException { 
          sync.acquireInterruptibly(1); 
        }

  1. 实现一个名为 tryLock() 的方法。调用 sync 对象的 tryAcquireNanos() 方法:
        @Override 
        public boolean tryLock() { 
          try { 
            return sync.tryAcquireNanos(1, 1000); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
            Thread.currentThread().interrupt(); 
            return false; 
          } 
        }

  1. 实现一个带有两个参数的 tryLock() 方法的另一个版本:一个名为 time 的长参数和一个名为 unitTimeUnit 参数。调用 sync 对象的 tryAcquireNanos() 方法:
        @Override 
        public boolean tryLock(long time, TimeUnit unit) throws
                                         InterruptedException { 
          return sync.tryAcquireNanos(1, TimeUnit.NANOSECONDS
                                             .convert(time, unit)); 
        }

  1. 实现一个名为 unlock() 的方法。调用 sync 对象的 release() 方法:
        @Override 
        public void unlock() { 
          sync.release(1); 
        }

  1. 实现一个名为 newCondition() 的方法。创建 sync 对象内部类 ConditionObject 的新对象:
        @Override 
        public Condition newCondition() { 
          return sync.new ConditionObject(); 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 声明一个名为 lock 的私有 MyLock 属性:
        private final MyLock lock;

  1. 声明一个名为 name 的私有 String 属性:
        private final String name;

  1. 实现类的构造函数,以初始化其属性:
        public Task(String name, MyLock lock){ 
          this.lock=lock; 
          this.name=name; 
        }

  1. 实现类的 run() 方法。获取锁,让线程休眠 2 秒,然后释放锁对象:
        @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(); 
          } 
        }

  1. 通过创建一个名为 Main 的类并包含一个 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个名为 lockMyLock 对象:
        MyLock lock=new MyLock();

  1. 创建并执行 10 个 Task 任务:
        for (int i=0; i<10; i++){ 
          Task task=new Task("Task-"+i,lock); 
          Thread thread=new Thread(task); 
          thread.start(); 
        }

  1. 使用 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);

  1. 写一条消息,表示您已获取锁并释放它:
        System.out.printf("Main: Got the lock\n"); 
        lock.unlock();

  1. 写一条消息,表示程序结束:
        System.out.printf("Main: End of the program\n");

它是如何工作的...

Java 并发 API 提供了一个可以用来实现具有锁或信号量功能的同步机制的类。它被称为AbstractQueuedSynchronizer,正如其名所示,它是一个抽象类。它提供了控制访问临界区和管理阻塞并等待访问该区域的线程队列的操作。这些操作基于两个抽象方法:

  • tryAcquire(): 这个方法被调用以尝试获取对临界区的访问。如果调用它的线程可以访问临界区,则该方法返回true值。否则,它返回false值。

  • tryRelease(): 这个方法被调用以尝试释放对临界区的访问。如果调用它的线程可以释放访问,则该方法返回true值。否则,它返回false值。

在这些方法中,你必须实现你用来控制临界区访问的机制。在这种情况下,你实现了扩展AbstractQueuedSyncrhonizer类的MyAbstractQueuedSynchonizer类,并使用AtomicInteger变量来控制临界区的访问。如果锁是空闲的,这个变量将存储值0,这样线程就可以访问临界区;如果锁被阻塞,变量将存储值1,这样线程就无法访问临界区。

你使用了AtomicInteger类提供的compareAndSet()方法,该方法尝试将第一个参数指定的值更改为第二个参数指定的值。为了实现tryAcquire()方法,你尝试将原子变量的值从零更改为一。同样,为了实现tryRelease()方法,你尝试将原子变量的值从一更改为零。

你必须实现AtomicInteger类,因为其他AbstractQueuedSynchronizer类的实现(例如,ReentrantLock使用的实现)在内部作为私有类实现。这是在使用的类中完成的,因此你无法访问它。

然后,你实现了MyLock类。这个类实现了Lock接口,并有一个MyQueuedSynchronizer对象作为属性。为了实现Lock接口的所有方法,你使用了MyQueuedSynchronizer对象的方法。

最后,你实现了实现Runnable接口的Task类,并使用MyLock对象来获取对临界区的访问。这个临界区使线程休眠 2 秒。main类创建了一个MyLock对象,并运行了 10 个共享锁的Task对象。main类还尝试使用tryLock()方法获取对锁的访问。

当你执行示例时,你可以看到只有一个线程可以访问临界区,当这个线程完成时,另一个线程就可以访问它了。

你可以使用自己的 Lock 接口来记录其利用率日志,控制其锁定时间,或者实现高级同步机制来控制,例如,资源的访问,以便它只在特定时间可用。

更多内容...

AbstractQueuedSynchronizer 类提供了两个可以用来管理锁状态的方法。它们是 getState()setState() 方法。这些方法接收和返回一个表示锁状态的整数值。你可以使用它们而不是 AtomicInteger 属性来存储锁的状态。

Java 并发 API 提供了另一个类来实现同步机制。它是 AbstractQueuedLongSynchronizer 类,它与 AbstractQueuedSynchronizer 相当,但使用一个长属性来存储线程的状态。

参见

  • 在 第二章 的 使用锁同步代码块 菜谱中,基本线程同步

基于优先级的传输队列实现

Java 9 API 提供了几个数据结构来处理并发应用程序。从这些数据结构中,我们想强调以下两个数据结构:

  • LinkedTransferQueue:这种数据结构应该用于具有生产者/消费者结构的应用程序。在这样的应用程序中,你有一个或多个数据的生产者和一个或多个数据的消费者,所有这些共享一个数据结构。生产者将数据放入数据结构中,消费者从那里取出数据。如果数据结构为空,消费者将被阻塞,直到他们有数据可以消费。如果它已满,生产者将被阻塞,直到他们有空间可以放置数据。

  • PriorityBlockingQueue:在这个数据结构中,元素以有序方式存储。它们必须实现 Comparable 接口并带有 compareTo() 方法。当你将一个元素插入结构时,它会与结构中的元素进行比较,直到找到其位置。

LinkedTransferQueue 的元素按照它们到达的顺序存储,因此先到达的元素会被优先消费。当你想要开发一个生产者/消费者程序,其中数据是按照某些优先级而不是到达时间进行消费时,这种情况可能会发生。在这个菜谱中,你将学习如何实现一个用于生产者/消费者问题的数据结构,其元素将按照优先级排序;优先级更高的元素将被优先消费。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为 MyPriorityTransferQueue 的类,该类扩展了 PriorityBlockingQueue 类并实现了 TransferQueue 接口:
        public class MyPriorityTransferQueue<E> extends
                 PriorityBlockingQueue<E> implements TransferQueue<E> {

  1. 声明一个名为 counter 的私有 AtomicInteger 属性,用于存储等待消费元素的消费者数量:
        private final AtomicInteger counter;

  1. 声明一个名为 transferred 的私有 LinkedBlockingQueue 属性:
        private final LinkedBlockingQueue<E> transfered;

  1. 声明一个名为 lock 的私有 ReentrantLock 属性:
        private final ReentrantLock lock;

  1. 实现类的构造函数以初始化其属性:
        public MyPriorityTransferQueue() { 
          counter=new AtomicInteger(0); 
          lock=new ReentrantLock(); 
          transfered=new LinkedBlockingQueue<E>(); 
        }

  1. 实现名为 tryTransfer() 的方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有消费者等待,则方法返回 false
        @Override 
        public boolean tryTransfer(E e) { 
          boolean value=false; 
          try { 
            lock.lock(); 
            if (counter.get() == 0) { 
              value = false; 
            } else { 
              put(e); 
              value = true; 
            } 
          } finally { 
            lock.unlock(); 
          } 
          return value;    
        }

  1. 实现名为 transfer() 的方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有消费者等待,该方法将元素存储在一个特殊队列中,以便发送给第一个尝试获取元素的消费者,并阻塞线程直到元素被消费:
          @Override 
          public void transfer(E e) throws InterruptedException { 
            lock.lock(); 
            if (counter.get()!=0) { 
              try { 
                put(e); 
              } finally { 
                lock.unlock(); 
              } 
            } else { 
              try { 
                transfered.add(e); 
              } finally { 
                lock.unlock(); 
              } 
              synchronized (e) { 
              e.wait(); 
            } 
          } 
        }

  1. 实现一个名为 tryTransfer() 的方法,该方法接收三个参数:元素、如果没有消费者等待时等待消费者的时间,以及指定等待时间的单位。如果有等待的消费者,它立即发送元素。否则,它将指定的时间转换为毫秒,并使用 wait() 方法使线程休眠。当消费者取走元素时,如果线程正在 wait() 方法中休眠,你需要使用 notify() 方法唤醒它,就像你马上要看到的那样:
        @Override 
        public boolean tryTransfer(E e, long timeout, TimeUnit unit)
                                       throws InterruptedException { 
          lock.lock(); 
          if (counter.get() != 0) { 
            try { 
              put(e); 
            } finally { 
              lock.unlock(); 
            } 
            return true; 
          } else { 
            long newTimeout=0; 
            try { 
              transfered.add(e); 
              newTimeout = TimeUnit.MILLISECONDS.convert(timeout, unit); 
            } finally { 
              lock.unlock(); 
            } 
            e.wait(newTimeout); 
            lock.lock(); 
            boolean value; 
            try { 
              if (transfered.contains(e)) { 
                transfered.remove(e); 
                value = false; 
              } else { 
                value = true; 
              } 
            } finally { 
              lock.unlock(); 
            } 
            return value; 
          } 
        }

  1. 实现名为 hasWaitingConsumer() 的方法。使用 counter 属性的值来计算此方法的返回值。如果 counter 的值大于零,则返回 true;否则,返回 false
        @Override 
        public boolean hasWaitingConsumer() { 
          return (counter.get()!=0); 
        }

  1. 实现名为 getWaitingConsumerCount() 的方法。返回 counter 属性的值:
        @Override 
        public int getWaitingConsumerCount() { 
          return counter.get(); 
        }

  1. 实现名为 take() 的方法。该方法由消费者在想要消费元素时调用。首先,获取之前定义的锁并增加等待消费者的数量:
        @Override 
        public E take() throws InterruptedException { 
          lock.lock(); 
          try { 
            counter.incrementAndGet();

  1. 如果已传递队列中没有元素,释放锁并尝试使用 take() 元素从队列中获取元素,然后再次获取锁。如果队列中没有元素,此方法将使线程休眠,直到有元素可以消费:
        E value=transfered.poll(); 
        if (value==null) { 
          lock.unlock(); 
          value=super.take(); 
          lock.lock();

  1. 否则,从已传递队列中取出元素,如果有一个线程正在等待消费该元素,则唤醒它。考虑到你正在同步一个从外部进入此类的对象。你必须保证该对象在应用程序的其他部分不会被用于锁定:
        } else { 
          synchronized (value) { 
            value.notify(); 
          } 
        }

  1. 最后,减少等待消费者的计数器并释放锁:
            counter.decrementAndGet(); 
          } finally { 
            lock.unlock(); 
          } 
          return value; 
        }

  1. 接下来,实现一个名为 Event 的类,该类扩展了由 Event 类参数化的 Comparable 接口:
        public class Event implements Comparable<Event> {

  1. 声明一个名为 thread 的私有 String 属性,用于存储创建事件的线程名称:
        private final String thread;

  1. 声明一个名为 priority 的私有 int 属性,用于存储事件的优先级:
        private final int priority;

  1. 实现类的构造函数以初始化其属性:
        public Event(String thread, int priority){ 
          this.thread=thread; 
          this.priority=priority; 
        }

  1. 实现一个方法以返回 thread 属性的值:
        public String getThread() { 
          return thread; 
        }

  1. 实现一个方法以返回 priority 属性的值:
        public int getPriority() { 
          return priority; 
        }

  1. 实现 compareTo() 方法。此方法比较实际事件与作为参数接收的事件。如果实际事件比参数具有更高的优先级,则返回 -1;如果实际事件比参数具有更低的优先级,则返回 1;如果两个事件具有相同的优先级,则返回 0。您将按优先级降序获取列表。具有更高优先级的事件将首先存储在队列中:
        public int compareTo(Event e) { 
          return Integer.compare(e.priority, this.getPriority()); 
        }

  1. 实现一个名为 Producer 的类,该类实现了 Runnable 接口:
        public class Producer implements Runnable {

  1. 声明一个名为 buffer 的私有 MyPriorityTransferQueue 属性,该属性由 Event 类参数化,用于存储由该生产者生成的事件:
        private final MyPriorityTransferQueue<Event> buffer;

  1. 实现类的构造函数以初始化其属性:
        public Producer(MyPriorityTransferQueue<Event> buffer) { 
          this.buffer=buffer; 
        }

  1. 实现类的 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); 
          } 
        }

  1. 实现一个名为 Consumer 的类,该类实现了 Runnable 接口:
        public class Consumer implements Runnable {

  1. 声明一个名为 buffer 的私有 MyPriorityTransferQueue 属性,用于获取该类消费的事件:
        private final MyPriorityTransferQueue<Event> buffer;

  1. 实现类的构造函数以初始化其属性:
        public Consumer(MyPriorityTransferQueue<Event> buffer) { 
          this.buffer=buffer; 
        }

  1. 实现 run() 方法。使用 take() 方法消费 1,002 个事件(示例中生成的事件全部),并将生成事件的线程数及其优先级写入控制台:
        @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(); 
            } 
          } 
        }

  1. 通过创建一个名为 Main 的类并实现一个 main() 方法来实现示例的 main 类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 创建一个名为 bufferMyPriorityTransferQueue 对象:
        MyPriorityTransferQueue<Event> buffer=new
                                MyPriorityTransferQueue<Event>();

  1. 创建一个 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(); 
        }

  1. 创建并启动一个 Consumer 任务:
        Consumer consumer=new Consumer(buffer); 
        Thread consumerThread=new Thread(consumer); 
        consumerThread.start();

  1. 在控制台中写入实际的消费者数量:
        System.out.printf("Main: Buffer: Consumer count: %d\n",
                          buffer.getWaitingConsumerCount());

  1. 使用 transfer() 方法将事件传递给消费者:
        Event myEvent=new Event("Core Event",0); 
        buffer.transfer(myEvent); 
        System.out.printf("Main: My Event has ben transfered.\n");

  1. 使用 join() 方法等待生产者的最终化:
        for (int i=0; i<producerThreads.length; i++) { 
          try { 
            producerThreads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 让线程休眠 1 秒:
        TimeUnit.SECONDS.sleep(1);

  1. 写入实际的消费者数量:
        System.out.printf("Main: Buffer: Consumer count: %d\n",
                          buffer.getWaitingConsumerCount());

  1. 使用 transfer() 方法传递另一个事件:
        myEvent=new Event("Core Event 2",0); 
        buffer.transfer(myEvent);

  1. 使用 join() 方法等待消费者的最终化:
        consumerThread.join();

  1. 编写一条消息以指示程序结束:
        System.out.printf("Main: End of the program\n");

它是如何工作的...

在这个菜谱中,你实现了 MyPriorityTransferQueue 数据结构。这是一个用于生产者/消费者问题的数据结构,但其元素是按优先级排序的,而不是按到达顺序排序。由于 Java 不允许多重继承,你做出的第一个决定与 MyPriorityTransferQueue 类的基类有关。你扩展了这个类以使用 PriorityBlockingQueue 中实现的操作,而不是实现它们。你还实现了 TransferQueue 接口以添加与生产者/消费者相关的功能。我们做出这个选择是因为我们认为实现 TransferQueue 接口的方法比实现 PriorityBlockingQueue 类中的方法更容易。然而,你也可以实现从 LinkedTransferQueue 类扩展的类,并实现必要的方法以获得自己的 PriorityBlockingQueue 类版本。

MyPriortyTransferQueue 类有以下三个属性:

  • AtomicInteger 属性名为 counter:此属性存储等待从数据结构中取元素的消费者数量。当消费者调用 take() 操作从数据结构中取元素时,计数器增加。当消费者完成 take() 操作的执行后,计数器再次减少。此计数器用于 hasWaitingConsumer()getWaitingConsumerCount() 方法的实现。

  • ReentrantLock 属性名为 lock:此属性用于控制对实现操作的访问。根据此属性,只允许一个线程与数据结构一起工作。

  • 最后,它有一个 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接口,因为这是你的数据结构的要求。

然后,你实现了ProducerConsumer类。在示例中,你有 10 个生产者和一个消费者,他们共享同一个缓冲区。每个生产者生成了 100 个具有递增优先级的事件,因此优先级较高的事件是最后生成的。

示例的主类创建了一个MyPriorityTransferQueue对象、10 个生产者和一个消费者,并使用MyPriorityTransferQueue缓冲区的transfer()方法将两个事件传输到缓冲区。

以下截图显示了程序执行的部分输出:

你可以查看具有更高优先级的事件是如何首先被消费的,以及消费者是如何消费传递的事件的。

相关内容

  • 在第七章的使用按优先级排序的阻塞线程安全队列使用阻塞线程安全双端队列菜谱中,并发集合

实现自己的原子对象

原子变量是在 Java 5 版本中引入的;它们提供对单个变量的原子操作。当一个线程使用原子变量执行操作时,类的实现包括一个机制来检查操作是否是原子性的。

在这个菜谱中,你将学习如何扩展原子对象并实现两个遵循原子对象机制的运算,以保证所有操作都在一个步骤中完成。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等不同的 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为ParkingCounter的类,并指定它扩展AtomicInteger类:
        public class ParkingCounter extends AtomicInteger {

  1. 声明一个名为maxNumber的私有int属性以存储允许进入停车场的最大汽车数量:
        private final int maxNumber;

  1. 实现类的构造函数以初始化其属性:
        public ParkingCounter(int maxNumber){ 
          set(0); 
          this.maxNumber=maxNumber; 
        }

  1. 实现名为 carIn() 的方法。如果计数器的值小于已设定的最大值,则该方法增加汽车计数器。构建一个无限循环并使用 get() 方法获取内部计数器的值:
        public boolean carIn() { 
          for (;;) { 
            int value=get();

  1. 如果值等于 maxNumber 属性,则计数器不能增加(停车场已满,汽车不能进入)。在这种情况下,方法返回 false 值:
        if (value==maxNumber) { 
          System.out.printf("ParkingCounter: The parking lot is full.\n"); 
          return false;

  1. 否则,增加值并使用 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; 
              } 
            } 
          } 
        }

  1. 实现名为 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; 
              } 
            } 
          } 
        }

  1. 创建一个名为 Sensor1 的类,该类实现了 Runnable 接口:
        public class Sensor1 implements Runnable {

  1. 声明一个名为 counter 的私有 ParkingCounter 属性:
        private final ParkingCounter counter;

  1. 实现类的构造函数以初始化其属性:
        public Sensor1(ParkingCounter counter) { 
          this.counter=counter; 
        }

  1. 实现名为 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(); 
        }

  1. 创建一个名为 Sensor2 的类,该类实现了 Runnable 接口:
        public class Sensor2 implements Runnable {

  1. 声明一个名为 counter 的私有 ParkingCounter 属性:
        private ParkingCounter counter;

  1. 实现类的构造函数以初始化其属性:
        public Sensor2(ParkingCounter counter) { 
          this.counter=counter; 
        }

  1. 实现名为 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(); 
        }

  1. 通过创建一个名为 Main 的类并包含一个 main() 方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 创建一个名为 counterParkingCounter 对象:
        ParkingCounter counter=new ParkingCounter(5);

  1. 创建并启动一个 Sensor1Sensor2 任务:
        Sensor1 sensor1=new Sensor1(counter); 
        Sensor2 sensor2=new Sensor2(counter); 

        Thread thread1=new Thread(sensor1); 
        Thread thread2=new Thread(sensor2); 

        thread1.start(); 
        thread2.start();

  1. 等待两个任务的最终化:
        thread1.join(); 
        thread2.join();

  1. 在控制台输出计数器的实际值:
        System.out.printf("Main: Number of cars: %d\n",counter.get());

  1. 在控制台输出表示程序结束的消息:
        System.out.printf("Main: End of the program.\n");

它是如何工作的...

ParkingCounter 类通过两个原子操作 carIn()carOut() 扩展了 AtomicInteger 类。该示例模拟了一个控制系统内停车场内汽车数量的系统。停车场可以接纳一定数量的汽车,该数量由 maxNumber 属性表示。

carIn() 操作比较停车场内实际汽车数量与最大值。如果它们相等,汽车不能进入停车场,方法返回 false 值。否则,它使用以下原子操作的以下结构:

  • 在局部变量中获取原子对象的值。

  • 将新值存储在不同的变量中。

  • 使用 compareAndSet() 方法尝试用新值替换旧值。如果此方法返回 true,则表示您作为参数发送的旧值是变量的值;因此,它改变了值。操作是以原子方式进行的,因为 carIn() 方法返回 true。如果 compareAndSet() 方法返回 false,则表示您作为参数发送的旧值不是变量的值(其他线程已修改它);因此,操作不能以原子方式完成。操作将重新开始,直到可以以原子方式完成。

carOut() 方法与 carIn() 方法类似。您还实现了两个 Runnable 对象,它们使用 carIn()carOut() 方法来模拟停车活动。当您执行程序时,您可以看到停车场永远不会超过汽车的最大数量。

参见

  • 在 第七章 的 使用原子变量 菜单中,并发集合

实现自己的流生成器

流是一系列数据,允许您以顺序或并行方式对其应用一系列操作(通常用 lambda 表达式表示),以过滤、转换、排序、归约或构建新的数据结构。它在 Java 8 中引入,并且是该版本中引入的最重要特性之一。

流基于 Stream 接口以及包含在 java.util.stream 包中的相关类和接口。它们还引发了在许多类中引入新方法,以从不同的数据结构生成流。您可以从实现 Collection 接口的每个数据结构创建 Stream 接口:从 FileDirectoryArray 以及许多其他来源。

Java 还包括从您自己的源创建流的不同机制。其中最重要的包括:

  • Supplier 接口:此接口定义了 get() 方法。当 Stream 需要处理另一个对象时,它将被调用。您可以使用 Stream 类的 generate() 静态方法从 Supplier 接口创建 Stream。请注意,此源可能是无限的,因此您必须使用 limit() 或类似方法来限制 Stream 中的元素数量。

  • Stream.Builder 接口:此接口提供了 accept()add() 元素来向 Stream 添加元素,以及 build() 方法,该方法返回使用之前添加的元素创建的 Stream 接口。

  • Spliterator 接口:此接口定义了遍历和分割源元素所需的方法。您可以使用 StreamSupport 类的 stream() 方法生成 Stream 接口以处理 Spliterator 的元素。

在本章中,你将学习如何实现自己的 Spliterator 接口以及如何创建一个 Stream 接口来处理其数据。我们将使用一个元素矩阵。正常的 Stream 接口应该一次处理一个元素,但我们将使用 Spliterator 类一次处理一行。

准备工作

这个食谱的示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。

如何实现...

按照以下步骤实现示例:

  1. 创建一个名为 Item 的类来存储矩阵中每个元素的信息。它将包含三个私有属性:一个名为 nameString 属性和两个名为 rowcolumn 的整型属性。创建获取和设置这些属性值的方法。这个类的代码非常简单,所以这里不会包含它。

  2. 创建一个名为 MySpliterator 的类。指定它实现了由 Item 类参数化的 Spliterator 接口。这个类有四个属性:一个名为 itemsItem 对象矩阵和三个整型属性 startendcurrent,用于存储将由这个 Spliterator 接口处理的第一和最后一个元素以及正在处理的当前元素。实现这个类的构造函数以初始化所有这些属性:

        public class MySpliterator implements Spliterator<Item> { 

          private Item[][] items; 
          private int start, end, current; 

          public MySpliterator(Item[][] items, int start, int end) { 
            this.items=items; 
            this.start=start; 
            this.end=end; 
            this.current=start; 
          }

  1. 实现 characteristics() 方法。这个方法将返回一个 int 类型的值,用于描述 Spliterator 的行为。这个值的含义将在后面的 工作原理... 部分进行解释:
        @Override 
        public int characteristics() { 
          return ORDERED | SIZED | SUBSIZED; 
        }

  1. 实现 estimatedSize() 方法。这个方法将返回这个 Spliterator 需要处理的元素数量。我们将通过计算 endcurrent 属性之间的差值来计算它:
        @Override 
        public long estimateSize() { 
          return end - current; 
        }

  1. 现在实现 tryAdvance() 方法。这个方法将被调用以尝试处理 Spliterator 的一个元素。tryAdvance() 方法的输入参数是一个实现了 Consumer 接口的对象。它将由 Stream API 调用,所以我们只需要关注它的实现。在我们的例子中,正如本章引言中提到的,我们有一个 Item 对象的矩阵,并且我们每次将处理一行。接收到的 Consumer 函数将处理一个 Item 对象。因此,如果 Spliterator 接口仍有元素要处理,我们将使用 Consumer 函数的 accept() 方法处理当前行的所有项目:
        @Override 
        public boolean tryAdvance(Consumer<? super Item> consumer) { 
          System.out.printf("MySpliterator.tryAdvance.start: %d, %d, %d\n",
                            start,end,current); 
            if (current < end) { 
              for (int i=0; i<items[current].length; i++) { 
                consumer.accept(items[current][i]); 
              } 
              current++; 
              System.out.printf("MySpliterator.tryAdvance.end:true\n"); 
              return true; 
            } 
            System.out.printf("MySpliterator.tryAdvance.end:false\n"); 
            return false; 
          }

  1. 现在实现 forEachRemaining() 方法。这个方法将接收一个 Consumer 接口的实现,并将这个函数应用于 Spliterator 的剩余元素。在我们的例子中,我们将为所有剩余元素调用 tryAdvance() 方法:
        @Override 
        public void forEachRemaining(Consumer<? super Item> consumer) { 
          System.out.printf("MySpliterator.forEachRemaining.start\n"); 
          boolean ret; 
          do { 
            ret=tryAdvance(consumer); 
          } while (ret); 
          System.out.printf("MySpliterator.forEachRemaining.end\n"); 
        }

  1. 最后,实现 trySplit() 方法。这个方法将由并行流调用,将 Spliterator 分割成两个子集。它将返回一个新 Spliterator 对象,该对象包含将由另一个线程处理的元素。当前线程将处理剩余的元素。如果 spliterator 对象不能分割,你必须返回一个 null 值。在我们的情况下,我们将计算我们必须处理的元素中间的元素。前半部分将由当前线程处理,后半部分将由另一个线程处理:
        @Override 
        public Spliterator<Item> trySplit() { 
          System.out.printf("MySpliterator.trySplit.start\n"); 

          if (end-start<=2) { 
            System.out.printf("MySpliterator.trySplit.end\n"); 
            return null; 
          } 
          int mid=start+((end-start)/2); 
          int newStart=mid; 
          int newEnd=end; 
          end=mid; 
          System.out.printf("MySpliterator.trySplit.end: %d, %d, %d,
                            %d, %d, %d\n",start, mid, end, newStart,
                            newEnd, current); 

          return new MySpliterator(items, newStart, newEnd); 
        }

  1. 现在,实现项目的 Main 类及其 main() 方法。首先,声明并初始化一个包含 10 行 10 列 Item 对象的矩阵:
        public class Main { 

          public static void main(String[] args) { 
            Item[][] items; 
            items= new Item[10][10]; 

            for (int i=0; i<10; i++) { 
              for (int j=0; j<10; j++) { 
                items[i][j]=new Item(); 
                items[i][j].setRow(i); 
                items[i][j].setColumn(j); 
                items[i][j].setName("Item "+i+" "+j); 
              } 
            }

  1. 然后,创建一个 MySpliterator 对象来处理矩阵中的所有元素:
        MySpliterator mySpliterator=new MySpliterator(items, 0,
                                                      items.length);

  1. 最后,使用 StreamSupport 类的 stream() 方法从 Spliterator 创建一个流。将 true 值作为第二个参数传递,表示我们的流将是并行的。然后,使用 Stream 类的 forEach() 方法写入每个元素的信息:
          StreamSupport.stream(mySpliterator, true).forEach( item -> { 
            System.out.printf("%s: %s\n",Thread.currentThread()
                              .getName(),item.getName()); 
          }); 
        }

它是如何工作的...

本例的主要元素是 Spliterator。这个接口定义了可以用来处理和分区元素源的方法,例如 Stream 对象的源。你很少需要直接使用 Spliterator 对象。只有当你想要不同的行为——也就是说,如果你想要实现自己的数据结构并从中创建 Stream——才使用 Spliterator 对象。

Spliterator 有一个定义其行为的特征集。具体如下:

  • CONCURRENT: 数据源可以安全地并发修改

  • DISTINCT: 数据源中的所有元素都是唯一的

  • IMMUTABLE: 数据源中的元素可以被添加、删除或替换

  • NONNULL: 数据源中没有 null 元素

  • ORDERED: 数据源中的元素有一个顺序

  • SIZED: estimateSize() 方法返回的值是 Spliterator 的确切大小

  • SORTED: Spliterator 的元素是有序的

  • SUBSIZED: 在调用 trySplit() 方法后,你可以获得 Spliterator 两部分的精确大小

在我们的情况下,我们使用 DISTINCTIMMUTABLENONNULLORDEREDSIZEDSUBSIZED 特征定义了 Spliterator

然后,我们实现了 Spliterator 接口中所有没有默认实现的方法:

  • characteristics(): 这个方法返回 Spliterator 对象的特征。具体来说,它返回一个整数值,你使用位或运算符 (|) 在你的 Spliterator 对象的各个特征之间计算得到。请注意,返回的值应该与你的 Spliterator 对象的真实特征一致。

  • estimatedSize():此方法返回在当前时刻调用forEachRemaining()方法时将处理的元素数量。在我们的情况下,我们返回了确切值,因为我们知道它,但方法的定义讨论了估计的大小。

  • tryAdvance():此方法将指定的函数应用于下一个要处理的元素(如果有的话),并返回 true。如果没有要处理的元素,它将返回 false。在我们的情况下,此方法接收了一个Consumer,它处理了一个 Item 对象,但我们一次处理一行 Item 对象。因此,我们遍历了该行的所有项目,并调用了Consumeraccept()方法。

  • trySplit():此方法用于将当前Spliterator分割成两个不同的部分,以便每个部分可以由不同的线程处理。在理想情况下,您应该将数据源分成具有相同元素数量的两半。但在我们的情况下,我们计算了起始索引和结束索引之间的中间元素,并生成了两个元素块。从起始到中间元素的部分由当前Spliterator处理,从中间到结束元素的部分由新的Spliterator对象处理。如果您不能分割数据源,此方法将返回一个 null 值。在我们的情况下,Spliterator只有两个元素,所以它不会被分割。

Spliterator接口的其他方法具有默认实现,但我们重写了forEachRemaining()方法。此方法将接收到的函数(Consumer接口的实现)应用于尚未处理的Spliterator的元素。我们实现了自己的版本,以便在控制台写入消息。我们使用了tryAdvance()方法来处理每个单独的项目。

以下截图显示了此示例的部分输出:

图片

首先,调用trySplit()方法来分割数据源,然后调用forEachRemaining()方法来处理由trySplit()方法生成的每个Spliterator的所有元素。

更多...

您可以从不同的数据源获取Spliterator接口的实现。BaseStream类提供了spliterator()方法,该方法从Stream的元素中返回一个Spliterator。其他数据结构,如ConcurrentLinkedDequeConcurrentLinkedQueueCollection,也提供了spliterator()方法,以获取该接口的实现来处理这些数据结构的元素。

参见

  • 第六章中“从不同源创建流”的配方,并行和反应式流

实现自己的异步流

反应式流(www.reactive-streams.org/)定义了一种机制,以提供具有非阻塞背压的异步流处理。

反应式流基于三个元素:

  • 它是信息发布者

  • 它有一个或多个此信息的订阅者

  • 它在发布者和消费者之间提供订阅关系

Java 9 包含了三个接口--Flow.PublisherFlow.SubscriberFlow.Subscription--以及一个实用类,SubmissionPublisher,以便我们实现反应式流应用程序。

在本菜谱中,您将学习如何仅使用三个接口实现自己的反应式应用程序。请注意,我们将实现三个元素之间的预期行为。发布者只会向请求它们的订阅者发送元素,并且将以并发方式这样做。但您可以通过修改方法的实现轻松地修改此行为。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 实现一个名为 News 的类。该类实现了从发布者发送到订阅者的元素。它将有两个名为 titlecontent 的私有 String 属性,以及一个名为 dateDate 属性。它还将有获取和设置这些属性值的方法。这个类的源代码非常简单,所以这里不会包含它。

  2. 创建一个名为 Consumer 的类,并指定它实现由 News 类参数化的 Subscriber 接口。它将有两个私有属性:一个名为 subscriptionSubscription 对象和一个名为 nameString 属性。实现类的构造函数以初始化 name 属性:

        public class Consumer implements Subscriber<News> { 

          private Subscription subscription; 
          private String name; 

          public Consumer(String name) { 
            this.name=name; 
          }

  1. 实现 onComplete() 方法。当发布者不再发送任何额外元素时,应调用此方法。在我们的例子中,我们只在控制台写入一条消息:
        @Override 
        public void onComplete() { 
          System.out.printf("%s - %s: Consumer - Completed\n", name,
                            Thread.currentThread().getName()); 
        }

  1. 实现 onError() 方法。当发生错误时,发布者应调用此方法。在我们的例子中,我们只在控制台写入一条消息:
        @Override 
        public void onError(Throwable exception) { 
          System.out.printf("%s - %s: Consumer - Error: %s\n", name,
                            Thread.currentThread().getName(),
                            exception.getMessage()); 
        }

  1. 然后,实现 onNext()。此方法接收一个 News 对象作为参数,并且当发布者向订阅者发送项目时,应由发布者调用。在我们的例子中,我们在控制台写入 News 对象属性的值,并使用 Subscription 对象的 request() 方法请求额外的项目:
        @Override 
        public void onNext(News item) { 
          System.out.printf("%s - %s: Consumer - News\n", name,
                            Thread.currentThread().getName()); 
          System.out.printf("%s - %s: Title: %s\n", name,
                            Thread.currentThread().getName(),
                            item.getTitle()); 
          System.out.printf("%s - %s: Content: %s\n", name,
                            Thread.currentThread().getName(),
                            item.getContent()); 
          System.out.printf("%s - %s: Date: %s\n", name,
                            Thread.currentThread().getName(),
                            item.getDate()); 
          subscription.request(1); 
        }

  1. 最后,实现 onSubscription()。此方法将由发布者调用,并且将是它调用的 Subscriber 的第一个方法。它接收发布者和订阅者之间的 Subscription。在我们的例子中,我们存储 Subscription 对象,并使用 request() 方法请求订阅者处理第一个项目:
        @Override 
        public void onSubscribe(Subscription subscription) { 
          this.subscription = subscription; 
          subscription.request(1); 
          System.out.printf("%s: Consumer - Subscription\n",
                            Thread.currentThread().getName()); 
        }

  1. 实现一个名为 MySubscription 的类,并指定它实现 Subscription 接口。它将有一个名为 canceled 的私有 Boolean 属性和一个名为 requested 的私有整数属性:
        public class MySubscription implements Subscription { 

          private boolean canceled=false; 
          private long requested=0;

  1. 实现由Subscription接口提供的cancel()方法以取消发布者和订阅者之间的通信。在我们的例子中,我们将取消属性设置为true
        @Override 
        public void cancel() { 
          canceled=true; 
        }

  1. 实现由Subscription接口提供的request()方法。该方法由订阅者用于从发布者请求元素。它接收订阅者请求的元素数量作为参数。在我们的例子中,我们增加请求属性的值:
        @Override 
        public void request(long value) { 
          requested+=value; 
        }

  1. 实现获取取消属性值的isCanceled()方法,获取请求属性值的getRequested()方法,以及减少请求属性值的decreaseRequested()方法:
        public boolean isCanceled() { 
          return canceled; 
        } 

        public long getRequested() { 
          return requested; 
        } 

        public void decreaseRequested() { 
          requested--;      
        }

  1. 实现一个名为ConsumerData的类。这个类将由发布者用来存储每个订阅者的信息。它将有一个名为consumer的私有Consumer属性和一个名为subscription的私有MySubscription属性。它还将有get()set()这些属性值的方法。这个类的源代码非常简单,所以这里不会包含它。

  2. 实现一个名为PublisherTask的类,并指定它实现了Runnable接口。它将有一个名为consumerData的私有ConsumerData属性和一个名为news的私有News属性。实现一个构造函数以初始化这两个属性:

        public class PublisherTask implements Runnable { 

        private ConsumerData consumerData; 
        private News news; 

        public PublisherTask(ConsumerData consumerData, News news) { 
          this.consumerData = consumerData; 
          this.news = news; 
        }

  1. 实现run()方法。该方法将获取ConsumerData属性的MySubscription对象。如果订阅没有被取消,并且已经请求了元素(属性值大于 0),我们将使用其onNext()方法将News对象发送给订阅者,然后减少请求属性的值:
          @Override 
          public void run() { 
            MySubscription subscription = consumerData.getSubscription(); 
            if (!(subscription.isCanceled() && (subscription.getRequested()
                                                               > 0))) { 
              consumerData.getConsumer().onNext(news); 
              subscription.decreaseRequested(); 
            } 
          } 
        }

  1. 然后,实现一个名为MyPublisher的类,并指定它实现了由News类参数化的Publisher接口。它将存储一个私有的ConsumerData对象ConcurrentLinkedDeque和一个名为executorThreadPoolExecutor对象。实现类的构造函数以初始化这两个属性:
        public class MyPublisher implements Publisher<News> { 

          private ConcurrentLinkedDeque<ConsumerData> consumers; 
          private ThreadPoolExecutor executor; 

          public MyPublisher() { 
            consumers=new ConcurrentLinkedDeque<>(); 
            executor = (ThreadPoolExecutor)Executors.newFixedThreadPool
                          (Runtime.getRuntime().availableProcessors()); 
          }

  1. 现在,实现subscribe()方法。该方法将接收一个Subscriber对象作为参数,该对象希望以这种形式接收此发布者的项目。我们创建MySubscriptionConsumerData对象,将ConsumerData存储在ConcurrentLinkedDeque中,并调用订阅者的onSubscribe()方法将订阅对象发送给Subscriber对象:
        @Override 
        public void subscribe(Subscriber<? super News> subscriber) { 

          ConsumerData consumerData=new ConsumerData(); 
          consumerData.setConsumer((Consumer)subscriber); 

          MySubscription subscription=new MySubscription(); 
          consumerData.setSubscription(subscription); 

          subscriber.onSubscribe(subscription); 

          consumers.add(consumerData); 
        }

  1. 现在实现publish()方法。该方法接收一个News参数,并将其发送给满足之前解释的条件的订阅者。为此,我们为每个Subscriber创建一个PublisherTask方法,并将这些任务发送到执行器:
        public void publish(News news) { 
          consumers.forEach( consumerData -> { 
            try { 
              executor.execute(new PublisherTask(consumerData, news)); 
            } catch (Exception e) { 
              consumerData.getConsumer().onError(e); 
            } 
          }); 
        }

  1. 最后,实现示例中的Main类及其main()方法。我们创建一个发布者和两个订阅者,并将它们订阅到发布者:
        public class Main { 

          public static void main(String[] args) { 

            MyPublisher publisher=new MyPublisher(); 

            Subscriber<News> consumer1, consumer2; 
            consumer1=new Consumer("Consumer 1"); 
            consumer2=new Consumer("Consumer 2"); 

            publisher.subscribe(consumer1); 
            publisher.subscribe(consumer2);

  1. 然后,创建一个News对象,将其发送到发布者,主线程休眠一秒,创建另一个News对象,并将其再次发送到发布者:
        System.out.printf("Main: Start\n"); 

          News news=new News(); 
          news.setTitle("My first news"); 
          news.setContent("This is the content"); 
          news.setDate(new Date()); 

          publisher.publish(news); 

          try { 
            TimeUnit.SECONDS.sleep(1); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 

          news=new News(); 
          news.setTitle("My second news"); 
          news.setContent("This is the content of the second news"); 
          news.setDate(new Date()); 
          publisher.publish(news); 

          System.out.printf("Main: End\n"); 

        }

它是如何工作的...

在此示例中,我们使用 Java 9 API 提供的接口实现了发布者和订阅者之间的响应式流通信,并遵循响应式流规范中定义的预期行为。

我们有一个由MyPublisher类实现的发布者,以及由Consumer类实现的订阅者。发布者之间存在订阅关系,每个订阅者都由MySubscription对象实现。

通信周期从订阅者调用发布者的subscribe()方法开始。发布者必须在他们之间创建订阅,并使用onSubscribe()方法将订阅发送给订阅者。订阅者必须使用订阅的request()方法来表明它已准备好处理来自发布者的更多元素。当发布者发布一个项目时,它将通过他们之间的订阅将其发送给所有请求了发布者元素的订阅者。

我们添加了所有必要的元素,以确保以并发方式保证这种行为。

以下截图显示了此示例的执行输出:

图片

还有更多...

创建一个使用响应式流的程序的最简单方法是使用SubsmissionPublisher类。这个类实现了Publisher接口,并提供了使用它作为应用程序发布部分所需的方法。

参见

  • 在第六章中,关于使用响应式流的响应式编程的配方,并行和响应式流,第七部分

第九章:测试并发应用程序

在本章中,我们将涵盖以下主题:

  • 监控锁接口

  • 监控 Phaser 类

  • 监控 Executor 框架

  • 监控 fork/join 池

  • 监控流

  • 编写有效的日志消息

  • 使用 FindBugs 分析并发代码

  • 配置 Eclipse 以调试并发代码

  • 配置 NetBeans 以调试并发代码

  • 使用 MultithreadedTC 测试并发代码

  • 使用 JConsole 进行监控

简介

测试应用程序是一个关键任务。在你将应用程序准备好供最终用户使用之前,你必须证明其正确性。你使用测试过程来证明已达到正确性并且错误已被修复。测试是任何软件开发和质量保证过程中的常见任务。你可以找到大量关于测试过程和你可以应用于你的开发的不同的方法的文献。还有很多库,如 JUnit,以及应用程序,如 Apache JMeter,你可以使用它们以自动化的方式测试你的 Java 应用程序。在并发应用程序开发中,测试甚至更为关键。

并发应用程序有两个或更多线程共享数据结构并相互交互的事实,给测试阶段增加了更多的难度。你在测试并发应用程序时面临的最大问题是线程执行的不可确定性。你不能保证线程执行的顺序,因此很难重现错误。

监控锁接口

Lock 接口是 Java 并发 API 提供的基本机制之一,用于同步代码块。它允许你定义一个 临界区。临界区是一段访问共享资源的代码块,不能同时被多个线程执行。此机制由 Lock 接口和 ReentrantLock 类实现。

在本食谱中,你将了解你可以从 Lock 对象中获得哪些信息以及如何获取这些信息。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为 MyLock 的类,该类扩展了 ReentrantLock 类:
        public class MyLock extends ReentrantLock {

  1. 实现 getOwnerName() 方法。此方法返回控制锁的线程的名称(如果有),使用 Lock 类的受保护方法 getOwner()
        public String getOwnerName() {
          if (this.getOwner()==null) {
            return "None";
          }
          return this.getOwner().getName();
        }

  1. 实现 getThreads() 方法。此方法返回一个列表,其中包含在锁中排队的线程,使用 Lock 类的受保护方法 getQueuedThreads()
        public Collection<Thread> getThreads() { 
          return this.getQueuedThreads(); 
        }

  1. 创建一个名为 Task 的类,实现 Runnable 接口:
        public class Task implements Runnable {

  1. 声明一个名为 lock 的私有 Lock 属性:
        private final Lock lock;

  1. 实现类的构造函数以初始化其属性:
        public Task (Lock lock) { 
          this.lock=lock; 
        }

  1. 实现 run() 方法。创建一个包含五个步骤的循环:
        @Override 
        public void run() { 
          for (int i=0; i<5; i++) {

  1. 使用lock()方法获取锁并打印一条消息:
        lock.lock(); 
        System.out.printf("%s: Get the Lock.\n",
                          Thread.currentThread().getName());

  1. 将线程休眠 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(); 
              } 
            } 
          } 
        }

  1. 通过创建一个名为Main的类并包含一个main()方法来创建示例的主类:
        public class Main { 
          public static void main(String[] args) throws Exception {

  1. 创建一个名为lockMyLock对象:
        MyLock lock=new MyLock();

  1. 创建一个包含五个Thread对象的数组:
        Thread threads[]=new Thread[5];

  1. 创建并启动五个线程以执行五个Task对象:
        for (int i=0; i<5; i++) { 
          Task task=new Task(lock); 
          threads[i]=new Thread(task); 
          threads[i].start(); 
        }

  1. 创建一个包含 15 个步骤的循环:
        for (int i=0; i<15; i++) {

  1. 在控制台写入锁的所有者名称:
        System.out.printf("Main: Logging the Lock\n"); 
        System.out.printf("************************\n"); 
        System.out.printf("Lock: Owner : %s\n",lock.getOwnerName());

  1. 显示等待获取锁的线程的数量和名称:
        System.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"); 
        }

  1. 显示Lock对象的公平性和状态信息:
        System.out.printf("Lock: Fairness: %s\n",lock.isFair()); 
        System.out.printf("Lock: Locked: %s\n",lock.isLocked()); 
        System.out.printf("************************\n");

  1. 将线程休眠 1 秒并关闭循环和类:
              TimeUnit.SECONDS.sleep(1); 
            } 
          } 
        }

它是如何工作的...

在这个配方中,你实现了扩展ReentrantLock类的MyLock类,以返回其他情况下不可用的信息——它是ReentrantLock类的受保护数据。MyLock类实现的方法如下:

  • getOwnerName(): 只有一个线程可以执行由Lock对象保护的临界区。锁存储正在执行临界区的线程。该线程由ReentrantLock类的受保护getOwner()方法返回。

  • getThreads(): 当一个线程正在执行临界区时,其他尝试进入该临界区的线程在继续执行该临界区之前将被休眠。ReentrantLock类的受保护方法getQueuedThreads()返回正在等待执行临界区的线程列表。

我们还使用了ReentrantLock类中实现的其他方法:

  • hasQueuedThreads(): 此方法返回一个Boolean值,指示是否有线程正在等待获取调用ReentrantLock

  • getQueueLength(): 此方法返回等待获取调用ReentrantLock的线程数量

  • isLocked(): 此方法返回一个Boolean值,指示调用ReentrantLock是否由一个线程拥有

  • isFair(): 此方法返回一个Boolean值,指示调用ReentrantLock是否已激活公平模式

更多...

ReentrantLock类中还有其他可以用来获取Lock对象信息的方法:

  • getHoldCount(): 此方法返回当前线程获取锁的次数

  • isHeldByCurrentThread(): 此方法返回一个Boolean值,指示锁是否由当前线程拥有

参见

  • 在第二章,基本线程同步中的使用锁同步代码块配方

  • 在第八章,自定义并发类中的实现自定义锁类配方

监控 Phaser 类

Java 并发 API 提供的最复杂和强大的功能之一是能够使用Phaser类执行并发分阶段任务。当我们将一些并发任务分为步骤时,此机制非常有用。Phaser类提供了在每个步骤末尾同步线程的机制,这样就没有线程在所有线程完成第一个步骤之前开始第二个步骤。

在此菜谱中,您将了解有关Phaser类状态的信息,以及如何获取这些信息。

准备工作

此菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Task的类,实现Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为time的私有int属性:
        private final int time;

  1. 声明一个名为phaser的私有Phaser属性:
        private final Phaser phaser;

  1. 实现类的构造函数以初始化其属性:
        public Task(int time, Phaser phaser) { 
          this.time=time; 
          this.phaser=phaser; 
        }

  1. 实现run()方法。首先,使用arrive()方法指示phaser属性任务开始执行:
        @Override 
        public void run() { 

          phaser.arrive();

  1. 在控制台写入一条消息,指示第一阶段开始。让线程休眠由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();

  1. 在第二阶段和第三阶段重复此行为。在第三阶段结束时,使用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();

  1. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主要类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 创建一个名为phaser的新Phaser对象,包含三个参与者:
        Phaser phaser=new Phaser(3);

  1. 创建并启动三个线程以执行三个任务对象:
        for (int i=0; i<3; i++) { 
          Task task=new Task(i+1, phaser); 
          Thread thread=new Thread(task); 
          thread.start(); 
        }

  1. 创建一个包含 10 个步骤的循环,以写入关于phaser对象的信息:
        for (int i=0; i<10; i++) {

  1. 写入有关已注册的参与者、phaser的相位、到达的参与者和未到达的参与者的信息:
        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. 让线程休眠 1 秒,并关闭循环和类:
              TimeUnit.SECONDS.sleep(1); 
            } 
          } 
        }

它是如何工作的...

在此菜谱中,我们在Task类中实现了一个分阶段任务。这个分阶段任务有三个阶段,并使用Phaser接口与其他Task对象同步。主类启动三个任务,当这些任务执行各自的阶段时,它将打印有关phaser对象状态的信息到控制台。我们使用了以下方法来获取phaser对象的状态:

  • getPhase(): 此方法返回phaser对象的实际相位

  • getRegisteredParties(): 此方法返回使用phaser对象作为同步机制的任务数量

  • getArrivedParties(): 此方法返回到达实际阶段末尾的任务数量

  • getUnarrivedParties():此方法返回尚未到达实际阶段结束的任务数量

以下截图显示了程序的部分输出:

参见

  • 在第三章的运行并发阶段任务食谱中,线程同步工具

监控 Executor 框架

Executor框架提供了一个机制,将任务的实现与线程的创建和管理分离,以便执行任务。如果你使用执行器,你只需要实现Runnable对象并将它们发送到执行器。管理线程是执行器的责任。当你向执行器发送任务时,它会尝试使用池化的线程来执行任务,以避免创建新的线程。这种机制由Executor接口及其实现类ThreadPoolExecutor类提供。

在本食谱中,你将学习可以获取有关ThreadPoolExecutor执行器状态的哪些信息以及如何获取这些信息。

准备工作

本例的食谱是用 Eclipse IDE 实现的。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Task的类,该类实现了Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为milliseconds的私有long属性:
        private final long milliseconds;

  1. 实现类的构造函数以初始化其属性:
        public Task (long milliseconds) { 
          this.milliseconds=milliseconds; 
        }

  1. 实现接收milliseconds属性指定的时间数目的run()方法。将线程休眠指定的时间:
        @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()); 

        }

  1. 通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 使用Executors类的newCachedThreadPool()方法创建一个新的Executor对象:
        ThreadPoolExecutor executor = (ThreadPoolExecutor)
                                Executors.newCachedThreadPool();

  1. 创建并提交 10 个Task对象到执行器。用随机数初始化这些对象:
        Random random=new Random(); 
        for (int i=0; i<10; i++) { 
          Task task=new Task(random.nextInt(10000)); 
          executor.submit(task); 
        }

  1. 创建一个包含五个步骤的循环。在每一步中,通过调用showLog()方法并将线程休眠一秒钟来记录执行者的信息:
        for (int i=0; i<5; i++){ 
          showLog(executor); 
          TimeUnit.SECONDS.sleep(1); 
        }

  1. 使用shutdown()方法关闭执行器:
        executor.shutdown();

  1. 创建另一个包含五个步骤的循环。在每一步中,通过调用showLog()方法并将线程休眠一秒钟来记录执行者的信息:
        for (int i=0; i<5; i++){ 
          showLog(executor); 
          TimeUnit.SECONDS.sleep(1); 
        }

  1. 使用awaitTermination()方法等待执行器的最终化:
        executor.awaitTermination(1, TimeUnit.DAYS);

  1. 显示一条消息,表明程序结束:
          System.out.printf("Main: End of the program.\n"); 
        }

  1. 实现接收Executor作为参数的showLog()方法。记录池的大小、任务的数量和执行器的状态:
        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值。

参见

  • 第四章中“创建线程执行器和控制其拒绝的任务”菜谱的Creating a thread executor and controlling its rejected tasksThread Executors

  • 第八章中“自定义 ThreadPoolExecutor 类”和“实现基于优先级的 Executor 类”菜谱的Customizing the ThreadPoolExecutor classImplementing a priority-based Executor classCustomizing Concurrency Classes

监控 fork/join 池

Executor 框架提供了一个机制,允许你将任务实现与创建和管理执行任务的线程分离。Java 9 为特定类型的问题扩展了 Executor 框架,这将提高其他解决方案的性能(直接使用Thread对象或 Executor 框架)。它是 fork/join 框架。

该框架旨在解决可以使用fork()join()操作分解成更小任务的问题。实现此行为的主要类是ForkJoinPool

在这个菜谱中,你将了解关于ForkJoinPool类可以获取哪些信息以及如何获取这些信息。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Task的类,该类继承自RecursiveAction类:
        public class Task extends RecursiveAction{

  1. 声明一个名为 array 的私有 int 数组属性,用于存储您想要增加的元素数组:
        private final int array[];

  1. 声明两个名为 startend 的私有 int 属性,用于存储此任务需要处理的元素块的开始和结束位置:
        private final int start; 
        private final int end;

  1. 实现类的构造函数以初始化其属性:
        public Task (int array[], int start, int end) { 
          this.array=array; 
          this.start=start; 
          this.end=end; 
        }

  1. 实现具有任务主要逻辑的 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();

  1. 如果任务需要处理 100 个或更少的元素,通过在每个操作后将线程休眠 5 毫秒来增加元素:
          } else { 
              for (int i=start; i<end; i++) { 
                array[i]++; 

                try { 
                  Thread.sleep(5); 
                } catch (InterruptedException e) { 
                  e.printStackTrace(); 
                } 
              } 
            } 
          } 
        }

  1. 通过创建一个名为 Main 的类并具有 main() 方法来实现示例的主要类:
        public class Main { 

          public static void main(String[] args) throws Exception {

  1. 创建一个名为 poolForkJoinPool 对象:
        ForkJoinPool pool=new ForkJoinPool();

  1. 创建一个包含 10,000 个整数的数组,命名为 array
        int array[]=new int[10000];

  1. 创建一个新的 Task 对象来处理整个数组:
        Task task1=new Task(array,0,array.length);

  1. 使用 execute() 方法将任务发送到池中执行:
        pool.execute(task1);

  1. 如果任务没有完成其执行,调用 showLog() 方法来写入有关 ForkJoinPool 类的状态的信息,并将线程休眠一秒:
        while (!task1.isDone()) { 
          showLog(pool); 
          TimeUnit.SECONDS.sleep(1); 
        }

  1. 使用 shutdown() 方法关闭池:
        pool.shutdown();

  1. 使用 awaitTermination() 方法等待池的最终化:
        pool.awaitTermination(1, TimeUnit.DAYS);

  1. 调用 showLog() 方法来写入有关 ForkJoinPool 类的状态的信息,并在控制台写入一条消息,指示程序的结束:
        showLog(pool); 
        System.out.printf("Main: End of the program.\n");

  1. 实现 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(): 此方法返回 ForkJoinPool 类内部池的工作线程数量的 int

  • getParallelism(): 此方法返回为池建立的期望的并行级别

  • getActiveThreadCount(): 此方法返回当前正在执行任务的线程数量

  • getRunningThreadCount(): 此方法返回未在任何同步机制中阻塞的工作线程数量

  • getQueuedSubmissionCount(): 此方法返回已提交到池中但尚未开始执行的任务数量

  • getQueuedTaskCount(): 此方法返回已提交到池中并已开始执行的任务数量

  • hasQueuedSubmissions():此方法返回一个 Boolean 值,指示池是否已排队等待执行的任务

  • getStealCount():此方法返回一个 long 值,指定工作线程从其他线程中窃取任务次数

  • isTerminated():此方法返回一个 Boolean 值,指示 fork/join 池是否已完成其执行

相关内容

  • 在 第五章 的 创建 fork/join 池 菜单中,Fork/Join 框架

  • 在 第八章 的 实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程自定义 fork/join 框架中运行的任务 菜单中,自定义并发类

监控流

Java 中的流是一系列可以处理的元素(映射、过滤、转换、归约和收集),这些元素可以并行或顺序地在声明性操作的管道中使用 lambda 表达式进行处理。它是在 Java 8 中引入的,以改变人们以函数式方式处理大量数据的方式,用 lambda 表达式代替传统的命令式方式。

Stream 接口不像其他并发类那样提供很多方法来监控其状态。只有 peek() 方法允许你写入正在处理的元素的相关日志信息。在本菜谱中,你将学习如何使用此方法来写入有关流的信息。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Main 的类,并包含一个 main() 方法。声明两个私有变量,即一个名为 counterAtomicInteger 变量和一个名为 randomRandom 对象:
        public class Main { 
          public static void main(String[] args) { 

            AtomicLong counter = new AtomicLong(0); 
            Random random=new Random();

  1. 创建一个包含 1,000 个随机 double 数字的流。创建的流是一个顺序流。你必须使用 parallel() 方法将其转换为并行流,并使用 peek() 方法增加 counter 变量的值并在控制台写入一条消息。之后,使用 count() 方法计算数组中的元素数量并将该数字存储在一个整型变量中。将存储在 counter 变量中的值和 count() 方法返回的值写入控制台:
        long streamCounter = random.doubles(1000).parallel()
                             .peek( number -> { 
          long actual=counter.incrementAndGet(); 
          System.out.printf("%d - %f\n", actual, number); 
        }).count(); 

        System.out.printf("Counter: %d\n", counter.get()); 
        System.out.printf("Stream Counter: %d\n", streamCounter);

  1. 现在,将 counter 变量的值设置为 0。创建另一个包含 1,000 个随机 double 数字的流。然后,使用 parallel() 方法将其转换为并行流,并使用 peek() 方法增加 counter 变量的值并在控制台写入一条消息。最后,使用 forEach() 方法将所有数字和计数器的值写入控制台:
            counter.set(0); 
            random.doubles(1000).parallel().peek(number -> { 
              long actual=counter.incrementAndGet(); 
              System.out.printf("Peek: %d - %f\n", actual,number); 
            }).forEach( number -> { 
              System.out.printf("For Each: %f\n", number); 
            }); 

            System.out.printf("Counter: %d\n", counter.get()); 
          } 
        }

它是如何工作的...

在这个例子中,我们使用了peek()方法在两种不同的情况下来计算通过流这一步骤的元素数量,并在控制台中写入一条消息。

如第六章中所述,“并行和响应式流”,Stream有一个源,零个或多个中间操作,以及一个最终操作。在第一种情况下,我们的最终操作是count()方法。这个方法不需要处理元素来计算返回值,所以peek()方法永远不会被执行。你不会在控制台中看到任何peek方法的日志消息,计数器的值将是 0。

第二种情况不同。最后的操作是forEach()方法,在这种情况下,流中的所有元素都将被处理。在控制台中,你会看到peek()forEach()方法的消息。counter变量的最终值将是 1,000。

peek()方法是一个流的中间操作。像所有中间操作一样,它们是延迟执行的,并且只处理必要的元素。这就是为什么它永远不会在第一种情况下执行的原因。

参见

  • 第六章中的“从不同源创建流”、“减少流元素”和“收集流元素”食谱

编写有效的日志消息

日志系统是一种允许你将信息写入一个或多个目的地的机制。一个Logger具有以下组件:

  • 一个或多个处理器:处理器将确定日志消息的目的地和格式。你可以将日志消息写入控制台、文件或数据库。

  • 一个名称:通常,在类中使用的 Logger 名称基于类名及其包名。

  • 一个级别:日志消息有不同的级别,表示它们的重要性。Logger 也有一个级别来决定它将要写入哪些消息。它只写入与其级别相同或更重要的消息。

你应该使用日志系统,主要有以下两个原因:

  • 当捕获到异常时,尽可能多地编写信息。这将帮助你定位错误并解决它。

  • 编写有关程序正在执行哪些类和方法的详细信息。

在这个食谱中,你将学习如何使用java.util.logging包提供的类将日志系统添加到你的并发应用程序中。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或不同的 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按以下步骤实现示例:

  1. 创建一个名为 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(); 
          }

  1. 创建一个名为 MyLoggerFactory 的类:
        public class MyLoggerFactory {

  1. 声明一个名为 handler 的私有静态 Handler 属性:
        private static Handler handler;

  1. 实现一个名为 getLogger() 的公共静态方法,用于创建你将要用于写入日志消息的 Logger 对象。它接收一个名为 nameString 参数。我们使用 synchronized 关键字同步此方法:
        public synchronized static Logger getLogger(String name){

  1. 使用 Logger 类的 getLogger() 方法获取与参数名称关联的 java.util.logging.Logger
        Logger logger=Logger.getLogger(name);

  1. 使用 setLevel() 方法设置日志级别,以便写入所有日志消息:
        logger.setLevel(Level.ALL);

  1. 如果处理程序属性具有空值,创建一个新的 FileHandler 对象以将日志消息写入 recipe8.log 文件。将 MyFormatter 对象分配给此处理程序;使用 setFormatter() 对象将其分配为格式化程序:
        try { 
          if (handler==null) { 
            handler=new FileHandler("recipe6.log"); 
            Formatter format=new MyFormatter(); 
            handler.setFormatter(format); 
          }

  1. 如果 Logger 对象没有与之关联的处理程序,请使用 addHandler() 方法分配处理程序:
            if (logger.getHandlers().length==0) { 
              logger.addHandler(handler); 
            } 
          } catch (SecurityException e | IOException e) { 
          e.printStackTrace(); 
        }

  1. 返回创建的 Logger 对象:
          return logger; 
        }

  1. 创建一个名为 Task 的类,该类实现了 Runnable 接口。它将用于测试你的 Logger 对象:
        public class Task implements Runnable {

  1. 实现 run() 方法:
        @Override 
        public void run() {

  1. 首先,声明一个名为 loggerLogger 对象。使用 MyLogger 类的 getLogger() 方法初始化它,通过传递此类的名称作为参数:
        Logger logger= MyLogger.getLogger(this.getClass().getName());

  1. 使用 entering() 方法写入一个日志消息,表明方法执行的开始:
        logger.entering(Thread.currentThread().getName(), "run()");

  1. 让线程休眠两秒钟:
        try { 
          TimeUnit.SECONDS.sleep(2); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        }

  1. 使用 exiting() 方法写入一个日志消息,表明方法执行的结束:
          logger.exiting(Thread.currentThread().getName(), "run()",
                         Thread.currentThread()); 
        }

  1. 通过创建一个名为 Main 的类并包含 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 声明一个名为 loggerLogger 对象。使用 MyLogger 类的 getLogger() 方法通过传递 Core 字符串作为参数来初始化它:
        Logger logger=MyLogger.getLogger(Main.class.getName());

  1. 使用 entering() 方法写入一个日志消息,表明主程序执行的开始:
        logger.entering(Main.class.getName(), "main()",args);

  1. 创建一个用于存储五个线程的 Thread 数组:
        Thread threads[]=new Thread[5];

  1. 创建五个 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(); 
        }

  1. 写入一个日志消息以表明你已创建了线程:
        logger.log(Level.INFO,"Ten Threads created."+
                   "Waiting for its finalization");

  1. 使用 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); 
          } 
        }

  1. 使用 exiting() 方法写入一个日志消息以表明主程序执行的结束:
          logger.exiting(Main.class.getName(), "main()"); 
        }

如何工作...

在这个菜谱中,您使用了 Java 日志 API 提供的Logger类来在并发应用程序中写入日志消息。首先,您实现了MyFormatter类来为日志消息分配格式。此类扩展了声明抽象format()方法的Formatter类。此方法接收一个包含日志消息所有信息的LogRecord对象,并返回一个格式化的日志消息。在您的类中,您使用了以下LogRecord类的方法来获取有关日志消息的信息:

  • getLevel(): 返回消息的级别

  • getMillis(): 返回消息发送到Logger对象时的日期

  • getSourceClassName(): 返回向Logger发送消息的类的名称

  • getSourceMessageName(): 返回向Logger发送消息的方法的名称

  • getMessage(): 返回日志消息

MyLogger类实现了静态方法getLogger()。此方法创建一个Logger对象,并将一个Handler对象分配给写入应用程序的日志消息到recipe6.log文件,使用MyFormatter格式化器。您通过Logger类的静态方法getLogger()创建Logger对象。此方法根据传递的参数名称返回不同的对象。您只创建了一个Handler对象,因此所有Logger对象都将它们的日志消息写入同一个文件。您还配置了记录器,无论其级别如何,都写入所有日志消息。

最后,您实现了一个Task对象和一个主程序,该程序在日志文件中写入不同的日志消息。您使用了以下方法:

  • entering(): 用于写入一个带有FINER级别的消息,表示一个方法已开始其执行

  • exiting(): 用于写入一个带有FINER级别的消息,表示一个方法已结束其执行

  • log(): 用于写入指定级别的消息

还有更多...

当您与日志系统一起工作时,您必须考虑两个重要点:

  • 写入必要的信息:如果您写入的信息太少,记录器将没有用,因为它无法完成其目的。如果您写入大量信息,您将生成大量难以管理的日志文件;这将使获取必要信息变得困难。

  • 使用适当的消息级别:如果您写入高级信息消息或低级错误消息,您将使查看日志文件的用户感到困惑。这将使在错误情况下了解发生了什么变得更加困难;或者,您将拥有过多的信息,这使得了解错误的主要原因变得困难。

有其他库提供了比java.util.logging包更完整的日志系统,例如Log4jslf4j库。但是java.util.logging包是 Java API 的一部分,并且所有方法都是线程安全的;因此,我们可以在并发应用程序中使用它而不会出现问题。

参见

  • 第七章中的使用非阻塞线程安全的 deque使用阻塞线程安全的 deque使用按优先级排序的阻塞线程安全的队列使用延迟元素的线程安全列表使用线程安全的可导航映射食谱,并发集合

使用 FindBugs 分析并发代码

静态代码分析工具是一组在查找潜在错误时分析应用程序源代码的工具。这些工具,如 Checkstyle、PMD 或 FindBugs,有一套预定义的编码规范规则,并解析源代码以查找违反这些规则的实例。目标是尽早找到错误或可能导致性能不佳的地方,在它们在生产环境中执行之前。编程语言通常提供此类工具,Java 也不例外。帮助分析 Java 代码的工具之一是 FindBugs。它是一个开源工具,包含一系列用于分析 Java 并发代码的规则。

在这个食谱中,您将学习如何使用这个工具来分析您的 Java 并发应用程序。

准备工作

在开始这个食谱之前,从项目的网页上下载 FindBugs(findbugs.sourceforge.net/)。您可以下载一个独立的应用程序或 Eclipse 插件。在这个食谱中,我使用了独立版本。

在撰写本文时,FindBugs 的实际版本(3.0.1)不包括对 Java 9 的支持。您可以从github.com/findbugsproject/findbugs/releases/tag/3.1.0_preview1下载支持 Java 9 的 3.1.0 版本预览。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为Task的类,该类扩展了Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为lock的私有ReentrantLock属性:
        private ReentrantLock lock;

  1. 实现类的构造函数:
        public Task(ReentrantLock lock) { 
          this.lock=lock; 
        }

  1. 实现run()方法。获取锁的控制权,让线程休眠 2 秒,然后释放锁:
        @Override 
        public void run() { 
          lock.lock(); 
          try { 
            TimeUnit.SECONDS.sleep(1); 
            lock.unlock(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 通过创建一个名为Main的类并包含一个main()方法来创建示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 声明并创建一个名为lockReentrantLock对象:
        ReentrantLock lock=new ReentrantLock();

  1. 创建 10 个Task对象和 10 个线程来执行任务。通过调用run()方法来启动线程:
          for (int i=0; i<10; i++) { 
            Task task=new Task(lock); 
            Thread thread=new Thread(task); 
            thread.run(); 
          } 
        }

  1. 将项目导出为.jar文件。命名为recipe7.jar。使用您 IDE 的菜单选项或javac.jar命令来编译和压缩您的应用程序。

  2. 通过在 Windows 上运行findbugs.bat命令或在 Linux 上运行findbugs.sh命令来启动 FindBugs 独立应用程序。

  3. 通过在菜单栏中的文件菜单下点击新建项目选项来创建新项目:

  1. FindBugs 应用程序显示一个窗口来配置项目。在项目名称字段中,输入 Recipe07。在分析类路径字段(jar、ear、war、zip 或目录)中,添加包含项目的 .jar 文件。在源代码目录字段(可选;浏览找到的错误时使用的类),添加示例源代码的目录。参考以下截图:

  1. 点击分析按钮以创建新项目并分析其代码。

  2. FindBugs 应用程序显示了代码的分析结果。在这种情况下,它找到了两个错误。

  3. 点击其中一个错误,你将在右侧面板中看到错误的源代码,并在屏幕底部的面板中看到错误的描述。

它是如何工作的...

以下截图显示了 FindBugs 分析的结果:

分析检测到应用程序中的以下两个潜在错误:

  • 其中一个错误检测到 Task 类的 run() 方法中。如果抛出 InterruptedExeption 异常,任务不会释放锁,因为它不会执行 unlock() 方法。这可能会在应用程序中引起死锁情况。

  • 另一个错误检测到在 Main 类的 main() 方法中,因为你直接调用了线程的 run() 方法,而不是调用 start() 方法来开始线程的执行。

如果你双击两个错误之一,你将看到有关它的详细信息。由于你在项目配置中包含了源代码引用,你还将看到检测到错误的源代码。以下截图显示了此示例:

更多...

注意,FindBugs 只能检测到一些问题情况(与并发代码相关或不相关)。例如,如果你在 Task 类的 run() 方法中删除 unlock() 调用并重复分析,FindBugs 不会警告你

你将在任务中获得锁,但你永远无法释放它。

将静态代码分析工具作为提高代码质量的一种辅助手段使用,但不要期望它能检测到所有错误。

参见

  • 本章中关于 配置 NetBeans 以调试并发代码 的配方

配置 Eclipse 以调试并发代码

现在,几乎每个程序员,无论使用哪种编程语言,都会使用 IDE 来创建他们的应用程序。它们在同一个应用程序中集成了许多有趣的功能,例如:

  • 项目管理

  • 自动代码生成

  • 自动文档生成

  • 与版本控制系统的集成

  • 用于测试应用程序的调试器

  • 用于创建项目和应用程序元素的不同的向导

IDE 最有用的功能之一是调试器。使用它,你可以逐步执行你的应用程序并分析程序中所有对象和变量的值。

如果你使用 Java,Eclipse 是最受欢迎的 IDE 之一。它集成了调试器,允许你测试你的应用程序。默认情况下,当你调试一个并发应用程序并且调试器发现一个断点时,它只会停止带有断点的线程,同时允许其他线程继续执行。在这个菜谱中,你将学习如何更改此配置以帮助你测试并发应用程序。

准备工作

你必须已经安装了 Eclipse IDE。打开它,并选择一个实现了并发应用程序的项目,例如,书中实现的一个菜谱。

如何操作...

按照以下步骤实现示例:

  1. 导航到窗口 | 首选项。

  2. 在左侧菜单中展开 Java 选项。

  3. 然后,选择调试选项。以下截图说明了窗口:

图片

  1. 将默认挂起策略的值从“挂起线程”更改为“挂起虚拟机”(在截图中被标记为红色)。

  2. 点击“确定”按钮以确认更改。

工作原理...

如本菜谱介绍中所述,默认情况下,当你使用 Eclipse 调试并发 Java 应用程序并且调试过程发现断点时,它只会挂起第一个遇到断点的线程,但允许其他线程继续执行。以下截图展示了这种情况的示例:

图片

你可以看到,只有 worker-21 被挂起(在截图中被标记为红色),而其他线程仍在运行。然而,在调试并发应用程序时,如果你将默认挂起策略更改为“挂起虚拟机”,所有线程将挂起它们的执行,调试过程将遇到断点。以下截图展示了这种情况的示例:

图片

通过更改,你可以看到所有线程都被挂起。你可以继续调试任何你想要的线程。选择最适合你需求的挂起策略。

配置 NetBeans 以调试并发代码

软件是开发能够正常工作、符合公司质量标准并且可以轻松修改(在尽可能低的时间和成本下)的应用程序所必需的。为了实现这一目标,使用能够集成多个工具(编译器和调试器)以在一个共同界面下简化应用程序开发的 IDE 是至关重要的。

如果你使用 Java,NetBeans 也是最受欢迎的 IDE 之一。它集成了调试器,允许你测试你的应用程序。

在这个菜谱中,你将学习如何更改 NetBeans 调试器的配置以帮助你测试并发应用程序。

准备工作

你应该已经安装了 NetBeans IDE。打开它,并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Task1 的类,并指定它实现 Runnable 接口:
        public class Task1 implements Runnable {

  1. 声明两个名为 lock1lock2 的私有 Lock 属性:
        private Lock lock1, lock2;

  1. 实现类的构造函数以初始化其属性:
        public Task1 (Lock lock1, Lock lock2) { 
          this.lock1=lock1; 
          this.lock2=lock2; 
        }

  1. 实现 run() 方法。首先,使用 lock() 方法获取 lock1 对象的控制权,并在控制台写入一条消息,表明你已获取它:
        @Override 
        public void run() { 
          lock1.lock(); 
          System.out.printf("Task 1: Lock 1 locked\n");

  1. 然后,使用 lock() 方法获取 lock2 的控制权,并在控制台写入一条消息,表明你已获取它:
        lock2.lock(); 
        System.out.printf("Task 1: Lock 2 locked\n");

  1. 最后,释放两个锁对象——首先释放 lock2 对象,然后释放 lock1 对象:
          lock2.unlock(); 
          lock1.unlock(); 
        }

  1. 创建一个名为 Task2 的类,并指定它实现 Runnable 接口:
        public class Task2 implements Runnable{

  1. 声明两个名为 lock1lock2 的私有 Lock 属性:
        private Lock lock1, lock2;

  1. 实现类的构造函数以初始化其属性:
        public Task2(Lock lock1, Lock lock2) { 
          this.lock1=lock1; 
          this.lock2=lock2; 
        }

  1. 实现 run() 方法。首先,使用 lock() 方法获取 lock2 对象的控制权,并在控制台写入一条消息,表明你已获取它:
        @Override 
        public void run() { 
          lock2.lock(); 
          System.out.printf("Task 2: Lock 2 locked\n");

  1. 然后,使用 lock() 方法获取 lock1 的控制权,并在控制台写入一条消息,表明你已获取它:
        lock1.lock(); 
        System.out.printf("Task 2: Lock 1 locked\n");

  1. 最后,释放两个锁对象——首先释放 lock1,然后释放 lock2
          lock1.unlock(); 
          lock2.unlock(); 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main {

  1. 创建两个名为 lock1lock2 的锁对象:
        Lock lock1, lock2; 
        lock1=new ReentrantLock(); 
        lock2=new ReentrantLock();

  1. 创建一个名为 task1Task1 对象:
        Task1 task1=new Task1(lock1, lock2);

  1. 创建一个名为 task2Task2 对象:
        Task2 task2=new Task2(lock1, lock2);

  1. 使用两个线程执行两个任务:
        Thread thread1=new Thread(task1); 
        Thread thread2=new Thread(task2); 

        thread1.start(); 
        thread2.start();

  1. 当两个任务完成执行时,每 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(); 
          } 
        }

  1. Task1 类的 run() 方法的 printf() 方法第一次调用处添加一个断点。

  2. 调试程序。你将在主 NetBeans 窗口的左上角看到调试窗口。下一张截图展示了包含执行 Task1 对象的线程的窗口。该线程正在断点处等待。应用程序的其他线程正在运行:

图片

  1. 暂停主线程的执行。选择线程,右键单击它,并选择“挂起”选项。以下截图显示了调试窗口的新外观。请参考以下截图:

图片

  1. 恢复两个暂停的线程。选择每个线程,右键单击它们,并选择“恢复”选项。

它是如何工作的...

在使用 NetBeans 调试并发应用程序时,当调试器遇到断点时,它会挂起遇到断点的线程,并在主窗口的左上角显示包含当前正在运行的线程的调试窗口。

你可以使用窗口暂停或恢复当前正在运行的线程,使用“暂停”或“恢复”选项。你还可以使用“变量”选项卡查看线程的变量或属性值。

NetBeans 还包括一个死锁检测器。当你从调试菜单中选择检查死锁选项时,NetBeans 会分析你正在调试的应用程序,以确定是否存在死锁情况。本例展示了一个明显的死锁。第一个线程首先获取 lock1,然后是 lock2。第二个线程以相反的顺序获取锁。插入的断点引发了死锁,但如果你使用 NetBeans 死锁检测器,你将找不到任何东西。因此,应谨慎使用此选项。通过 synchronized 关键字更改两个任务中使用的锁,并再次调试程序。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/archive/p/multithreadedtc/ 下载 MultithreadedTC 库和 JUnit 库,版本 4.10,从 junit.org/junit4/。将 junit-4.10.jarMultithreadedTC-1.01.jar 文件添加到项目的库中。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 ProducerConsumerTest 的类,该类扩展了 MultithreadedTestCase 类:
        public class ProducerConsumerTest extends MultithreadedTestCase {

  1. 声明一个名为 queue 的私有 LinkedTransferQueue 属性参数,该参数由 String 类指定:
        private LinkedTransferQueue<String> queue;

  1. 实现initialize()方法。此方法不会接收任何参数,也不会返回任何值。它将调用其父类的initialize()方法,然后初始化队列属性:
        @Override 
        public void initialize() { 
          super.initialize(); 
          queue=new LinkedTransferQueue<String>(); 
          System.out.printf("Test: The test has been initialized\n"); 
        }

  1. 实现thread1()方法。它将实现第一个消费者的逻辑。调用队列的take()方法,然后将返回的值写入控制台:
        public void thread1() throws InterruptedException { 
          String ret=queue.take(); 
          System.out.printf("Thread 1: %s\n",ret); 
        }

  1. 实现thread2()方法。它将实现第二个消费者的逻辑。首先等待第一个线程在take()方法中休眠。为了使线程休眠,使用waitForTick()方法。然后,调用队列的take()方法并将返回的值写入控制台:
        public void thread2() throws InterruptedException { 
          waitForTick(1); 
          String ret=queue.take(); 
          System.out.printf("Thread 2: %s\n",ret); 
        }

  1. 实现thread3()方法。它将实现生产者的逻辑。

    首先,等待两个消费者在take()方法中被阻塞;使用waitForTick()方法两次来阻塞此方法。然后,调用队列的put()方法,在队列中插入两个字符串:

         public void thread3() { 
          waitForTick(1); 
          waitForTick(2); 
          queue.put("Event 1"); 
          queue.put("Event 2"); 
          System.out.printf("Thread 3: Inserted two elements\n"); 
        }

  1. 最后,实现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"); 
        }

  1. 接下来,通过创建一个名为Main的类并包含一个main()方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) throws Throwable {

  1. 创建一个名为testProducerConsumerTest对象:
        ProducerConsumerTest test=new ProducerConsumerTest();

  1. 使用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()方法。此方法接收一个整数参数,并将正在执行该方法的线程休眠,直到测试中运行的线程全部阻塞。当它们阻塞时,MultithreadedTC库通过调用waitForTick()方法恢复被阻塞的线程。

你传递给waitForTick()方法的整数用于控制执行顺序。MultithreadedTC库的节拍器有一个内部计数器。当所有线程都阻塞时,库将此计数器递增到waitForTick()调用中指定的下一个数字。

内部,当MultithreadedTC库需要执行一个测试时,首先执行initialize()方法。然后为以thread关键字开头的方法(在你的例子中,是thread1()thread2()thread3())创建一个线程。当所有线程完成执行后,它执行finish()方法。为了执行测试,你使用了TestFramework类的runOnce()方法。

更多内容...

如果MultithreadedTC库检测到测试的所有线程都阻塞,除了waitForTick()方法外,测试将被声明为死锁状态,并抛出java.lang.IllegalStateException异常。

参见

  • 本章中关于使用 FindBugs 分析并发代码的菜谱

使用 JConsole 进行监控

JConsole是一个遵循 JMX 规范的监控工具,允许你获取有关应用程序执行的信息,例如线程数、内存使用或类加载。它包含在 JDK 中,可以用来监控本地或远程应用程序。在这个菜谱中,你将学习如何使用这个工具来监控一个简单的并发应用程序。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等不同的 IDE,打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为Task的类并指定Runnable接口。实现run()方法,在 100 秒内在控制台写入消息:
        public class Task implements Runnable { 

          @Override 
          public void run() { 

            Date start, end; 
            start = new Date(); 
            do { 
              System.out.printf("%s: tick\n",
                                Thread.currentThread().getName()); 
              end = new Date(); 
            } while (end.getTime() - start.getTime() < 100000); 
          } 
        }

  1. 实现带有main()方法的Main类。创建 10 个Task对象以创建 10 个线程。启动它们并使用join()方法等待它们的最终化:
        public class Main { 
          public static void main(String[] args) { 

            Thread[] threads = new Thread[10]; 

            for (int i=0; i<10; i++) { 
              Task task=new Task(); 
              threads[i]=new Thread(task); 
              threads[i].start(); 
            } 

            for (int i=0; i<10; i++) { 
              try { 
                threads[i].join(); 
              } catch (InterruptedException e) { 
                e.printStackTrace(); 
              } 
            } 
          } 
        }

  1. 打开一个控制台窗口并执行JConsole应用程序。它包含在 JDK-9 安装的 bin 目录中:

它是如何工作的...

在这个菜谱中,我们实现了一个非常简单的例子:运行 10 个线程 100 秒。这些线程是在控制台写入消息的线程。

当您执行 JConsole 时,您将看到一个窗口,显示您系统中正在运行的全部 Java 应用程序。您可以选择要监控的应用程序。窗口将类似于以下内容:

在此情况下,我们选择我们的示例应用程序并点击“连接”按钮。然后,您将被要求与应用程序建立不安全的连接,对话框类似于以下内容:

点击“不安全连接”按钮。JConsole 将使用六个标签页显示您应用程序的信息:

  • “概览”标签页提供了内存使用情况、应用程序中运行的线程数量、创建的对象数量以及应用程序的 CPU 使用情况的概述。

  • “内存”标签页显示了应用程序使用的内存量。它有一个组合框,您可以选择要监控的内存类型(堆、非堆或池)。

  • “线程”标签页显示了应用程序中的线程数量以及每个线程的详细信息。

  • “类”标签页显示了应用程序中加载的对象数量信息。

  • “VW 概览”标签页提供了运行应用程序的 JVM 的摘要。

  • “MBeans”标签页显示了应用程序的管理 bean 信息。

“线程”标签页类似于以下内容:

它分为两个不同的部分。在上部,您有关于峰值线程数量(用红线表示)和活动线程数量(用蓝线表示)的实时信息。在下部,我们有一个活动线程列表。当您选择这些线程之一时,您将看到该线程的详细信息,包括其状态和实际的堆栈跟踪。

更多内容...

您可以使用其他应用程序来监控运行 Java 的应用程序。例如,您可以使用包含在 JDK 中的 VisualVM。您可以在visualvm.github.io获取关于 visualvm 的必要信息。

参见

  • 本章中关于使用MultithreadedTC进行并发代码测试的配方

第十章:补充信息

在本章中,我们将涵盖以下主题:

  • 在 Executor 框架中处理 Runnable 对象的结果

  • 在 ForkJoinPool 类中处理未受控的异常

  • 使用阻塞线程安全的队列与生产者和消费者进行通信

  • 监控 Thread

  • 监控 Semaphore

  • 生成并发随机数

简介

本章包括关于 Executor 框架和 fork/join 框架、并发数据结构、监控并发对象以及生成并发随机数的食谱。

在 Executor 框架中处理 Runnable 对象的结果

Executor 框架允许使用 CallableFuture 接口执行返回结果的并发任务。Java 中的传统并发编程基于 Runnable 对象,但这种类型的对象不返回结果。

在本食谱中,你将学习如何将 Runnable 对象适配为 Callable 对象,允许并发任务返回结果。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

执行以下步骤以实现示例:

  1. 创建一个名为 FileSearch 的类,并指定它实现 Runnable 接口。此类实现了文件搜索操作:
        public class FileSearch implements Runnable { 

  1. 声明两个私有 String 属性:一个名为 initPath,它将存储搜索操作的初始文件夹,另一个名为 end,它将存储此任务将要查找的文件的扩展名:
        private String initPath; 
        private String end; 

  1. 声明一个名为 results 的私有 List<String> 属性,该属性将存储此任务找到的文件的完整路径:
        private List<String> results; 

  1. 实现类的构造函数,初始化其属性:
        public FileSearch(String initPath, String end) { 
          this.initPath = initPath; 
          this.end = end; 
          results=new ArrayList<>(); 
        } 

  1. 实现方法 getResults()。此方法返回包含此任务找到的文件完整路径的列表:
        public List<String> getResults() { 
          return results; 
        } 

  1. 实现方法 run()。首先,向控制台写入一条日志消息,表明任务开始执行工作:
        @Override 
        public void run() { 
          System.out.printf("%s: Starting\n",
                            Thread.currentThread().getName()); 

  1. 然后,如果 initPath 属性存储了现有文件夹的名称,请调用辅助方法 directoryProcess() 来处理其文件和文件夹:
        File file = new File(initPath); 
          if (file.isDirectory()) { 
            directoryProcess(file); 
          } 

  1. 实现辅助的 diretoryProcess() 方法,该方法接收一个 File 对象作为参数。首先,获取参数指向的文件夹的内容:
        private void directoryProcess(File file) { 
          File list[] = file.listFiles(); 

  1. 对于文件夹中的所有元素,如果它们是文件夹,则递归调用 directoryProcess() 方法。如果它们是文件,则调用辅助方法 fileProcess()
        if (list != null) { 
          for (int i = 0; i < list.length; i++) { 
            if (list[i].isDirectory()) { 
              directoryProcess(list[i]); 
            } else { 
              fileProcess(list[i]); 
            } 
          } 
        } 

  1. 实现辅助方法 fileProcess(),该方法接收一个包含文件完整路径的 File 对象。此方法检查文件扩展名是否与存储在 end 属性中的扩展名相等。如果它们相等,将文件的完整路径添加到结果列表中:
        private void fileProcess(File file) { 
          if (file.getName().endsWith(end)) { 
            results.add(file.getAbsolutePath()); 
          } 
        } 

  1. 实现一个名为 Task 的类,该类扩展了 FutureTask 类。你将使用 List<String> 作为参数化类型,因为这将是这个任务返回的数据类型:
        public class Task extends FutureTask<List<String>> { 

  1. 声明一个名为 fileSearch 的私有 FileSearch 属性:
        private FileSearch fileSearch; 

  1. 实现这个类的构造函数。这个构造函数有两个参数:一个名为 runnableRunnable 对象和一个名为 resultList<String> 对象。在构造函数中,你必须调用父类的构造函数,并将相同的参数传递给它。然后,存储 runnable 参数,将其转换为 FileSearch 对象:
        public Task(Runnable runnable, List<String> result) { 
          super(runnable, result); 
          this.fileSearch=(FileSearch)runnable; 
        } 

  1. 重写 FutureTask 类的 set() 方法:
        @Override 
        protected void set(List<String> v) { 

  1. 如果它接收到的参数是 null,则将其存储为调用 FileSearch 类的 getResults() 方法的返回结果:
        v=fileSearch.getResults();     

  1. 然后,调用父类的方法,将接收到的参数作为参数传递:
        super.set(v); 

  1. 最后,实现示例的主类。创建一个名为 Main 的类并实现 main() 方法:
        public class Main { 
          public static void main(String[] args) { 

  1. 调用 Executors 类的 newCachedThreadPool() 方法创建一个名为 executorThreadPoolExecutor 对象:
        ExecutorService executor = Executors.newCachedThreadPool(); 

  1. 创建三个具有不同初始文件夹的 FileSearch 对象。你将查找具有 log 扩展名的文件:
        FileSearch system=new FileSearch("C:\\Windows", "log"); 
        FileSearch apps=new FileSearch("C:\\Program Files","log"); 
        FileSearch documents=new FileSearch("C:\\Documents And
                                             Settings","log"); 

  1. 创建三个 Task 对象以在执行器中执行搜索操作:
        Task systemTask=new Task(system,null); 
        Task appsTask=new Task(apps,null); 
        Task documentsTask=new Task(documents,null); 

  1. 使用 submit() 方法将这些对象发送到执行器对象。这个版本的 submit() 方法返回一个 Future<?> 对象,但你将忽略它。你有一个扩展了 FutureTask 类的类来控制这个任务的执行:
        executor.submit(systemTask); 
        executor.submit(appsTask); 
        executor.submit(documentsTask); 

  1. 调用执行器对象的 shutdown() 方法,表示当这三个任务完成时,它应该完成其执行:
        executor.shutdown(); 

  1. 调用执行器对象的 awaitTermination() 方法,表示一个较长的等待期,以确保这个方法不会在三个任务完成之前返回:
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 对于每个任务,使用 Task 对象的 get() 方法写一个包含结果列表大小的消息:
        try { 
          System.out.printf("Main: System Task: Number of Results: %d\n",
                            systemTask.get().size()); 
          System.out.printf("Main: App Task: Number of Results: %d\n",
                            appsTask.get().size()); 
          System.out.printf("Main: Documents Task: Number of 
                             Results: %d\n",documentsTask.get().size()); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } catch (ExecutionException e) { 
          e.printStackTrace(); 
        } 

它是如何工作的...

理解这个示例的第一个要点是,当将 Callable 对象作为参数传递给 ThreadPoolExecutor 类的 submit() 方法时,与将 Runnable 对象作为参数传递时的 submit() 方法的区别。在前一种情况下,你可以使用这个方法返回的 Future 对象来控制任务的状态并获取其结果。但在第二种情况下,当你传递一个 Runnable 对象时,你只能使用这个方法返回的 Future 对象来控制任务的状态。如果你调用那个 Future 对象的 get() 方法,你会得到一个 null 值。

要覆盖此行为,您已实现了Task类。这个类扩展了实现了Future接口和Runnable接口的FutureTask类。当您调用返回Future对象的方法(例如,submit()方法)时,您通常会得到一个FutureTask对象。因此,您可以使用这个类实现两个目标:

  1. 首先,执行Runnable对象(在这种情况下,一个FileSearch对象)。

  2. 其次,返回此任务生成的结果。为了实现这一点,您已覆盖了Task类的set()方法。内部,FutureTask类控制它必须执行的任务何时完成。在那个时刻,它调用set()方法来设置任务的返回值。当您执行Callable对象时,这个调用使用call()方法返回的值,但当您执行Runnable对象时,这个调用使用 null 值。您已将此 null 值更改为由FileSearch对象生成的结果列表。set()方法只有在第一次调用时才会生效。当它第一次被调用时,它将任务标记为完成,其余的调用将不会修改任务的返回值。

Main类中,您可以将FutureTask对象发送到执行器对象,而不是发送到CallableRunnable对象。主要区别在于您使用FutureTask对象来获取任务的结果,而不是使用submit()方法返回的Future对象。

在这种情况下,您仍然可以使用submit()方法返回的Future对象来控制任务的状态,但请记住,由于此任务执行一个Runnable对象(您已使用实现了Runnable接口的FileSearch对象初始化了FutureTask对象),如果您在Future对象中调用get()方法,您将得到 null 值。

更多内容...

FutureTask类提供了一个不在Future接口中包含的方法。它是setException()方法。此方法接收一个Throwable对象作为参数,当调用get()方法时,将抛出ExecutionException异常。此调用只有在尚未调用FutureTask对象的set()方法时才有效。

参见

  • 在第四章的在返回结果的执行器中执行任务菜谱中,线程执行器

  • 在第一章的创建、运行和设置线程特性菜谱中,线程管理

在 ForkJoinPool 类中处理不受控制的异常

Fork/Join 框架为您提供了为ForkJoinPool类的工作线程抛出的异常设置处理程序的可能性。当您使用ForkJoinPool类工作时,您应该了解任务和工作线程之间的区别。

要与 fork/join 框架一起工作,你实现一个扩展 ForkJoinTask 类的任务,通常是 RecursiveActionRecursiveTask 类。任务实现了你想要与框架并发执行的操作。它们由工作线程在 ForkJoinPool 类中执行。工作线程将执行各种任务。在 ForkJoinPool 类实现的 work-stealing 算法中,当工作线程执行的任务完成其执行或正在等待另一个任务的完成时,它将寻找新的任务。

在这个菜谱中,你将学习如何处理工作线程抛出的异常。为了使其按以下项目所述工作,你必须实现两个额外的元素:

  • 第一个元素是 ForkJoinWorkerThread 类的扩展类。这个类实现了 ForkJoinPool 类的工作线程。你将实现一个基本的子类,该子类将抛出异常。

  • 第二个元素是创建自定义类型工作线程的工厂。ForkJoinPool 类使用工厂来创建其工作线程。你必须实现一个类,该类实现了 ForkJoinWorkerThreadFactory 接口,并在 ForkJoinPool 类的构造函数中使用该类的对象。创建的 ForkJoinPool 对象将使用该工厂来创建工作线程。

如何做到这一点...

执行以下步骤以实现示例:

  1. 首先,实现你自己的工作线程类。创建一个名为 AlwaysThrowsExceptionWorkerThread 的类,该类扩展了 ForkJoinWorkerThread 类:
        public class AlwaysThrowsExceptionWorkerThread extends
                                ForkJoinWorkerThread { 

  1. 实现类的构造函数。它接收一个 ForkJoinPool 类作为参数,并调用其父类的构造函数:
        protected AlwaysThrowsExceptionWorkerThread(ForkJoinPool pool) { 
          super(pool); 
        } 

  1. 实现 onStart() 方法。这是 ForkJoinWorkerThread 类的一个方法,在工作线程开始执行时执行。当被调用时,实现将抛出一个 RuntimeException 异常:
        protected void onStart() { 
          super.onStart(); 
          throw new RuntimeException("Exception from worker thread"); 
        } 

  1. 现在,实现创建工作线程所需的工厂。创建一个名为 AlwaysThrowsExceptionWorkerThreadFactory 的类,该类实现了 ForkJoinWorkerThreadFactory 接口:
        public class AlwaysThrowsExceptionWorkerThreadFactory implements
                                 ForkJoinWorkerThreadFactory {  

  1. 实现 newThread() 方法。它接收一个 ForkJoinPool 对象作为参数,并返回一个 ForkJoinWorkerThread 对象。创建一个 AlwaysThrowsExceptionWorkerThread 对象并返回它:
        @Override 
        public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 
          return new AlwaysThrowsExceptionWorkerThread(pool); 
        } 

  1. 实现一个将管理工作线程抛出的异常的类。实现一个名为 Handler 的类,该类实现了 UncaughtExceptionHandler 接口:
        public class Handler implements UncaughtExceptionHandler { 

  1. 实现 uncaughtException() 方法。它接收一个线程对象和一个 Throwable 对象作为参数,并在工作线程抛出异常时由 ForkJoinPool 类调用。向控制台写入一条消息并退出程序:
        @Override 
        public void uncaughtException(Thread t, Throwable e) { 
          System.out.printf("Handler: Thread %s has thrown an
                             Exception.\n",t.getName()); 
          System.out.printf("%s\n",e); 
          System.exit(-1); 
        } 

  1. 现在,实现一个要在 ForkJoinPool 执行器中执行的任务。创建一个名为 OneSecondLongTask 的类,该类扩展了 RecursiveAction 类:
        public class OneSecondLongTask extends RecursiveAction{ 

  1. 实现了 compute() 方法。它简单地在经过一秒后将线程休眠:
        @Override 
        protected void compute() { 
          System.out.printf("Task: Starting.\n"); 
          try { 
            TimeUnit.SECONDS.sleep(1); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
          System.out.printf("Task: Finish.\n"); 
        } 

  1. 现在,实现示例的主类。创建一个名为 Main 的类,并包含一个 main() 方法:
        public class Main { 
          public static void main(String[] args) { 

  1. 创建一个新的 OneSecondLongTask 对象:
        OneSecondLongTask task=new OneSecondLongTask(); 

  1. 创建一个新的 Handler 对象:
        Handler handler = new Handler(); 

  1. 创建一个新的 AlwaysThrowsExceptionWorkerThreadFactory 对象:
        AlwaysThrowsExceptionWorkerThreadFactory factory=new
                            AlwaysThrowsExceptionWorkerThreadFactory(); 

  1. 创建一个新的 ForkJoinPool 对象。将值 2、工厂对象、处理程序对象和值 false 作为参数传递:
        ForkJoinPool pool=new ForkJoinPool(2,factory,handler,false); 

  1. 使用 execute() 方法在池中执行任务:
        pool.execute(task); 

  1. 使用 shutdown() 方法关闭池:
        pool.shutdown(); 

  1. 使用 awaitTermination() 方法等待任务的最终化:
        try { 
          pool.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 编写一条消息以指示程序的结束:
         System.out.printf("Task: Finish.\n"); 

它是如何工作的...

在这个示例中,你实现了以下元素:

  • 你自己的工作线程类:你已经实现了 AlwaysThrowsExceptionWorkerThread 类,它扩展了 ForkJoinWorkerThread 类,该类实现了 fork/join 池的工作线程。你已经重写了 onStart() 方法。该方法在工作线程开始执行时执行。它简单地在被调用时抛出一个 RuntimeException 异常。

  • 你自己的线程工厂ForkJoinPool 类使用一个工厂来创建其工作线程。由于你想创建一个使用 AlwaysThrowsExceptionWorkerThreadFactory 工作线程的 ForkJoinPool 对象,你已经实现了一个创建它们的工厂。要实现工作线程工厂,你需要实现 ForkJoinWorkerThreadFactory 接口。该接口只有一个名为 newThread() 的方法,该方法创建工作线程并将其返回给 ForkJoinPool 类。

  • 任务类:工作线程执行你发送给 ForkJoinPool 执行器的任务。由于你想启动工作线程的执行,你需要将任务发送给 ForkJoinPool 执行器。任务会休眠一秒,但由于 AlwaysThrowsExceptionWorkerThread 线程抛出异常,它将永远不会被执行。

  • 未捕获异常的处理程序类:当工作线程抛出异常时,ForkJoinPool 类会检查是否已注册异常处理程序。你已实现了 Handler 类来完成此目的。此处理程序实现了 UncaughtExceptionHandler 接口,该接口只有一个方法,即 uncaughtException() 方法。该方法接收一个参数,即抛出异常的线程及其抛出的异常。

在 Main 类中,你已经将这些元素组合在一起。你向 ForkJoinPool 类的构造函数传递了四个参数:并行级别、你想要拥有的活动工作线程数量、你想要在 ForkJoinPool 对象中使用的线程工厂、你想要用于工作线程未捕获异常的处理程序,以及异步模式。

以下截图显示了此示例的执行结果:

当你执行程序时,一个工作线程抛出一个RuntimeException异常。ForkJoinPool类将其传递给你的处理器,然后处理器将消息写入控制台并退出程序。任务不会开始执行。

还有更多...

你可以测试这个示例的两个有趣的变体:

  • 如果你取消注释处理器类中的以下行并执行程序,你将在控制台看到很多消息。ForkJoinPool类试图启动一个工作线程来执行任务,因为它无法执行(因为它们总是抛出异常),所以它一次又一次地尝试:
        System.exit(-1); 

  • 如果你将ForkJoinPool类构造函数的第三个参数(异常处理器)改为 null 值,就会发生类似的情况。在这种情况下,你将看到 JVM 如何在控制台写入异常。

  • 在实现自己的工作线程并可能抛出异常时,请考虑这一点。

参见

  • 在第五章的创建 fork/join 池配方中,Fork/Join 框架,第六部分

  • 在第八章的自定义在 fork/join 框架中运行的任务实现 ThreadFactory 接口以生成 fork/join 框架的自定义线程配方中,自定义并发类,第六部分

使用阻塞线程安全的队列与生产者和消费者进行通信

生产者/消费者问题是并发编程中的一个经典问题。你有一个或多个生产者将数据存储在缓冲区中。你还有一个或多个消费者从同一个缓冲区中获取数据。生产者和消费者共享同一个缓冲区,因此你必须控制对其的访问以避免数据不一致问题。当缓冲区为空时,消费者会等待直到缓冲区有元素。如果缓冲区已满,生产者会等待直到缓冲区有空闲空间。

这个问题已经使用 Java 和其他语言中开发的几乎所有技术和同步机制来实现(有关更多信息,请参阅参见部分)。这个问题的优点是它可以推广到许多现实世界的情况。

Java 7 并发 API 引入了一个面向这些类型问题的数据结构。它是LinkedTransferQueue类,其主要特点如下:

  • 它是一个阻塞的数据结构。线程在操作可以执行之前会被阻塞,前提是操作立即执行。

  • 它的大小没有限制。你可以插入你想要的任意多个元素。

  • 它是一个参数化类。你必须指定你打算存储在列表中的元素的类。

在这个配方中,你将学习如何使用LinkedTransferQueue类运行许多共享字符串缓冲区的生产者和消费者任务。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或任何其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

执行以下步骤以实现示例:

  1. 创建一个名为Producer的类,并指定它实现Runnable接口:
        public class Producer implements Runnable { 

  1. 声明一个名为buffer的私有LinkedTransferQueue属性,参数化为String类:
        private LinkedTransferQueue<String> buffer; 

  1. 声明一个名为name的私有String属性以存储生产者的名称:
        private String name; 

  1. 实现类的构造函数以初始化其属性:
        public Producer(String name, LinkedTransferQueue<String> buffer){ 
          this.name=name; 
          this.buffer=buffer; 
        } 

  1. 实现该run()方法。使用缓冲区对象的put()方法将10,000个字符串存储在缓冲区中,并向控制台写入一条消息,表示方法结束:
        @Override 
        public void run() { 
          for (int i=0; i<10000; i++) { 
            buffer.put(name+": Element "+i); 
          } 
          System.out.printf("Producer: %s: Producer done\n",name); 
        } 

  1. 实现一个名为Consumer的类,并指定它实现Runnable接口:
        public class Consumer implements Runnable { 

  1. 声明一个名为buffer的私有LinkedTransferQueue属性,参数化为String类:
        private LinkedTransferQueue<String> buffer; 

  1. 声明一个名为name的私有String属性以存储消费者的名称:
        private String name; 

  1. 实现类的构造函数以初始化其属性:
        public Consumer(String name, LinkedTransferQueue<String> buffer){ 
          this.name=name; 
          this.buffer=buffer; 
        } 

  1. 实现该run()方法。使用缓冲区对象的take()方法从缓冲区中取出 10,000 个字符串,并向控制台写入一条消息,表示方法结束:
        @Override 
        public void run() { 
          for (int i=0; i<10000; i++){ 
            try { 
              buffer.take(); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
          System.out.printf("Consumer: %s: Consumer done\n",name); 
        } 

  1. 实现示例的主类。创建一个名为Main的类,并向其中添加main()方法:
        public class Main { 
          public static void main(String[] args) { 

  1. 声明一个名为THREADS的常量,并将其值设置为100。创建一个参数化为String类的LinkedTransferQueue对象,并将其命名为 buffer:
        final int THREADS=100; 
        LinkedTransferQueue<String> buffer=new LinkedTransferQueue<>(); 

  1. 创建一个包含 100 个Thread对象的数组以执行 100 个生产者任务:
        Thread producerThreads[]=new Thread[THREADS]; 

  1. 创建一个包含 100 个Thread对象的数组以执行 100 个消费者任务:
        Thread consumerThreads[]=new Thread[THREADS]; 

  1. 创建并启动 100 个Consumer对象,并将线程存储在之前创建的数组中:
        for (int i=0; i<THREADS; i++){ 
          Consumer consumer=new Consumer("Consumer "+i,buffer); 
          consumerThreads[i]=new Thread(consumer); 
          consumerThreads[i].start(); 
        } 

  1. 创建并启动 100 个Producer对象,并将线程存储在之前创建的数组中:
        for (int i=0; i<THREADS; i++) { 
          Producer producer=new Producer("Producer: "+ i , buffer); 
          producerThreads[i]=new Thread(producer); 
          producerThreads[i].start(); 
        } 

  1. 使用join()方法等待线程的最终化:
        for (int i=0; i<THREADS; i++){ 
          try { 
            producerThreads[i].join(); 
            consumerThreads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 

  1. 向控制台写入一条包含缓冲区大小的消息:
        System.out.printf("Main: Size of the buffer: %d\n",
                          buffer.size()); 
        System.out.printf("Main: End of the example\n"); 

它是如何工作的...

在这个菜谱中,你使用了参数化为String类的LinkedTransferQueue类来实现生产者/消费者问题。这个LinkedTransferQueue类被用作缓冲区,在生产者和消费者之间共享数据。

你实现了一个Producer类,使用put()方法向缓冲区添加字符串。你已经执行了 100 个生产者,每个生产者在缓冲区中插入 10,000 个字符串,因此你在缓冲区中插入了 1,000,000 个字符串。put()方法将元素添加到缓冲区的末尾。

你还实现了一个Consumer类,该类使用take()方法从缓冲区中获取字符串。此方法返回并删除缓冲区的第一个元素。如果缓冲区为空,则该方法将阻塞调用线程,直到缓冲区中有可消费的字符串。你已经执行了 100 个消费者,每个消费者从缓冲区中获取 10,000 个字符串。

在示例中,首先启动了消费者,然后是生产者,因此,由于缓冲区为空,所有消费者都将被阻塞,直到生产者开始执行并将字符串存储在列表中。

以下截图显示了此示例执行的部分输出:

图片

要写入缓冲区中的元素数量,你使用了size()方法。你必须考虑到,如果你在列表中有线程添加或删除数据时使用此方法,它可能返回一个非真实值。该方法必须遍历整个列表来计数元素,并且列表的内容可能会因为此操作而改变。只有在你没有线程修改列表时使用它们,你才能保证返回的结果是正确的。

更多...

LinkedTransferQueue类提供了其他有用的方法。以下是一些:

  • getWaitingConsumerCount(): 此方法返回因为LinkedTransferQueue对象为空而在take()方法或poll (long timeout, TimeUnit unit)中被阻塞的消费者数量。

  • hasWaitingConsumer(): 如果LinkedTransferQueue对象有等待的消费者,则此方法返回true,否则返回false

  • offer(E e): 此方法将作为参数传递的元素添加到LinkedTransferQueue对象的末尾,并返回 true 值。E代表用于参数化LinkedTransferQueue类声明或其子类的类。

  • peek(): 此方法返回LinkedTransferQueue对象中的第一个元素,但不从列表中删除它。如果队列是空的,则方法返回 null 值。

  • poll(long timeout, TimeUnit unit): 此版本的poll方法,如果LinkedTransferQueue缓冲区为空,将等待指定的时间。如果指定的时间过去后缓冲区仍然为空,则方法返回null值。TimeUnit类是一个枚举,具有以下常量-DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

参见

  • 在第二章的使用同步代码中的条件配方中,基本线程同步

  • 在第三章的在并发任务之间交换数据配方中,线程同步工具

监控线程类

线程是 Java 并发 API 中最基本的元素。每个 Java 程序至少有一个执行main()方法的线程,它反过来启动应用程序的执行。当你启动一个新的Thread类时,它将与应用程序的其他线程以及操作系统上的其他进程并行执行。进程和线程之间存在一个关键的区别。进程是一个正在运行的应用程序的实例(例如,你正在文本处理器中编辑文档)。此进程有一个或多个执行使进程运行的任务的线程。你可以运行同一应用程序的多个进程,例如,两个文本处理器的实例。进程内的线程共享内存,而同一操作系统上的进程则不共享。

你可以执行的 Java 任务类型(RunnableCallable或 fork/join 任务)都是在线程中执行的,所有高级 Java 并发机制,如Executor框架和 fork/join 框架,都是基于线程池的。

在这个菜谱中,你将学习你可以从Thread类的状态中获得哪些信息,以及如何获取这些信息。

准备工作

本菜谱的示例使用 Eclipse IDE 实现。如果你使用 Eclipse 或任何其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做...

执行以下步骤来实现示例:

  1. 创建一个名为Task的类,该类实现了Runnable接口:
        public class Task implements Runnable { 

  1. 实现任务的run()方法:
        @Override 
        public void run() { 

  1. 创建一个包含 100 个步骤的循环:
        for (int i=0; i<100; i++) { 

  1. 在每个步骤中,让线程休眠 100 毫秒:
        try { 
          TimeUnit.MILLISECONDS.sleep(100); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 

  1. 在控制台输出一条消息,包含线程的名称和步骤编号:
              System.out.printf("%s: %d\n",Thread.currentThread()
                                                  .getName(),i);      
            } 
          } 
        } 

  1. 创建示例的主类。创建一个名为Main的类,并包含一个main()方法:
        public class Main { 
          public static void main(String[] args) throws Exception{ 

  1. 创建一个名为taskTask对象:
        Task task = new Task(); 

  1. 创建一个包含五个元素的Thread数组:
        Thread threads[] = new Thread[5]; 

  1. 创建并启动五个线程来执行之前创建的Task对象:
        for (int i = 0; i < 5; i++) { 
          threads[i] = new Thread(task); 
          threads[i].setPriority(i + 1); 
          threads[i].start(); 
        } 

  1. 创建一个包含十个步骤的循环来输出之前启动的线程的信息。在它内部,创建另一个包含五个步骤的循环:
        for (int j = 0; j < 10; j++) { 
          System.out.printf("Main: Logging threads\n"); 
          for (int i = 0; i < threads.length; i++) { 

  1. 对于每个线程,在控制台输出其名称、状态、所属组和堆栈跟踪的长度:
        System.out.printf("**********************\n"); 
        System.out.printf("Main: %d: Id: %d Name: %s: Priority: %d\n",i,
                          threads[i].getId(),threads[i].getName(),
                          threads[i].getPriority()); 
        System.out.printf("Main: Status: %s\n",threads[i].getState()); 
        System.out.printf("Main: Thread Group: %s\n",
                          threads[i].getThreadGroup()); 
        System.out.printf("Main: Stack Trace: \n"); 

  1. 编写一个循环来输出线程的堆栈跟踪:
          for (int t=0; t<threads[i].getStackTrace().length; t++) { 
            System.out.printf("Main: %s\n",threads[i].getStackTrace()
                              [t]); 
          } 
          System.out.printf("**********************\n"); 
        } 

  1. 让线程休眠一秒,然后关闭循环和类:
              TimeUnit.SECONDS.sleep(1); 
            } 
          } 
        } 

它是如何工作的...

在这个菜谱中,你使用了以下方法来获取Thread类的信息:

  • getId(): 此方法返回线程的 ID。它是一个唯一的 long 数字,不能更改。

  • getName(): 此方法返回线程的名称。如果你没有为线程设置名称,Java 会为其提供一个默认名称。

  • getPriority(): 这个方法返回线程的执行优先级。优先级较高的线程会优先于优先级较低的线程执行。它是一个介于Thread类的MIN_PRIORITYMAX_PRIORITY常量之间的int值。默认情况下,线程会以Thread类中常量NORM_PRIORITY指定的相同优先级创建。

  • getState(): 这个方法返回线程的状态。它是一个Thread.State对象。Thread.State枚举包含了线程的所有可能状态。

  • getThreadGroup(): 这个方法返回线程的ThreadGroup对象。默认情况下,线程属于同一个线程组,但你可以在线程的构造函数中创建一个不同的线程组。

  • getStackTrace(): 这个方法返回一个StackTraceElement对象的数组。这些对象中的每一个代表从线程的run()方法开始的调用,包括所有被调用的方法,直到实际的执行点。当调用一个新的方法时,一个新的堆栈跟踪元素会被添加到数组中。当一个方法完成执行后,它的堆栈跟踪元素会被从数组中移除。

还有更多...

Thread类包括其他一些方法,这些方法提供了关于线程的信息,可能很有用。这些方法如下:

  • activeCount(): 这个方法返回线程组中活动线程的数量。

  • dumpStack(): 这个方法将线程的堆栈跟踪打印到标准错误输出。

参见

  • 在第一章,“线程管理”中的创建、运行和设置线程的特性配方

  • 在第八章,“自定义并发类”中的在 Executor 框架中使用 ThreadFactory 接口在 fork/join 框架中实现 ThreadFactory 接口以生成自定义线程的配方

监控信号量类

信号量是一个保护一个或多个共享资源访问的计数器。

信号量的概念是由 Edsgar Dijkstra 在 1965 年提出的,并首次用于 THEOS 操作系统。

当一个线程想要使用共享资源时,它必须获取一个信号量。如果信号量的内部计数器大于 0,信号量会减少计数器并允许访问共享资源。如果信号量的计数器为 0,信号量会阻塞线程,直到计数器大于 0。当线程完成使用共享资源后,它必须释放信号量。这个操作会增加信号量的内部计数器。

在 Java 中,信号量是在Semaphore类中实现的。

在这个配方中,你将学习你可以获取关于信号量状态的哪些信息以及如何获取这些信息。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或任何其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

执行以下步骤以实现示例:

  1. 创建一个名为Task的类,该类实现了Runnable接口:
        public class Task implements Runnable { 

  1. 声明一个名为semaphore的私有Semaphore属性:
        private final Semaphore semaphore; 

  1. 实现类的构造函数以初始化其属性:
        public Task(Semaphore semaphore){ 
          this.semaphore=semaphore; 
        } 

  1. 实现run()方法。首先,获取semaphore属性的许可,并在控制台写入一条消息以指示该情况:
        @Override 
        public void run() { 
          try { 
            semaphore.acquire(); 
            System.out.printf("%s: Get the semaphore.\n",
                              Thread.currentThread().getName()); 

  1. 然后,使用sleep()方法将线程休眠两秒。最后,释放许可,并在控制台写入一条消息以指示该情况:
          TimeUnit.SECONDS.sleep(2); 
          System.out.println(Thread.currentThread().getName()+":
                             Release the semaphore."); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } finally { 
          semaphore.release();       
        } 

  1. 实现示例的主类。创建一个名为Main的类,其中包含一个main()方法:
        public class Main { 
          public static void main(String[] args) throws Exception { 

  1. 创建一个名为semaphoreSemaphore对象,具有三个许可:
        Semaphore semaphore=new Semaphore(3); 

  1. 创建一个数组以存储 10 个Thread对象:
        Thread threads[]=new Thread[10]; 

  1. 创建并启动 10 个Thread对象以执行 10 个Task对象。启动线程后,将其休眠 200 毫秒,并调用showLog()方法来记录关于Semaphore类的信息:
        for (int i=0; i<threads.length; i++) { 
          Task task=new Task(semaphore); 
          threads[i]=new Thread(task); 
          threads[i].start(); 

          TimeUnit.MILLISECONDS.sleep(200); 

          showLog(semaphore); 
        } 

  1. 实现一个包含五个步骤的循环,调用showLog()方法记录关于semaphore的信息,并将线程休眠1秒:
          for (int i=0; i<5; i++) { 
            showLog(semaphore); 
            TimeUnit.SECONDS.sleep(1); 
          } 
        } 

  1. 实现一个名为showLog()的方法。它接收一个Semaphore对象作为参数。在控制台写入有关可用许可、队列线程和semaphore许可的信息:
        private static void showLog(Semaphore semaphore) { 
          System.out.printf("********************\n"); 
          System.out.printf("Main: Semaphore Log\n"); 
          System.out.printf("Main: Semaphore: Avalaible Permits: %d\n",
                            semaphore.availablePermits()); 
          System.out.printf("Main: Semaphore: Queued Threads: %s\n",
                            semaphore.hasQueuedThreads()); 
          System.out.printf("Main: Semaphore: Queue Length: %d\n",
                            semaphore.getQueueLength()); 
          System.out.printf("Main: Semaphore: Fairness: %s\n",
                            semaphore.isFair()); 
          System.out.printf("********************\n"); 
        } 

它是如何工作的...

在本食谱中,您已使用以下方法来获取关于semaphore的信息:

  • availablePermits(): 此方法返回一个int值,表示信号量的可用资源数量。

  • hasQueuedThreads(): 此方法返回一个布尔值,指示是否有线程正在等待由信号量保护的资源。

  • getQueueLength(): 此方法返回正在等待由信号量保护的资源的线程数量。

  • isFair(): 此方法返回一个布尔值,指示信号量是否激活了公平模式。当公平模式激活(此方法返回 true 值)时,并且锁必须选择另一个线程以提供对共享资源的访问时,它选择等待时间最长的线程。如果公平模式未激活(此方法返回 false 值),则没有关于线程选择顺序的保证。

参见

  • 在第三章的控制对资源的一个或多个副本的并发访问食谱中,线程同步工具

生成并发随机数

Java 并发 API 提供了一个特定的类来在并发应用程序中生成伪随机数。它是 ThreadLocalRandom 类,并且是 Java 7 版本中新增的。它作为线程的局部变量工作。每个想要生成随机数的线程都有一个不同的生成器,但它们都由同一个类管理,对程序员来说是透明的。通过这种机制,你将获得比使用共享的 Random 对象生成所有线程的随机数更好的性能。

在本菜谱中,你将学习如何使用 ThreadLocalRandom 类在并发应用程序中生成随机数。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 TaskLocalRandom 的类,并指定它实现 Runnable 接口:
        public class TaskLocalRandom implements Runnable { 

  1. 实现 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)); 
          } 
        } 

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例中的主类:
        public class Main { 
          public static void main(String[] args) { 

  1. 创建一个包含三个 Thread 对象的数组:
        Thread threads[]=new Thread[3]; 

  1. 创建并启动三个 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 类还提供了生成长整型、浮点型和双精度型数字以及布尔值的函数。有一些函数允许你提供一个数字作为参数来生成介于零和该数字之间的随机数。其他函数允许你提供两个参数来生成介于这两个数字之间的随机数。

参见

在 第一章 的 使用局部线程变量 菜谱中,线程管理

第十一章:并发编程设计

在本章中,我们将涵盖以下主题:

  • 尽可能使用不可变对象

  • 通过排序锁来避免死锁

  • 使用原子变量而不是同步

  • 尽可能短时间持有锁

  • 将线程的管理委托给执行器

  • 使用并发数据结构而不是自己编程

  • 使用懒初始化时采取预防措施

  • 使用 fork/join 框架而不是执行器

  • 避免在锁内使用阻塞操作

  • 避免使用已弃用方法

  • 使用执行器而不是线程组

  • 使用流来处理大数据集

  • 其他技巧和窍门

简介

实现一个并发应用程序是一项困难的任务。在执行过程中,你同时有多个线程,并且它们共享资源,如文件、内存、对象等。你必须对你的设计决策非常小心。一个糟糕的决定可能会以影响你的程序的方式导致性能下降或简单地引发数据不一致的情况。

在本章中,我提供了一些建议,以帮助您做出正确的设计决策,这将使您的并发应用程序变得更好。

尽可能使用不可变对象

当你使用面向对象的编程在 Java 中开发应用程序时,你创建了一些由属性和方法组成的类。类的方法决定了你可以对该类执行的操作。属性存储定义对象的 数据。通常,在每一个类中,你实现一些方法来设置属性值。此外,对象在应用程序运行时也会发生变化,你使用这些方法来改变它们的属性值。

当你开发一个并发应用程序时,你必须特别注意多个线程共享的对象。你必须使用同步机制来保护对这些对象的访问。如果你不使用它,你可能在应用程序中遇到数据不一致的问题。

当你与并发应用程序一起工作时,你可以实现一些特殊类型的对象。它们被称为不可变对象;它们的主要特征是它们在创建后不能被修改。如果你需要更改不可变对象,你必须创建一个新的对象,而不是更改对象的属性值。

当你在并发应用程序中使用此机制时,它具有以下优点:

  • 这些对象一旦创建,就不能被任何线程修改,因此你不需要使用任何同步机制来保护对其属性的访问。

  • 你不会遇到任何数据不一致的问题。由于这些对象的属性不能被修改,你将始终能够访问数据的一致副本。

这种方法的唯一缺点是开销:创建新对象而不是修改现有对象。

Java 提供了一些不可变类,例如String类。当您有一个String对象,并尝试为其分配新值时,您实际上是在创建一个新的String对象,而不是修改对象的旧值。例如,查看以下代码:

    String var = "hello"; 
    var = "new";

在第二行,JVM 创建了一个新的String对象。

准备工作

此示例的配方已使用 Eclipse IDE 实现。如果您使用 Eclipse 或不同的 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现不可变类:

  1. 将类标记为final。它不应该被另一个类扩展。

  2. 所有属性都必须是finalprivate。您只能给属性赋值一次。

  3. 不要提供可以分配属性值的函数。属性必须在类的构造函数中初始化。

  4. 如果任何字段值对象是可变的(例如,java.util.Date),在 getter 字段中始终返回防御性副本。

  5. 不要从不可变类构造函数中泄露this引用(例如,在构造函数完成之前泄露this引用的以下代码):

        public final NotSoImmutable implements Listener { 
          private final int x; 
          public NotSoImmutable(int x, Observable o) { 
            this.x = x; 
            o.registerListener(this); 
          } 
        }

它是如何工作的...

如果您想实现一个存储人的姓名和姓氏的类,您通常会实现如下:

    public class PersonMutable { 
      private String firstName; 
      private String lastName; 
      private Date birthDate; 

      public String getFirstName() { 
        return firstName; 
      } 

      public void setFirstName(String firstName) { 
        this.firstName = firstName; 
      } 

      public String getLastName() { 
        return lastName; 
      } 

      public void setLastName(String lastName) { 
        this.lastName = lastName; 
      } 
      public Date getBirthDate() { 
        return birthDate; 
      } 

      public void setBirthDate(Date birthDate) { 
        this.birthDate = birthDate; 
      } 

    }

您可以通过遵循前面解释的规则将此类转换为不可变类。以下就是结果:

    public final class PersonImmutable { 

      final private String firstName; 
      final private String lastName; 
      final private Date birthDate; 

      public PersonImmutable (String firstName, String lastName,
                              String address, Date birthDate) { 
        this.firstName=firstName; 
        this.lastName=lastName; 
        this.birthDate=birthDate; 
      } 

      public String getFirstName() { 
        return firstName; 
      } 

      public String getLastName() { 
        return lastName; 
      }

      public Date getBirthDate() { 
        return new Date(birthDate.getTime()); 
      } 

    }

实际上,您遵循了不可变类的基本原则,如下所示:

  • 类被标记为final

  • 属性被标记为finalprivate

  • 属性的值只能在类的构造函数中建立。

    它们的方法返回属性的值,但不会修改它们。

  • 对于可变属性(在我们的例子中是birthDate属性),我们通过创建一个新对象来返回get()方法的防御性副本。

更多内容...

不可变对象并不总是可以使用。分析您应用程序的每个类,以确定您是否可以将它们实现为不可变对象。如果您无法将一个类实现为不可变类,并且其对象被多个线程共享,您必须使用同步机制来保护对类属性的访问。

参见

  • 本章中关于“使用原子变量代替同步”的配方

通过排序锁来避免死锁

当您需要在应用程序的方法中获取多个锁时,您必须非常小心地控制锁的获取顺序。错误的选择可能导致死锁情况。

在这个配方中,您将实现一个死锁情况的示例,然后学习如何解决它。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为BadLocks的类,其中包含两个方法,分别命名为operation1()operation2()
        public class BadLocks { 

          private Lock lock1, lock2; 

          public BadLocks(Lock lock1, Lock lock2) { 
            this.lock1=lock1; 
            this.lock2=lock2; 
          } 

          public void operation1(){ 
            lock1.lock(); 
            lock2.lock(); 

            try { 
              TimeUnit.SECONDS.sleep(2); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } finally { 
              lock2.unlock(); 
              lock1.unlock(); 
            } 
          } 

          public void operation2(){ 
            lock2.lock(); 
            lock1.lock(); 

            try { 
              TimeUnit.SECONDS.sleep(2); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } finally { 
              lock1.unlock(); 
              lock2.unlock(); 
            } 
          } 

        }

  1. 让我们分析前面的代码。如果一个线程调用 operation1() 方法,而另一个线程调用 operation2() 方法,你可能会遇到死锁。如果 operation1()operation2() 同时执行它们各自的第一句话,那么 operation1() 方法将等待获取 lock2 的控制权,而 operation2() 方法将等待获取 lock1 的控制权。现在你有一个死锁的情况。

  2. 要解决这个问题,你可以遵循以下规则:

  • 如果你必须在不同的操作中控制多个锁,尝试在所有方法中以相同的顺序锁定它们。

  • 然后,以相反的顺序释放它们,并将锁及其解锁封装在单个类中。这样,你就不需要在代码中分散同步相关的代码。

它是如何工作的...

使用这个规则,你将避免死锁情况。例如,在前面提到的案例中,你可以将 operation2() 改为首先获取 lock1,然后获取 lock2。现在如果 operation1()operation2() 同时执行它们各自的第一句话,其中一个将被阻塞等待 lock1,而另一个将获取 lock1lock2 并执行它们的操作。之后,被阻塞的线程将获取 lock1lock2 锁,并执行其操作。

还有更多...

你可能会遇到一种情况,其中某个要求阻止你在所有操作中以相同的顺序获取锁。在这种情况下,你可以使用 Lock 类的 tryLock() 方法。此方法返回一个 Boolean 值,以指示你是否控制了锁。你可以尝试使用 tryLock() 方法获取你需要的所有锁来完成操作。如果你无法控制其中一个锁,你必须释放你可能拥有的所有锁,并重新开始操作。

参见

  • 本章中 尽可能短时间持有锁 的配方

使用原子变量而不是同步

当你需要在多个线程之间共享数据时,你必须使用同步机制来保护对这块数据的访问。你可以在修改数据的方法的声明中使用 synchronized 关键字,以确保一次只有一个线程可以修改数据。另一种可能性是使用 Lock 类来创建一个临界区,其中包含修改数据的指令。

自版本 5 以来,Java 包含原子变量。当一个线程使用原子变量执行操作时,类的实现包括一个机制来检查操作是否一步完成。基本上,操作获取变量的值,在一个局部变量中更改值,然后尝试用新值替换旧值。如果旧值仍然是相同的,它就会进行更改。如果不是,方法将重新开始操作。Java 提供以下类型的原子变量:

  • AtomicBoolean

  • AtomicInteger

  • AtomicLong

  • AtomicReference

在某些情况下,Java 的原子变量比基于同步机制的解决方案(尤其是当我们关心每个单独变量的原子性时)提供更好的性能。java.util.concurrent 包中的某些类使用原子变量而不是同步。在这个菜谱中,你将开发一个示例,展示原子属性如何比同步提供更好的性能。

准备工作

这个菜谱的示例已经使用 Eclipse IDE 实现。如果你使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 TaskAtomic 的类并指定它实现 Runnable 接口:
        public class TaskAtomic implements Runnable {

  1. 声明一个名为 number 的私有 AtomicInteger 属性:
        private final AtomicInteger number;

  1. 实现类的构造函数以初始化其属性:
        public TaskAtomic () { 
          this.number=new AtomicInteger(); 
        }

  1. 实现 run() 方法。在一个包含 1,000,000 步的循环中,使用 set() 方法将步数作为值分配给原子属性:
        @Override 
        public void run() { 
          for (int i=0; i<1000000; i++) { 
            number.set(i); 
          } 
        }

  1. 创建一个名为 TaskLock 的类并指定它实现 Runnable 接口:
        public class TaskLock implements Runnable {

  1. 声明一个名为 number 的私有 int 属性和一个名为 lock 的私有 Lock 属性:
        private Lock lock; 
        private int number;

  1. 实现类的构造函数以初始化其属性:
        public TaskLock() { 
          this.lock=new ReentrantLock(); 
        }

  1. 实现 run() 方法。在一个包含 1,000,000 步的循环中,将步数分配给整数属性。你必须在分配之前获取锁,并在分配之后释放它:
        @Override 
        public void run() { 
          for (int i=0; i<1000000; i++) { 
            lock.lock(); 
            number=i; 
            lock.unlock(); 
          } 

        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个名为 atomicTaskTaskAtomic 对象:
        TaskAtomic atomicTask=new TaskAtomic();

  1. 创建一个名为 lockTaskTaskLock 对象:
        TaskLock lockTask=new TaskLock();

  1. 声明线程数并创建一个 Thread 对象数组来存储线程:
        int numberThreads=50; 
        Thread threads[]=new Thread[numberThreads]; 
        Date begin, end;

  1. 启动指定数量的线程来执行 TaskLock 对象。计算并写入其执行时间到控制台:
        begin=new Date(); 
        for (int i=0; i<numberThreads; i++) { 
          threads[i]=new Thread(lockTask); 
          threads[i].start(); 
        }

        for (int i=0; i<numberThreads; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 
        end=new Date(); 

        System.out.printf("Main: Lock results: %d\n",
                          (end.getTime()-begin.getTime()));

  1. 启动指定数量的线程来执行 TaskAtomic 对象。计算并写入其执行时间到控制台:
        begin=new Date(); 
        for (int i=0; i<numberThreads; i++) { 
          threads[i]=new Thread(atomicTask); 
          threads[i].start(); 
        } 

        for (int i=0; i<numberThreads; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 
        end=new Date(); 

        System.out.printf("Main: Atomic results: %d\n",
                          (end.getTime()-begin.getTime()));

它是如何工作的...

当你执行示例时,你会看到使用原子变量的 TaskAtomic 任务的执行时间总是比使用锁的 TaskLock 任务的执行时间更好。如果你使用 synchronized 关键字而不是锁,你将获得类似的结果。

这个菜谱的结论是,利用原子变量将比其他同步方法提供更好的性能。如果你没有适合你需求的原子类型,也许你可以尝试实现你自己的原子类型。

参见

  • 在第八章 实现自己的原子对象 的 自定义并发类 菜谱中

尽可能短时间持有锁

锁,就像其他同步机制一样,允许定义一个临界区,一次只有一个线程可以执行。您必须非常小心地定义临界区。它必须只包括真正需要互斥的指令。如果临界区包括长时间操作,这一点尤为重要。如果临界区包括不使用共享资源的长时间操作,应用程序的性能将比可能的情况更差。

在本菜谱中,您将实现一个示例,以查看临界区内部有长时间操作的任务与临界区外部有长时间操作的任务之间的性能差异。

准备工作

该菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 Operations 的类:
        public class Operations {

  1. 实现一个名为 readData()public static 方法。它使当前线程休眠 500 毫秒:
        public static void readData(){ 
          try { 
            TimeUnit.MILLISECONDS.sleep(500); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现一个名为 writeData()public static 方法。它使当前线程休眠 500 毫秒:
        public static void writeData(){ 
          try { 
            TimeUnit.MILLISECONDS.sleep(500); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现一个名为 processData()public static 方法。它使当前线程休眠 2,000 毫秒:
        public static void processData(){ 
          try { 
            TimeUnit.SECONDS.sleep(2); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 实现一个名为 Task1 的类,并指定它实现 Runnable 接口:
        public class Task1 implements Runnable {

  1. 声明一个名为 lock 的私有 Lock 属性:
        private final Lock lock;

  1. 实现类的构造函数以初始化其属性:
        public Task1 (Lock lock) { 
          this.lock=lock; 
        }

  1. 实现 run() 方法。获取锁,调用 Operations 类的三个操作,并释放锁:
        @Override 
        public void run() { 
          lock.lock(); 
          Operations.readData(); 
          Operations.processData(); 
          Operations.writeData(); 
          lock.unlock(); 
        }

  1. 实现一个名为 Task2 的类,并指定它实现 Runnable 接口:
        public class Task2 implements Runnable {

  1. 声明一个名为 lock 的私有 Lock 属性:
        private final Lock lock;

  1. 实现类的构造函数以初始化其属性:
        public Task2 (Lock lock) { 
          this.lock=lock; 
        }

  1. 实现 run() 方法。获取锁,调用 readData() 操作,并释放锁。然后,调用 processData() 方法,获取锁,调用 writeData() 操作,并释放锁:
        @Override 
        public void run() { 
          lock.lock(); 
          Operations.readData(); 
          lock.unlock(); 
          Operations.processData(); 
          lock.lock(); 
          Operations.writeData(); 
          lock.unlock(); 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建一个名为 lockLock 对象,一个名为 task1Task1 对象,一个名为 task2Task2 对象,以及一个包含 10 个线程的数组:
        Lock lock=new ReentrantLock(); 
        Task1 task1=new Task1(lock); 
        Task2 task2=new Task2(lock); 
        Thread threads[]=new Thread[10];

  1. 通过控制执行时间来启动 10 个线程以执行第一个任务:
        Date begin, end; 

        begin=new Date(); 
        for (int i=0; i<threads.length; i++) { 
          threads[i]=new Thread(task1); 
          threads[i].start(); 
        } 

        for (int i=0; i<threads.length; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 
        end=new Date(); 
        System.out.printf("Main: First Approach: %d\n",
                          (end.getTime()-begin.getTime()));

  1. 通过控制执行时间来启动 10 个线程以执行第二个任务:
        begin=new Date(); 
        for (int i=0; i<threads.length; i++) { 
          threads[i]=new Thread(task2); 
          threads[i].start(); 
        } 

        for (int i=0; i<threads.length; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 
        end=new Date(); 
        System.out.printf("Main: Second Approach: %d\n",
                          (end.getTime()-begin.getTime()));

它是如何工作的...

如果您执行示例,您将看到两种方法执行时间之间的巨大差异。所有操作都在临界区内的任务比其他任务花费的时间更长。

当您需要实现一个受锁保护的代码块时,仔细分析它,只包括必要的指令。将方法拆分为多个关键部分,并在必要时使用多个锁以获得最佳的应用程序性能。

参见

  • 本章中关于通过排序锁避免死锁的配方

将线程管理委托给执行器

在 Java 5 之前,当我们想要实现一个并发应用程序时,我们必须自己管理线程。最初,我们通常实现Runnable接口或Thread类的扩展。然后,我们创建一个thread对象,并使用其start()方法启动其执行。我们还需要控制其状态,以了解线程是否已经完成执行或仍在运行。

在 Java 5 版本中,执行器作为提供执行线程池的提供者的概念出现。这种机制由ExecutorExecutorService接口以及ThreadPoolExecutorScheduledThreadPoolExecutor类实现,它允许您只关注任务的逻辑实现。您实现任务并将其发送到执行器。它有一个线程池,并且是这个池负责线程的创建、管理和最终化。在 Java 7 版本中,在 fork/join 框架中出现了执行器机制的另一种实现,专门用于可以分解为更小子问题的任务。这种方法具有许多优势,如下所述:

  • 我们不需要为所有任务创建线程。当我们向执行器发送任务,并由池中的线程执行时,我们节省了创建新线程所需的时间。如果我们的应用程序必须执行大量任务,那么节省的总时间将会非常显著,并且应用程序的性能将会更好。

  • 如果我们创建的线程较少,我们的应用程序也将使用更少的内存。这也可以从我们的应用程序中提取更好的性能。

  • 我们可以通过实现RunnableCallable接口来构建由执行器执行的任务。Callable接口允许我们实现返回结果的任务,这比传统任务提供了很大的优势。

  • 当我们向执行器发送任务时,它返回一个Future对象,允许我们轻松了解任务的状态和返回的结果,无论它是否已经完成执行。

  • 我们可以使用ScheduledThreadPoolExecutor类实现的特殊执行器来安排我们的任务并重复执行它们。

  • 我们可以轻松控制执行器使用的资源。我们可以设置池中线程的最大数量,这样我们的执行器就不会同时运行超过这个数量的任务。

与直接使用线程相比,使用执行器有很多优势。在本配方中,您将实现一个示例,展示如何使用执行器而不是自己创建线程来获得更好的性能。

准备工作

本配方示例使用 Eclipse IDE 实现。如果您使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 实现运行方法。创建一个包含 1,000,000 步的循环,并在每一步中,对一个整型变量进行一些数学运算:
        @Override 
        public void run() { 
          int r; 
          for (int i=0; i<1000000; i++) { 
            r=0; 
            r++; 
            r++; 
            r*=r; 
          } 
        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例中的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建 1,000 个线程来执行 1,000 个任务对象并等待它们完成,控制总执行时间:
        Thread threads[]=new Thread[1000]; 
        Date start,end; 

        start=new Date(); 
        for (int i=0; i<threads.length; i++) { 
          Task task=new Task(); 
          threads[i]=new Thread(task); 
          threads[i].start(); 
        } 

        for (int i=0; i<threads.length; i++) { 
          try { 
            threads[i].join(); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        } 
        end=new Date(); 
        System.out.printf("Main: Threads: %d\n",
                          (end.getTime()-start.getTime()));

  1. 创建一个 Executor 对象,发送 1,000 个 Task 对象给它,并等待它们完成。测量总执行时间:
        ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors
                                                .newCachedThreadPool(); 

        start=new Date();

        for (int i=0; i<threads.length; i++) { 
          Task task=new Task(); 
          executor.execute(task); 
        } 
        executor.shutdown(); 
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 
        end=new Date(); 
        System.out.printf("Main: Executor: %d\n",
                          (end.getTime()-start.getTime()));

它是如何工作的...

在整个示例执行过程中,我们总是获得了比直接创建线程更小的执行时间。如果你的应用程序必须执行大量任务,最好使用执行器。

参见

  • 本章中的 使用执行器而不是线程组使用 fork/join 框架而不是执行器 的食谱

使用并发数据结构而不是自己编程

数据结构是每个程序的基本组成部分。你必须始终管理存储在数据结构中的数据。数组、列表或树是常见的数据结构示例。Java API 提供了许多现成可用的数据结构,但在处理并发应用程序时,你必须小心,因为 Java API 提供的所有结构并不都是 线程安全 的。如果你选择了一个非线程安全的数据结构,你的应用程序中可能会有不一致的数据。

当你想要在你的并发应用程序中使用一种数据结构时,你必须审查实现该数据结构的类的文档,以检查它是否支持并发操作。Java 提供以下两种类型的并发数据结构:

  • 非阻塞数据结构:这些数据结构提供的所有操作,无论是向数据结构中插入元素还是从中移除元素,如果当前无法执行(因为数据结构已满或为空),都会返回 null 值。

  • 阻塞数据结构:这些数据结构提供了与非阻塞数据结构相同的操作。然而,它们还提供了插入和移除数据操作,如果未立即执行,将阻塞线程,直到你能够执行操作。

这些是 Java API 提供的一些数据结构,你可以在你的并发应用程序中使用:

  • ConcurrentLinkedDeque:这是一个基于链节点的非阻塞数据结构,允许你在结构的开始或末尾插入数据。

  • LinkedBlockingDeque:这是一个基于链节点的阻塞数据结构。它可以具有固定容量。你可以在结构的开始或末尾插入元素。它提供了操作,如果未立即执行,将阻塞线程,直到你能够执行操作。

  • ConcurrentLinkedQueue:这是一个非阻塞队列,允许您在队列末尾插入元素并从其开头取出元素。

  • ArrayBlockingQueue:这是一个固定大小的阻塞队列。您可以在队列的末尾插入元素,并从其开头取出元素。它提供了操作,如果因为队列已满或为空而没有执行,则将线程休眠,直到您能够执行操作。

  • LinkedBlockingQueue:这是一个允许您在队列末尾插入元素并从其开头取出元素的阻塞队列。它提供了操作,如果因为队列已满或为空而没有执行,则将线程休眠,直到您能够执行操作。

  • DelayQueue:这是一个带有延迟元素的LinkedBlockingQueue队列。每个插入到该队列的元素都必须实现Delayed接口。一个元素不能从列表中移除,直到其延迟时间为 0。

  • LinkedTransferQueue:这是一个阻塞队列,提供在可以表示为生产者/消费者问题的场景中工作的操作。它提供了操作,如果因为队列已满或为空而没有执行,则将线程休眠,直到您能够执行操作。

  • PriorityBlockingQueue:这是一个基于优先级对元素进行排序的阻塞队列。所有插入到该队列的元素都必须实现Comparable接口。compareTo()方法返回的值将确定元素在队列中的位置。就像所有阻塞数据结构一样,它提供了操作,如果立即执行,则将线程休眠,直到您能够执行操作。

  • SynchronousQueue:这是一个阻塞队列,其中每个insert操作都必须等待另一个线程的remove操作。这两个操作必须同时进行。

  • ConcurrentHashMap:这是一个允许并发操作的自定义HashMap。它是一个非阻塞的数据结构。

  • ConcurrentSkipListMap:此数据结构将键与值关联。每个键只能有一个值。它以有序方式存储键并提供方法来查找元素和从映射中获取一些元素。它是一个非阻塞数据结构。

还有更多...

如果您需要在您的并发应用程序中使用数据结构,请查看 Java API 文档以找到最适合您需求的数据结构。实现您自己的并发数据结构,它存在以下问题:

  • 它们具有复杂的内部结构

  • 您必须考虑许多不同的情况

  • 您必须设计大量的测试来确保其正确性

如果您找不到完全符合您需求的数据结构,请尝试扩展现有的并发数据结构之一,以适当地实现您的问题。

参见

  • 第七章中的食谱,并发集合

使用懒加载时的注意事项

懒加载是一种常见的编程技术,它将对象创建推迟到第一次需要时。这通常会导致对象初始化是在操作的实现中而不是在类的构造函数中进行的。这种技术的优点是您可以节省内存。这是因为您只为应用程序的执行创建必需的对象。您可以在一个类中声明很多对象,但在程序的每次执行中,您并不使用每个对象;因此,您的应用程序不会使用在程序执行中不使用的对象的内存。这种优势对于在资源有限的环境中运行的应用程序非常有用。

相比之下,这种技术在第一次在操作中使用对象时创建对象,可能会在应用程序中引起性能问题。

如果您在并发应用程序中使用此技术,它也可能引发问题。由于一次可以有多个线程执行操作,它们可以在同一时间创建对象,这种情况可能会出现问题。这对于单例类尤为重要。应用程序只有一个这些类的对象,如前所述,并发应用程序可以创建多个对象。考虑以下代码:

    public static DBConnection getConnection(){ 
      if (connection==null) { 
        connection=new DBConnection(); 
      } 
      return connection; 
    }

这是单例类中获取该类在应用程序中存在的唯一对象引用的典型方法,使用懒加载初始化。如果对象尚未创建,则创建该对象。最后,它总是返回它。

如果两个或多个线程同时执行第一句话的比较(connection == null),它们都会创建一个 Connection 对象。这不是一个理想的情况。

在这个菜谱中,你将实现一个优雅的懒初始化问题的解决方案。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,例如 NetBeans,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为 DBConnectionOK 的类:
        public class DBConnectionOK {

  1. 声明一个 private 构造函数。写上执行它的线程名称:
        private DBConnectionOK() { 
          System.out.printf("%s: Connection created.\n",
                            Thread.currentThread().getName()); 
        }

  1. 声明一个名为 LazyDBConnectionOKprivate static 类。它有一个名为 INSTANCEprivate static final DBConnectionOK 实例:
        private static class LazyDBConnection { 
          private static final DBConnectionOK INSTANCE = new
                                                   DBConnectionOK(); 
        }

  1. 实现 getConnection() 方法。它不接受任何参数,并返回一个 DBConnectionOK 对象。它返回 INSTANCE 对象:
        public static DBConnectionOK getConnection() { 
          return LazyDBConnection.INSTANCE; 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口。实现 run() 方法。调用 DBConnectionOK() 方法的 getConnection() 方法:
        public class Task implements Runnable { 

          @Override 
          public void run() { 

            System.out.printf("%s: Getting the connection...\n",
                              Thread.currentThread().getName()); 
            DBConnectionOK connection=DBConnectionOK.getConnection(); 
            System.out.printf("%s: End\n",
                              Thread.currentThread().getName()); 
          } 

        }

  1. 通过创建一个名为 Main 的类并添加 main() 方法来实现示例的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建 20 个 Task 对象和 20 个线程来执行它们:
            for (int i=0; i<20; i++){ 
              Task task=new Task(); 
              Thread thread=new Thread(task); 
              thread.start(); 
            } 
          }

它是如何工作的...

示例的关键是 getConnection() 方法以及 private static class LazyDBConnection 实例。当第一个线程调用 getConnection() 方法时,LazyDBConnection 类通过调用 DBConnection 类的构造函数来初始化 INSTANCE 对象。这个对象由 getConnection() 方法返回。当其他线程调用 getConnection() 方法时,对象已经创建,所以所有线程都使用只创建一次的同一个对象。

当你运行示例时,你会看到 20 个任务的开始和结束消息,但只有一个创建消息。

使用 fork/join 框架而不是执行器

Executors 允许你避免创建和管理线程。你通过实现 RunnableCallable 接口来执行任务,并将它们发送到执行器。它有一个线程池,并使用其中的一个来执行任务。

Java 7 提供了一种新的 executor,即 fork/join 框架。这个在 ForkJoinPool 类中实现的 executor,旨在解决可以使用划分和征服技术划分为更小部分的问题。当你为 fork/join 框架实现任务时,你必须检查你要解决的问题的大小。如果它大于预定义的大小,你将问题划分为两个或更多子类别,并创建与划分次数相等的子任务。任务使用 fork() 操作将这些子任务发送到 ForkJoinPool 类,并使用 join() 操作等待它们的最终化。

对于这类问题,fork/join 池比经典执行器有更好的性能。在这个菜谱中,你将实现一个示例,以检查这个点。

准备工作

这个菜谱的示例是使用 Eclipse IDE 实现的。如果你使用 Eclipse 或其他 IDE,例如 NetBeans,打开它并创建一个新的 Java 项目。

如何做...

按照以下步骤实现示例:

  1. 创建一个名为 TaskFJ 的类,并指定它扩展 RecursiveAction 类:
        public class TaskFJ extends RecursiveAction {

  1. 声明一个名为 array 的私有 int 数组:
        private final int array[];

  1. 声明两个名为 startend 的私有 int 属性:
        private final int start, end;

  1. 实现类的构造函数以初始化其属性:
        public TaskFJ(int array[], int start, int end) { 
          this.array=array; 
          this.start=start; 
          this.end=end; 
        }

  1. 实现 compute() 方法。如果这个任务需要处理超过 1,000 个元素的块(由 startend 属性确定),创建两个 TaskFJ 对象,使用 fork() 方法将它们发送到 ForkJoinPool 类,并使用 join() 方法等待它们的最终化:
        @Override 
        protected void compute() { 
          if (end-start>1000) { 
            int mid=(start+end)/2; 
            TaskFJ task1=new TaskFJ(array,start,mid); 
            TaskFJ task2=new TaskFJ(array,mid,end); 
            task1.fork(); 
            task2.fork(); 
            task1.join(); 
            task2.join();

  1. 否则,增加这个任务需要处理的元素。在每次增加操作后,让线程休眠 1 毫秒:
        } else { 
          for (int i=start; i<end; i++) { 
            array[i]++; 
            try { 
              TimeUnit.MILLISECONDS.sleep(1); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 创建一个名为 Task 的类,并指定它实现 Runnable 接口:
        public class Task implements Runnable {

  1. 声明一个名为 array 的私有 int 数组:
        private final int array[];

  1. 实现类的构造函数以初始化其属性:
        public Task(int array[]) { 
          this.array=array; 
        }

  1. 实现一个run()方法。增加数组中所有元素。在每次增加操作后,让线程休眠 1 毫秒:
        @Override 
        public void run() { 
          for (int i=0; i<array.length; i++ ){ 
            array[i]++; 
            try { 
              TimeUnit.MILLISECONDS.sleep(1); 
            } catch (InterruptedException e) { 
              e.printStackTrace(); 
            } 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现示例的主类:
        public class Main { 

          public static void main(String[] args) {

  1. 创建一个包含 100,000 个元素的int数组:
        int array[]=new int[100000];

  1. 创建一个Task对象和一个ThreadPoolExecutor对象并执行它们。通过控制任务运行的时间来执行任务:
        Task task=new Task(array); 
        ExecutorService executor=Executors.newCachedThreadPool(); 

        Date start,end; 
        start=new Date(); 
        executor.execute(task); 
        executor.shutdown(); 
        try { 
          executor.awaitTermination(1, TimeUnit.DAYS); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 
        end=new Date(); 
        System.out.printf("Main: Executor: %d\n",
                          (end.getTime()-start.getTime()));

  1. 创建一个TaskFJ对象和一个ForkJoinPool对象并执行它们。通过控制任务运行的时间来执行任务:
          TaskFJ taskFJ=new TaskFJ(array,1,100000); 
          ForkJoinPool pool=new ForkJoinPool(); 
          start=new Date(); 
          pool.execute(taskFJ); 
          pool.shutdown(); 
          try { 
            pool.awaitTermination(1, TimeUnit.DAYS); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
          end=new Date(); 
          System.out.printf("Core: Fork/Join: %d\n",
                            (end.getTime()-start.getTime())); 
        }

它是如何工作的...

当你执行示例时,你会看到ForkJoinPoolTaskFJ类比ThreadPoolExecutorTask类有更好的性能。

如果你必须解决一个可以使用分而治之技术分割的问题,请使用ForkJoinPool类而不是ThreadPoolExecutor类。你将获得更好的性能。

参见

  • 本章的将线程管理委托给执行器食谱

避免在锁内部使用阻塞操作

阻塞操作是那些在事件发生之前阻止当前线程执行的操作。典型的阻塞操作包括与控制台、文件或网络的输入或输出操作。

如果你在一个锁的临界区内部使用阻塞操作,你会降低应用程序的性能。当一个线程正在等待完成阻塞操作的事件时,应用程序的其余部分可能也在等待相同的事件;然而,其他线程将无法访问临界区并执行其代码(临界区的代码)。

在本食谱中,你将实现这种情况的一个示例。线程在临界区内部从控制台读取一行。这条指令使得应用程序的其他线程将被阻塞,直到用户输入该行。

准备工作

本食谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何操作...

按照以下步骤实现示例:

  1. 创建一个名为Task的类并指定它实现Runnable接口:
        public class Task implements Runnable {

  1. 声明一个名为lock的私有Lock属性:
        private final Lock lock;

  1. 实现类的构造函数以初始化其属性:
        public Task (Lock lock) { 
          this.lock=lock; 
        }

  1. 实现一个run()方法:
        @Override 
        public void run() { 
          System.out.printf("%s: Starting\n",
                            Thread.currentThread().getName());

  1. 使用lock()方法获取锁:
        lock.lock();

  1. 调用criticalSection()方法:
        try { 
          criticalSection();

  1. 从控制台读取一行:
          System.out.printf("%s: Press a key to continue: \n",
                            Thread.currentThread().getName()); 
          InputStreamReader converter = new InputStreamReader
                                                    (System.in); 
          BufferedReader in = new BufferedReader(converter); 
          String line=in.readLine(); 
        } catch (IOException e) { 
          e.printStackTrace();

  1. 使用finally部分中的unlock()方法释放锁:
          } finally {          
            lock.unlock(); 
          } 
        }

  1. 实现一个criticalSection()方法。等待一个随机的时间段:
        private void criticalSection() { 
          Random random=new Random(); 
          int wait=random.nextInt(10); 
          System.out.printf("%s: Wait for %d seconds\n",
                            Thread.currentThread().getName(),wait); 
          try { 
            TimeUnit.SECONDS.sleep(wait); 
          } catch (InterruptedException e) { 
            e.printStackTrace(); 
          } 
        }

  1. 通过创建一个名为Main的类并添加main()方法来实现应用程序的主类:
        public class Main { 
          public static void main(String[] args) {

  1. 创建一个名为lock的新ReentrantLock对象。创建 10 个Task对象和 10 个线程来执行它们:
        ReentrantLock lock=new ReentrantLock(); 
        for (int i=0; i<10; i++) { 
          Task task=new Task(lock); 
          Thread thread=new Thread(task); 
          thread.start(); 
        }

它是如何工作的...

当您执行此示例时,10 个线程开始执行,但只有一个进入临界区,该临界区在run()方法中实现。由于每个任务在释放锁之前都会从控制台读取一行文本,因此所有应用程序都将被阻塞,直到您在控制台中输入文本。

参见

  • 尽可能短地持有锁的技巧

避免使用已弃用的方法

Java 并发 API 也有一些已弃用的操作。这些是包含在 API 的第一个版本中的操作,但现在您不应使用它们。它们已被其他操作所取代,这些操作实现了比原始操作更好的实践。

最关键的已弃用操作是那些由Thread类提供的。这些操作包括:

  • destroy(): 在过去,此方法销毁线程。实际上,它抛出NoSuchMethodError异常。

  • suspend(): 此方法将线程的执行挂起,直到它被恢复。

  • stop(): 此方法强制线程完成其执行。

  • resume(): 此方法恢复线程的执行。

ThreadGroup类也有一些已弃用的方法,如下所示:

  • suspend(): 此方法将此线程组中属于此线程的所有线程的执行挂起

  • stop(): 此方法强制此线程组中所有线程的执行完成

  • resume(): 此方法恢复此线程组中所有线程的执行

stop()操作已被弃用,因为它可能引发不一致的错误。因为它强制线程完成其执行,您可能会遇到线程在操作完成之前完成其执行,并可能使数据处于不一致状态的情况。例如,如果您有一个正在修改银行账户的线程,并且在完成之前被停止,那么银行账户可能包含错误数据。

stop()操作也可能导致死锁情况。如果在线程执行由同步机制(例如,锁)保护的临界区时调用此操作,则此同步机制将继续阻塞,并且没有线程能够进入临界区。这就是为什么suspend()resume()操作已被弃用的原因。

如果您需要这些操作的替代方案,可以使用一个内部属性来存储线程的状态。此属性必须使用同步访问进行保护,或者使用原子变量。您必须检查此属性值并根据它采取行动。请注意,您必须避免数据不一致和死锁情况,以确保应用程序的正确运行。

使用执行器而不是线程组

ThreadGroup类提供了一个机制,可以将线程组织成层次结构,这样你就可以通过一个调用对属于线程组的所有线程执行操作。默认情况下,所有线程都属于同一个组,但当你创建线程时可以指定不同的组。

无论如何,线程组不提供任何使其使用有趣的功能:

  • 你必须创建线程并管理它们的状态

  • 控制线程组所有线程状态的这些方法已被弃用,并建议不要使用。

如果你需要将线程分组到一个共同的架构下,最好使用Executor实现,例如ThreadPoolExecutor。它提供了更多功能,具体如下:

  • 你不必担心线程的管理。执行器创建并重用线程以节省执行资源。

  • 你可以通过实现RunnableCallable接口来实施你的并发任务。Callable接口允许你实现返回结果的任务,这比传统任务提供了很大的优势。

  • 当你向执行器发送任务时,它返回一个Future对象,允许你轻松地知道任务的状态以及如果它已经完成执行,则返回的结果。

  • 你可以使用ScheduledThreadPoolExecutor类实现的特殊执行器来安排任务并重复执行它们。

  • 你可以轻松控制执行器使用的资源。你可以设置池中线程的最大数量,这样你的执行器一次就不会运行超过那么多任务。

由于这些原因,最好你不使用线程组而使用执行器。

参见

  • 本章中关于将线程管理委托给执行器的配方

使用流处理大数据集

Stream接口是一系列可以按顺序或并行过滤和转换以获得最终结果的元素序列。这个最终结果可以是原始数据类型(整数、长整型等)、对象或数据结构。这些是更好地定义Stream的特征:

  • 流是一系列数据,而不是数据结构。

  • 你可以从不同的源创建流,如集合(列表、数组等)、文件、字符串或提供流元素的类。

  • 你不能访问流中的单个元素。

  • 你不能修改流的源。

  • 流定义了两种类型的操作:中间操作,它产生一个新的Stream接口,允许你转换、过滤、映射或排序流中的元素,以及终端操作,它生成操作的最终结果。流管道由零个或多个中间操作和一个最终操作组成。

  • 中间操作是懒执行的。它们不会在终端操作开始执行之前执行。如果 Java 检测到中间操作不会影响操作最终结果,它可以避免对流中的元素或元素集合执行中间操作。

当你需要以并发方式实现处理大量数据的操作时,你可以使用Java 并发 API的不同元素来实现它。你可以将 Java 线程分配给fork/join 框架Executor 框架,但我认为并行流是最佳选择。在这个菜谱中,我们将实现一个示例来解释使用并行流提供的优势。

准备工作

本菜谱的示例已使用 Eclipse IDE 实现。如果你使用 Eclipse 或 NetBeans 等其他 IDE,请打开它并创建一个新的 Java 项目。

如何做到这一点...

按照以下步骤实现示例:

  1. 创建一个名为Person的类。这个类将有六个属性来定义一个人的基本特征。我们将实现获取和设置属性值的方法,但它们将不包括在这里:
        public class Person { 
          private int id; 
          private String firstName; 
          private String lastName; 
          private Date birthDate; 
          private int salary; 
          private double coeficient;

  1. 现在,实现一个名为PersonGenerator的类。这个类将只有一个名为generatedPersonList()的方法,用于生成具有指定参数大小的随机Person对象列表。这是该类的源代码:
        public class PersonGenerator { 
          public static List<Person> generatePersonList (int size) { 
            List<Person> ret = new ArrayList<>(); 

            String firstNames[] = {"Mary","Patricia","Linda",
                                   "Barbara","Elizabeth","James",
                                   "John","Robert","Michael","William"}; 
            String lastNames[] = {"Smith","Jones","Taylor",
                                  "Williams","Brown","Davies",
                                  "Evans","Wilson","Thomas","Roberts"}; 

            Random randomGenerator=new Random(); 
            for (int i=0; i<size; i++) { 
              Person person=new Person(); 
              person.setId(i); 
              person.setFirstName(firstNames
                                       [randomGenerator.nextInt(10)]); 
              person.setLastName(lastNames
                                     [randomGenerator.nextInt(10)]); 
              person.setSalary(randomGenerator.nextInt(100000)); 
              person.setCoeficient(randomGenerator.nextDouble()*10); 
              Calendar calendar=Calendar.getInstance(); 
              calendar.add(Calendar.YEAR, -randomGenerator
                                                     .nextInt(30)); 
              Date birthDate=calendar.getTime(); 
              person.setBirthDate(birthDate); 

              ret.add(person); 
            } 
            return ret; 
          } 
        }

  1. 现在,实现一个名为PersonMapTask的任务。这个任务的主要目的是将人员列表转换为映射,其中键将是人员的姓名,值将是具有与键相同名称的Person对象的列表。我们将使用 fork/join 框架来实现这种转换,因此PersonMapTask将扩展RecursiveAction类:
        public class PersonMapTask extends RecursiveAction {

  1. PersonMapTask类将有两个私有属性:要处理的Person对象列表和用于存储结果的ConcurrentHashMap。我们将使用类的构造函数来初始化这两个属性:
        private List<Person> persons; 
        private ConcurrentHashMap<String, ConcurrentLinkedDeque
                                                <Person>> personMap; 

        public PersonMapTask(List<Person> persons, ConcurrentHashMap
                   <String, ConcurrentLinkedDeque<Person>> personMap) { 
          this.persons = persons; 
          this.personMap = personMap; 
        }

  1. 现在是时候实现compute()方法了。如果列表少于 1,000 个元素,我们将处理元素并将它们插入到ConcurrentHashMap中。我们将使用computeIfAbsent()方法获取与键关联的List或如果键不存在于映射中,则生成一个新的ConcurrentMapedDeque对象:
        protected void compute() { 

          if (persons.size() < 1000) { 

            for (Person person: persons) { 
              ConcurrentLinkedDeque<Person> personList=personMap
                     .computeIfAbsent(person.getFirstName(), name -> { 
              return new ConcurrentLinkedDeque<>(); 
              }); 

              personList.add(person); 
            } 
            return; 
          }

  1. 如果List有超过 1,000 个元素,我们将创建两个子任务并将列表的一部分处理过程委托给它们:
          PersonMapTask child1, child2; 

          child1=new PersonMapTask(persons.subList(0,persons.size()/2),
                                   personMap); 
          child2=new PersonMapTask(persons.subList(persons.size()/2,
                                                   persons.size()),
                                   personMap); 

            invokeAll(child1,child2);   
          } 
        }

  1. 最后,实现带有main()方法的Main类。首先,生成一个包含 100,000 个随机Person对象的列表:
        public class Main { 

          public static void main (String[] args) { 
            List<Person> persons=PersonGenerator
                                        .generatePersonList(100000);

  1. 然后,比较两种方法来生成以名称作为键、Person作为值的Map。列表将使用并行Stream函数和collect()方法使用groupingByConcurrent()收集器:
        Date start, end; 

        start =  new Date(); 
        Map<String, List<Person>> personsByName = persons
                                                  .parallelStream() 
        .collect(Collectors.groupingByConcurrent(p -> p
                                                   .getFirstName())); 
        end = new Date(); 
        System.out.printf("Collect: %d - %d\n", personsByName.size(),
                          end.getTime()-start.getTime());

  1. 第二种选择是使用 fork/join 框架和PersonMapTask类:
            start = new Date(); 
            ConcurrentHashMap<String, ConcurrentLinkedDeque<Person>>
                          forkJoinMap=new ConcurrentHashMap<>(); 
            PersonMapTask personMapTask=new PersonMapTask
                                            (persons,forkJoinMap); 
            ForkJoinPool.commonPool().invoke(personMapTask); 
            end = new Date(); 

            System.out.printf("Collect ForkJoinPool: %d - %d\n",
                              forkJoinMap.size(),
                              end.getTime()-start.getTime()); 
          } 
        }

它是如何工作的...

在这个菜谱中,我们实现了从ListMap的同一算法的两个不同版本。如果你执行它,你会得到相同的结果和相似的执行时间(至少在我用四核计算机执行示例时,后者是正确的)。我们使用流获得的最大优势是解决方案的简单性和其开发时间。我们只用一行代码就实现了解决方案。而在另一种情况下,我们使用并发数据结构实现了一个新的类(PersonMapTask),然后在 fork/join 框架中执行它。

使用流,你可以将你的算法分解成简单的步骤,这些步骤可以用优雅的方式表达,易于编程和理解。

参见

  • 第六章中的从不同来源创建流减少流元素排序流元素的菜谱,并行和反应流

其他技巧和窍门

在这个最后的菜谱中,我们包括了本章其他菜谱中没有包含的其他技巧和窍门:

  • 在可能的情况下,使用并发设计模式:在软件工程中,设计模式是解决常见问题的方案。它们在软件开发和并发应用中普遍使用,并不例外。如信号量、 rendezvous 和互斥锁等模式定义了如何在具体情况下实现并发应用程序,并且它们已被用于实现并发工具。

  • 在尽可能高的级别上实现并发:丰富的线程 API,如 Java 并发 API,为你提供了不同的类来实现应用程序中的并发。尽量使用那些提供更高抽象级别的类。这将使你更容易实现你的算法,并且它们被优化以提供比直接使用线程更好的性能。因此,性能不会成为问题。

  • 考虑可伸缩性:当你实现一个并发算法时,主要目标之一是利用你计算机的所有资源,特别是处理器或核心的数量。但这个数字可能会随时间变化。当你设计一个并发算法时,不要预设你的应用程序将要执行的核心或处理器的数量。动态获取系统信息。例如,在 Java 中,你可以使用Runtime.getRuntime().availableProcessors()方法来获取它,并让你的算法使用这些信息来计算它将要执行的任务数量。

  • 在可能的情况下,优先使用局部线程变量而不是静态和共享变量:线程局部变量是一种特殊的变量。每个任务都将为这个变量有一个独立的值,因此你不需要任何同步机制来保护对它的访问。

参见

  • 本章所有菜谱
posted @ 2025-09-10 15:06  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报