Skip to content

Latest commit

 

History

History
1689 lines (1175 loc) · 68.7 KB

File metadata and controls

1689 lines (1175 loc) · 68.7 KB

三、线程同步工具

在本章中,我们将介绍:

  • 控制对资源的并发访问
  • 控制对资源的多个副本的并发访问
  • 等待多个并发事件
  • 在公共点中同步任务
  • 运行并发分阶段任务
  • 控制并发阶段性任务中的阶段变化
  • 在并发任务之间更改数据

导言

第 2 章基本线程同步中,我们学习了同步和临界段的概念。基本上,当多个并发任务共享一个资源(例如,对象或对象的属性)时,我们讨论同步。访问此共享资源的代码块称为关键部分。

如果不使用适当的机制,可能会出现错误的结果、数据不一致或错误情况,因此我们必须采用 Java 语言提供的一种同步机制来避免所有这些问题。

第 2 章基本线程同步向我们介绍了以下基本同步机制:

  • synchronized关键字
  • Lock接口及其实现类:ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock

在本章中,我们将学习如何使用高级机制实现多线程的同步。这些高级别机制如下:

  • 信号量:信号量是控制访问一个或多个共享资源的计数器。这种机制是并发编程的基本工具之一,由大多数编程语言提供。
  • 倒计时闩锁CountDownLatch类是 Java 语言提供的一种机制,允许线程等待多个操作的完成。
  • CyclicBarrierCyclicBarrier类是 Java 语言提供的另一种机制,允许在一个公共点上同步多个线程。
  • PhaserPhaser类是 Java 语言提供的另一种机制,用于控制分阶段执行的并发任务。所有线程必须先完成一个阶段,然后才能继续下一个阶段。这是 Java7API 的一个新特性。
  • 交换机Exchanger类是 Java 语言提供的另一种机制,提供两个线程之间的数据交换点。

信号量是一种通用的同步机制,您可以使用它来保护任何问题中的任何关键部分。其他机制被认为用于具有特定功能的应用中,如前所述。请确保根据应用的特点选择适当的机制。

本章介绍七种配方,向您展示如何使用所描述的机制。

控制对资源的并发访问

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

信号量的概念由 Edsger Dijkstra 于 1965 年引入,并首次在 THEOS 操作系统中使用。

当线程想要访问其中一个共享资源时,首先,它必须获取信号量。如果信号量的内部计数器大于0,则信号量将计数器递减,并允许访问共享资源。大于0的计数器意味着可以使用空闲资源,因此线程可以访问和使用其中的一个。

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

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

在本教程中,您将学习如何使用Semaphore类实现称为二进制信号量的特殊类型的信号量。这些类型的信号量保护对唯一共享资源的访问,因此信号量的内部计数器只能接受值10。为了演示如何使用它,您将实现一个打印队列,并发任务可以使用该队列打印其作业。这个打印队列将受到二进制信号量的保护,因此一次只能打印一个线程。

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

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

    public class PrintQueue {
  2. 声明一个Semaphore对象。叫它semaphore

      private final Semaphore semaphore;
  3. 实现类的构造函数。它初始化将保护打印队列访问权限的semaphore对象。

      public PrintQueue(){
        semaphore=new Semaphore(1);
      }
  4. 实现模拟文档打印的printJob()方法。它接收被调用的Object作为参数。

      public void printJob (Object document){
  5. 在方法内部,首先必须获取调用acquire()方法的信号量。这个方法可以抛出一个InterruptedException异常,所以您必须包含一些代码来处理它。

        try {
          semaphore.acquire();
  6. 然后,实现模拟等待随机时间段的文档打印的行。

      long duration=(long)(Math.random()*10);
          System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),duration);
          Thread.sleep(duration);    
  7. 最后,通过调用信号量的release()方法释放信号量。

        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          semaphore.release();      
        }
  8. 创建一个名为Job的类,并指定它实现Runnable接口。此类实现将文档发送到打印机的作业。

    public class Job implements Runnable {
  9. 声明一个PrintQueue对象。叫它printQueue

      private PrintQueue printQueue;
  10. 实现类的构造函数。它初始化类中声明的PrintQueue对象。

```java
  public Job(PrintQueue printQueue){
    this.printQueue=printQueue;
  }
```
  1. 执行run()方法。
```java
  @Override
   public void run() {
```
  1. 首先,该方法向控制台写入一条消息,显示作业已开始执行。
```java
    System.out.printf("%s: Going to print a job\n",Thread.currentThread().getName());
```
  1. 然后调用PrintQueue对象的printJob()方法。
```java
    printQueue.printJob(new Object());
```
  1. 最后,该方法向控制台写入一条消息,表明其已完成执行。
```java
    System.out.printf("%s: The document has been printed\n",Thread.currentThread().getName());        
  }
```
  1. 通过创建一个名为Main的类来实现示例的主类,并实现main()方法。
```java
public class Main {

  public static void main (String args[]){
```
  1. 创建一个名为printQueuePrintQueue对象。
```java
    PrintQueue printQueue=new PrintQueue();
```
  1. 创建 10 个线程。这些线程中的每一个都将执行一个Job对象,该对象将向打印队列发送一个文档。
```java
    Thread thread[]=new Thread[10];
    for (int i=0; i<10; i++){
      thread[i]=new Thread(new Job(printQueue),"Thread"+i);
    }
```
  1. 最后,启动 10 个线程。
```java
    for (int i=0; i<10; i++){
      thread[i].start();
    }
```

它是如何工作的。。。

这个示例的关键在于PrintQueue类的printJob()方法。此方法显示了使用信号量实现关键部分并保护对共享资源的访问时必须遵循的三个步骤:

  1. 首先,使用acquire()方法获取信号量。
  2. 然后,使用共享资源执行必要的操作。
  3. 最后,使用release()方法释放信号量。

本例中的另一个要点是PrintQueue类的构造函数和Semaphore对象的初始化。您将值1作为此构造函数的参数传递,因此您正在创建一个二进制信号量。内部计数器的初始值为1,因此您将保护对一个共享资源的访问,在本例中为打印队列。

当您启动 10 个线程时,第一个线程获取信号量并获得对关键部分的访问权。其余的被信号量阻塞,直到获取它的线程释放它。当这种情况发生时,信号量选择一个等待的线程,并允许它访问关键部分。所有的工作都打印他们的文档,但是一个接一个。

还有更多。。。

Semaphore类有acquire()方法的两个附加版本:

  • acquireUninterruptibly():采用acquire()方法;当信号量的内部计数器为0时,阻塞线程,直到信号量释放。在此阻塞时间内,线程可能被中断,然后此方法抛出一个InterruptedException异常。此版本的 acquire 操作忽略线程的中断,并且不会引发任何异常。
  • tryAcquire():此方法尝试获取信号量。如果可以,该方法返回true值。但如果不能,该方法将返回false值,而不是被阻止,并等待信号量的释放。您有责任根据return值采取正确的行动。

信号量的公平性

公平性的概念由 Java 语言在所有类中使用,这些类可以阻止各种线程等待同步资源(例如,信号量)的释放。默认模式称为非公平模式。在这种模式下,当同步资源被释放时,将选择一个等待线程来获取该资源,但选择它时没有任何条件。公平模式改变此行为,并强制选择等待时间更长的线程。

与其他类一样,Semaphore类在其构造函数中允许第二个参数。此参数必须采用Boolean值。如果您给它false值,那么您正在创建一个信号量,它将在非公平模式下工作。如果不使用此参数,将得到相同的行为。如果您给它true值,那么您正在创建一个信号量,它将在公平模式下工作。

另见

  • 第 8 章测试并发应用中的监控锁接口配方
  • 第 2 章基本线程同步中的修改锁公平性配方

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

控制对资源的并发访问配方中,您学习了信号量的基础。

在这个配方中,您使用二进制信号量实现了一个示例。这些类型的信号量用于保护对一个共享资源或一次只能由一个线程执行的关键部分的访问。但是,当您需要保护资源的各种副本时,或者当您有一个可以由多个线程同时执行的关键部分时,也可以使用信号量。

在本教程中,您将学习如何使用信号量保护资源的多个副本。您将实现一个示例,其中有一个打印队列,可以在三台不同的打印机中打印文档。

准备好了吗

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

实现本章控制对资源配方的并发访问中描述的示例。

怎么做。。。

按照以下步骤来实现该示例:

  1. 正如我们前面提到的,您将修改用信号量实现的打印队列示例。打开PrintQueue类并声明一个名为freePrintersboolean数组。此阵列存储可自由打印作业的打印机和正在打印文档的打印机。

      private boolean freePrinters[];
  2. 另外,声明一个名为lockPrintersLock对象。您将使用此对象来保护对freePrinters阵列的访问。

      private Lock lockPrinters;
  3. 修改类的构造函数以初始化新声明的对象。freePrinters数组有三个元素,都初始化为true值。信号量的初始值为3

      public PrintQueue(){
        semaphore=new Semaphore(3);
        freePrinters=new boolean[3];
        for (int i=0; i<3; i++){
          freePrinters[i]=true;
        }
        lockPrinters=new ReentrantLock();
      }
  4. 同时修改的printJob()方法。它接收一个名为documentObject作为唯一参数。

      public void printJob (Object document){
  5. 首先,该方法调用acquire()方法获取对信号量的访问权。由于此方法可能引发InterruptedException异常,因此必须包含处理该异常的代码。

        try {
          semaphore.acquire();
  6. 然后,您可以使用专用方法getPrinter()获取分配用于打印此作业的打印机的编号。

          int assignedPrinter=getPrinter();
  7. 然后,实现模拟等待随机时间段的文档打印的行。

          long duration=(long)(Math.random()*10);
          System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds\n",Thread.currentThread().getName(),assignedPrinter,duration);
          TimeUnit.SECONDS.sleep(duration);
  8. 最后,释放调用release()方法的信号量,将使用的打印机标记为空闲,将true分配给freePrinters数组中相应的索引。

          freePrinters[assignedPrinter]=true;
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          semaphore.release();      
        }
  9. 执行getPrinter()方法。它是一个私有方法,返回一个int值,并且没有参数。

      private int getPrinter() {
  10. 首先,声明一个int变量来存储打印机的索引。

```java
    int ret=-1;
```
  1. 然后,获得对lockPrinters对象的访问权。
```java
    try {
      lockPrinters.lock();
```
  1. 然后,在freePrinters数组中找到第一个true值,并将其索引保存在变量中。将此值修改为false,因为此打印机将忙。
```java
    for (int i=0; i<freePrinters.length; i++) {
      if (freePrinters[i]){
        ret=i;
        freePrinters[i]=false;
        break;
      }
    }
```
  1. 最后,释放lockPrinters对象并返回true值的索引。
```java
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      lockPrinters.unlock();
    }
    return ret;
```
  1. JobCore类没有修改。

它是如何工作的。。。

本例的键在PrintQueue类中。Semaphore对象是使用3作为构造函数的参数创建的。调用acquire()方法的前三个线程将获得对本例关键部分的访问,而其余线程将被阻止。当一个线程完成关键部分并释放信号量时,另一个线程将获取它。

在此关键部分中,线程获取分配用于打印此作业的打印机的索引。示例的这一部分用于使示例更加真实,但它不使用任何与信号量相关的代码。

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

How it works...

每个文档都在其中一台打印机中打印。第一个是免费的。

还有更多。。。

acquire()acquireUninterruptibly()tryAcquire()release()方法都有一个附加版本,其中包含一个int参数。这个参数表示使用它们的线程想要获取或释放的许可数量,也就是说,这个线程想要删除或添加到信号量内部计数器的单元数量。在的acquire()acquireUninterruptibly()tryAcquire()方法的情况下,如果此计数器的值小于此值,线程将被阻塞,直到计数器获得此值或更大的值。

另见

  • 第 3 章线程同步工具中的控制对资源配方的并发访问
  • 第 8 章测试并发应用中的监控锁接口配方
  • 第 2 章基本线程同步中的修改锁公平性配方

等待多个并发事件

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

在本食谱中,您将学习如何使用CountDownLatch类实现视频会议系统。视频会议系统将在开始之前等待所有参与者的到来。

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

  1. 创建一个名为Videoconference的类,并指定它实现Runnable接口。本课程将实现视频会议系统。

    public class Videoconference implements Runnable{
  2. 声明一个名为controllerCountDownLatch对象。

      private final CountDownLatch controller;
  3. 实现初始化CountDownLatch属性的类的构造函数。Videoconference类将等待作为参数接收的参与者数量的到来。

      public Videoconference(int number) {
        controller=new CountDownLatch(number);
      }
  4. 执行arrive()方法。每次参与者到达视频会议时都会调用此方法。它接收一个名为nameString类型作为参数。

      public void arrive(String name){
  5. 首先,它使用已接收的参数编写消息。

        System.out.printf("%s has arrived.",name);
  6. 然后调用CountDownLatch对象的countDown()方法。

        controller.countDown();
  7. 最后,它使用CountDownLatch对象的getCount()方法编写另一条包含参与者数量的消息,参与者的到达被挂起。

        System.out.printf("VideoConference: Waiting for %d participants.\n",controller.getCount());
  8. 实现视频会议系统的主要方式。这是每个Runnable对象必须具备的run()方法。

       @Override
      public void run() {
  9. 首先,使用的getCount()方法写一条视频会议参与者人数的消息。

        System.out.printf("VideoConference: Initialization: %d participants.\n",controller.getCount());
  10. 然后,使用await()方法等待所有参与者。由于此方法可能引发InterruptedException异常,因此必须包含处理该异常的代码。

```java
    try {
      controller.await();
```
  1. 最后,写一条消息,表明所有参与者都已到达。
```java
      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接口。本课程代表视频会议的每位参与者。
```java
public class Participant implements Runnable {
```
  1. 声明一个名为conference的私有Videoconference属性。
```java
  private Videoconference conference;
```
  1. 声明一个名为name的私有String属性。
```java
  private String name;
```
  1. 实现初始化这两个属性的类的构造函数。
```java
  public Participant(Videoconference conference, String name) {
    this.conference=conference;
    this.name=name;
  }
```
  1. 执行参与者的run()方法。
```java
   @Override
  public void run() {
```
  1. 首先,让线程随机休眠一段时间。
```java
    long duration=(long)(Math.random()*10);
    try {
      TimeUnit.SECONDS.sleep(duration);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 然后,使用Videoconference对象的arrive()方法表示该参与者的到达。
```java
    conference.arrive(name);
```
  1. 最后,通过创建一个名为Main的类来实现示例的主类,并向其添加main()方法。
```java
public class Main {

  public static void main(String[] args) {
```
  1. 创建一个名为conferenceVideoconference对象,等待 10 名参与者。
```java
    Videoconference conference=new Videoconference(10);
```
  1. 创建Thread以运行此Videoconference对象并启动它。
```java
    Thread threadConference=new Thread(conference);
    threadConference.start();
```
  1. 创建 10 个Participant对象,一个Thread对象来运行每个对象,并启动所有线程。
```java
    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,对其方法的所有调用都无效。如果要再次执行相同的同步,必须创建一个新对象。

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

How it works...

您可以看到最后的参与者是如何到达的,内部计数器到达0后,CountDownLatch对象会唤醒Videoconference对象,该对象会写入指示视频会议应开始的消息。

还有更多。。。

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

  • awaitlong``time,``TimeUnit``unit:线程将处于休眠状态,直到中断为止;CountDownLatch的内部计数器到达0或经过指定的时间。TimeUnit类是一个具有以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

在公共点同步任务

Java 并发 API 提供了一个同步工具,允许在一个确定的点上同步两个或多个线程。是的CyclicBarrier班。这个类类似于本章等待多个并发事件配方中解释的CountDownLatch类,但存在一些差异,使它们成为一个更强大的类。

CyclicBarrier类用一个整数初始化,整数是将在一个确定点上同步的线程数。当其中一个线程到达确定的点时,它调用await()方法等待其他线程。当线程调用该方法时,CyclicBarrier类将阻塞正在休眠的线程,直到其他线程到达。当最后一个线程调用CyclicBarrier类的await()方法时,它会唤醒所有正在等待的线程并继续其作业。

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

在此配方中,您将学习如何使用CyclicBarrier类在确定的点上同步一组线程。您还将使用一个Runnable对象,该对象将在所有线程到达该点后执行。在本例中,您将在数字矩阵中查找数字。矩阵将被划分为子集(使用分治技术),因此每个线程将在一个子集中查找数字。一旦所有线程都完成了它们的工作,最后一个任务将统一它们的结果。

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

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

    public class MatrixMock {
  2. 声明一个名为dataprivate``int矩阵。

      private int data[][];
  3. 实现类的构造函数。此构造函数将接收矩阵的行数、每行的长度以及我们将要查找的作为参数的数量。三个参数均为int型。

      public MatrixMock(int size, int length, int number){
  4. 初始化构造函数中使用的变量和对象。

        int counter=0;
        data=new int[size][length];
        Random random=new Random();
  5. 用随机数填充矩阵。每次生成一个数字时,将其与要查找的数字进行比较。如果它们相等,则递增计数器。

        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++;
            }
          }
        }
  6. 最后,在控制台中打印一条消息,显示要在生成的矩阵中查找的数字的出现次数。此消息将用于检查线程是否得到正确的结果。

        System.out.printf("Mock: There are %d ocurrences of number in generated data.\n",counter,number);
  7. 执行getRow()方法。此方法接收一个带有矩阵中行数的int参数,如果该行存在,则返回该行;如果该行不存在,则返回null

      public int[] getRow(int row){
        if ((row>=0)&&(row<data.length)){
          return data[row];
        }
        return null;
      }
  8. 现在,实现一个名为Results的类。此类将在数组中存储矩阵每行中搜索的数字的出现次数。

    public class Results {
  9. 声明一个名为data的私有 int数组。

      private int data[];
  10. 实现类的构造函数。此构造函数接收一个带有数组元素数的整数参数。

```java
  public Results(int size){
    data=new int[size];
  }
```
  1. 执行setData()方法。该方法接收数组中的一个位置和一个值作为参数,并在数组中建立该位置的值。
```java
  public void  setData(int position, int value){
    data[position]=value;
  }
```
  1. getData()实现方法。此方法返回带有结果数组的数组。
```java
  public int[] getData(){
    return data;
  }
```
  1. 现在您有了辅助类,是时候实现线程了。首先,实现Searcher类。此类将在随机数矩阵的确定行中查找一个数。创建一个名为Searcher的类,并指定它实现Runnable接口。
```java
public class Searcher implements Runnable {
```
  1. 声明两个名为firstRowlastRow的私有int属性。这两个属性将确定此对象将查找的行的子集。
```java
  private int firstRow;

  private int lastRow;
```
  1. 声明名为mock的私有MatrixMock属性。
```java
  private MatrixMock mock;
```
  1. 声明名为results的私有Results属性。
```java
  private Results results;
```
  1. 声明一个名为number的私有int属性,该属性将存储我们要查找的号码。
```java
  private int number;
```
  1. 声明一个名为barrierCyclicBarrier对象。
```java
  private final CyclicBarrier barrier;
```
  1. 实现初始化之前声明的所有属性的类的构造函数。
```java
  public Searcher(int firstRow, int lastRow, NumberMock mock, Results results, int number, CyclicBarrier barrier){
    this.firstRow=firstRow;
    this.lastRow=lastRow;
    this.mock=mock;
    this.results=results;
    this.number=number;
    this.barrier=barrier;
  }
```
  1. 执行搜索号码的run()方法。它使用一个名为counter的内部变量,该变量将存储该数字在每行中出现的次数。
```java
   @Override
  public void run() {
    int counter;
```
  1. 在控制台中使用分配给此对象的行打印消息。
```java
    System.out.printf("%s: Processing lines from %d to %d.\n",Thread.currentThread().getName(),firstRow,lastRow);
```
  1. 处理分配给该线程的所有行。对于每一行,计算您正在搜索的数字的出现次数,并将该数字存储在Results对象的相应位置。
```java
    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. 在控制台中打印一条消息,指示此对象已完成搜索。
```java
    System.out.printf("%s: Lines processed.\n",Thread.currentThread().getName());        
```
  1. 调用CyclicBarrier对象的await()方法,并添加必要的代码来处理该方法可以抛出的InterruptedExceptionBrokenBarrierException异常。
```java
    try {
      barrier.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (BrokenBarrierException e) {
      e.printStackTrace();
    }
```
  1. 现在,实现一个类,该类计算该数字在矩阵中出现的总数。它使用Results对象来进行计算,该对象存储矩阵每行中的数字的出现次数。创建一个名为Grouper的类,并指定它实现Runnable接口。
```java
public class Grouper implements Runnable {
```
  1. 声明名为results的私有Results属性。
```java
  private Results results;
```
  1. 实现初始化Results属性的类的构造函数。
```java
  public Grouper(Results results){
    this.results=results;
  }
```
  1. 执行run()方法,该方法将计算结果数组中该数字的出现总数。
```java
   @Override
  public void run() {
```
  1. 声明一个int变量,并向控制台写入一条消息以指示流程的开始。
```java
    int finalResult=0;
    System.out.printf("Grouper: Processing results...\n");
```
  1. 使用results对象的getData()方法获取该数字在每行中出现的次数。然后,处理数组中的所有元素,并将它们的值添加到finalResult变量中。
```java
    int data[]=results.getData();
    for (int number:data){
      finalResult+=number;
    }
```
  1. 在控制台中打印结果。
```java
    System.out.printf("Grouper: Total result: %d.\n",finalResult);
```
  1. 最后,通过创建一个名为Main的类来实现示例的主类,并向其添加main()方法。
```java
public class Main {

  public static void main(String[] args) {
```
  1. 声明并初始化五个常量来存储应用的参数。
```java
    final int ROWS=10000;
    final int NUMBERS=1000;
    final int SEARCH=5; 
    final int PARTICIPANTS=5;
    final int LINES_PARTICIPANT=2000;
```
  1. 创建一个名为mockMatrixMock对象。它将有 10000 行 1000 个元素。现在,您要搜索数字 5。
```java
    MatrixMock mock=new MatrixMock(ROWS, NUMBERS,SEARCH);
```
  1. 创建一个名为resultsResults对象。它将有 10000 个元素。
```java
    Results results=new Results(ROWS);
```
  1. 创建一个名为grouperGrouper对象。
```java
    Grouper grouper=new Grouper(results);
```
  1. 创建一个名为barrierCyclicBarrier对象。此对象将等待五个线程。当此线程完成时,它将执行先前创建的Grouper对象。
```java
    CyclicBarrier barrier=new CyclicBarrier(PARTICIPANTS,grouper);
```
  1. 创建五个Searcher对象,五个线程执行它们,并启动五个线程。
```java
    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");
```

它是如何工作的。。。

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

How it works...

示例中解决的问题很简单。我们有一个大的随机整数矩阵,你想知道这个矩阵中某个数字出现的总数。为了获得更好的性能,我们使用了分治技术。我们将矩阵划分为五个子集,并使用线程查找每个子集中的数字。这些线程是Searcher类的对象。

我们使用一个CyclicBarrier对象来同步五个线程的完成,并执行Grouper任务来处理部分结果,并计算最终结果。

正如我们前面提到的,CyclicBarrier类有一个内部计数器来控制有多少线程必须到达同步点。每次线程到达同步点时,它都会调用await()方法来通知已到达其同步点的CyclicBarrier对象。CyclicBarrier使所有线程进入睡眠状态。

当所有线程都到达它们的同步点时,CyclicBarrier对象会唤醒在await()方法中等待的所有线程,并且可以选择创建一个新线程,该线程执行一个Runnable对象,该对象作为CyclicBarrier构造中的参数传递(在我们的例子中,是一个Grouper对象),以执行其他任务。

还有更多。。。

CyclicBarrier类有await()方法的另一个版本:

  • awaitlong``time,``TimeUnit``unit:线程将处于休眠状态,直到中断为止;CyclicBarrier的内部计数器到达0或经过指定的时间。TimeUnit类是一个具有以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS

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

重置自行车运载器对象

CyclicBarrier类与CountDownLatch类有一些共同点,但也有一些差异。最重要的区别之一是,CyclicBarrier对象可以重置为其初始状态,并将初始化时使用的值分配给其内部计数器。

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

自行车运载器物体破损

CyclicBarrier对象可以处于表示的特殊状态。当await()方法中有多个线程等待,其中一个线程被中断时,该线程接收到InterruptedException异常,而其他等待的线程接收到BrokenBarrierException异常,并且CyclicBarrier处于断开状态。

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

另见

  • 第三章线程同步工具中的等待多个并发事件配方

运行并发阶段性任务

Java 并发 API 提供的最复杂、最强大的功能之一是使用Phaser类执行并发阶段性任务的能力。当我们将一些并发任务划分为多个步骤时,此机制非常有用。Phaser类为我们提供了在每个步骤结束时同步线程的机制,因此在所有线程完成第一步之前,没有线程开始第二步。

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

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

  1. 获取指定文件夹及其子文件夹中扩展名为.log的文件列表。
  2. 通过删除超过 24 小时前修改的文件来筛选第一步中创建的列表。
  3. 在控制台中打印结果。

在步骤 1 和 2 的末尾,我们检查列表是否包含任何元素。如果没有任何元素,线程将结束其执行并从phaser类中删除。

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

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

    public class FileSearch implements Runnable {
  2. 声明一个私有的String属性来存储搜索操作将在其中开始的文件夹。

      private String initPath;
  3. 声明另一个 privateString属性来存储我们要查找的文件的扩展名。

      private String end;
  4. 声明一个 privateList属性来存储我们将找到的具有所需特征的文件的完整路径。

      private List<String> results;
  5. 最后,声明一个私有的Phaser属性来控制任务不同阶段的同步。

      private Phaser phaser;
  6. 实现类的构造函数,该构造函数将初始化类的属性。它接收初始文件夹的完整路径、文件扩展名和相位器作为参数。

      public FileSearch(String initPath, String end, Phaser phaser) {
        this.initPath = initPath;
        this.end = end;
        this.phaser=phaser;
        results=new ArrayList<>();
      }
  7. 现在,您有来实现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]);
            }
          }
        }
      }
  8. 现在,实现fileProcess()方法。它接收一个File对象作为参数,并检查其扩展名是否等于我们正在寻找的扩展名。如果它们相等,此方法会将文件的绝对路径添加到结果列表中。

      private void fileProcess(File file) {
        if (file.getName().endsWith(end)) {
          results.add(file.getAbsolutePath());
        }
      }
  9. 现在,执行filterResults()方法。它不接收任何参数,并过滤第一阶段获得的文件列表,删除 24 小时前修改过的文件。首先,创建一个新的空列表并获取实际日期。

      private void filterResults() {
        List<String> newResults=new ArrayList<>();
        long actualDate=new Date().getTime();
  10. 然后,检查结果列表的所有元素。对于结果列表中的每个路径,为该文件创建一个File对象,并获取其最后修改日期。

```java
    for (int i=0; i<results.size(); i++){
      File file=new File(results.get(i));
      long fileDate=file.lastModified();

```
  1. 然后,将日期与实际日期进行比较,如果差异小于一天,则将文件的完整路径添加到新的结果列表中。
```java
      if (actualDate-fileDate< TimeUnit.MILLISECONDS.convert(1,TimeUnit.DAYS)){
        newResults.add(results.get(i));
      }
    }
```
  1. 最后,将旧结果列表更改为新结果列表。
```java
    results=newResults;
  }
```
  1. 现在,执行checkResults()方法。此方法将在第一和第二阶段结束时调用,它将检查结果列表是否为空。此方法没有任何参数。
```java
  private boolean checkResults() {
```
  1. 首先,检查结果列表的大小。如果是0,则对象向控制台写入一条消息,指示此情况,然后调用Phaser对象的arriveAndDeregister()方法,通知该线程已完成实际阶段,并离开阶段操作。
```java
  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. 否则,如果结果列表中有元素,则对象会向控制台写入一条消息,指示这种情况,然后调用PhaserarriveAndAwaitAdvance()方法对象通知它此线程已完成实际阶段,并且它希望被阻止,直到分阶段操作中的所有参与线程完成实际阶段。
```java
    } else {
    System.out.printf("%s: Phase %d: %d results.\n",Thread.currentThread().getName(),phaser.getPhase(),results.size());
      phaser.arriveAndAwaitAdvance();
      return true;
    }    
  }
```
  1. 最后一个辅助方法是showInfo()方法,它将结果列表的元素打印到控制台。
```java
  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()方法。在创建所有线程之前,搜索不会开始。
```java
   @Override
  public void run() {

    phaser.arriveAndAwaitAdvance();
```
  1. 然后,向控制台写入一条消息,指示搜索任务的开始。
```java
    System.out.printf("%s: Starting.\n",Thread.currentThread().getName());
```
  1. 检查initPath属性是否存储文件夹名称,并使用directoryProcess()方法在该文件夹及其所有子文件夹中查找具有指定扩展名的文件。
```java
    File file = new File(initPath);
    if (file.isDirectory()) {
      directoryProcess(file);
    }
```
  1. 使用checkResults()方法检查是否有任何结果。如果没有结果,则使用return关键字完成线程的执行。
```java
    if (!checkResults()){
      return;
    }
```
  1. 使用filterResults()方法过滤结果列表。
```java
    filterResults();
```
  1. 使用checkResults()方法再次检查是否有任何结果。如果没有结果,则使用return关键字完成线程的执行。
```java
    if (!checkResults()){
      return;
    }
```
  1. 使用showInfo()方法将最终结果列表打印到控制台,取消注册线程,并打印一条消息,指示线程已完成。
```java
    showInfo();
    phaser.arriveAndDeregister();
    System.out.printf("%s: Work completed.\n",Thread.currentThread().getName());
```
  1. 现在,通过创建一个名为Main的类来实现示例的主类,并向其添加main()方法。
```java
public class Main {

  public static void main(String[] args) {
```
  1. 创建一个有三个参与者的Phaser对象。
```java
    Phaser phaser=new Phaser(3);
```
  1. 创建三个FileSearch对象,每个对象具有不同的初始文件夹。查找扩展名为.log的文件。
```java
    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对象。
```java
    Thread systemThread=new Thread(system,"System");
    systemThread.start();
```
  1. 创建并启动一个线程来执行第二个FileSearch对象。
```java
    Thread appsThread=new Thread(apps,"Apps");
    appsThread.start();
```
  1. 创建并启动一个线程来执行第三个FileSearch对象。
```java
    Thread documentsThread=new Thread(documents, "Documents");
    documentsThread.start();
```
  1. 等待三个线程的完成。
```java
    try {
      systemThread.join();
      appsThread.join();
      documentsThread.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 使用isFinalized()方法写入Phaser对象的最终标志值。
```java
    System.out.println("Terminated: "+ phaser.isTerminated());
```

它是如何工作的。。。

程序开始创建一个Phaser对象,该对象将在每个阶段结束时控制线程的同步。Phaser的构造函数接收参与者的数量作为参数。在我们的例子中,Phaser有三个参与者。此数字向Phaser表示在Phaser更改阶段并唤醒正在睡眠的线程之前,必须执行arriveAndAwaitAdvance()方法的线程数。

创建Phaser后,我们启动三个线程,执行三个不同的FileSearch对象。

在本例中,我们使用 Windows 操作系统的路径。如果使用其他操作系统,请修改路径以使其适应环境中的现有路径。

FileSearch对象的run()方法中的第一条指令是对Phaser对象的arriveAndAwaitAdvance()方法的调用。正如我们前面提到的,Phaser知道要同步的线程数。当一个线程调用此方法时,Phaser减少必须完成实际阶段的线程数,并将此线程置于睡眠状态,直到所有剩余线程完成此阶段。在run()方法开始时调用此方法会使FileSearch线程在创建所有线程之前都不会开始作业。

在第一阶段和第二阶段结束时,我们检查该阶段是否已生成结果,且包含结果的列表是否包含元素,否则该阶段未生成结果且列表为空。在第一种情况下,checkResults()方法调用arriveAndAwaitAdvance(),如前所述。在第二种情况下,如果列表为空,则线程中没有继续执行的点,因此返回。但您必须通知相位器,将减少一名参与者。为此,我们使用了arriveAndDeregister()。这会通知移相器该线程已完成实际阶段,但它不会参与未来阶段,因此移相器不必等待它继续。

showInfo()方法中实现的第三阶段结束时,有一个对移相器arriveAndAwaitAdvance()方法的调用。通过此调用,我们保证所有线程同时完成。当此方法结束执行时,会调用 phaser 的arriveAndDeregister()方法。通过这个调用,我们取消了 phaser 线程的注册,正如前面所解释的,因此当所有线程完成时,phaser 将没有参与者。

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

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

  • 活动Phaser在每个阶段结束时接受新参与者的注册和同步时进入此状态。在这种状态下,Phaser的工作原理如本配方所述。Java 并发 API 中没有提到这种状态。
  • 终止:默认情况下,PhaserPhaser中的所有参与者都已注销时进入此状态,因此Phaser中没有参与者。更详细地说,onAdvance()方法返回true值时Phaser处于终止状态。如果重写该方法,则可以更改默认行为。当Phaser处于该状态时,同步方法arriveAndAwaitAdvance()立即返回,不进行任何同步操作。

Phaser类的一个显著特征是,您不必控制与 phaser 相关的方法的任何异常。与其他同步工具不同,在移相器中休眠的线程不会响应中断事件,也不会引发InterruptedException异常。只有一个例外在下面的部分有更多的部分解释。

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

How it works...

它显示了执行的前两个阶段。您可以看到应用线程如何在第二阶段完成其执行,因为其结果列表为空。当您执行该示例时,您将看到一些线程如何在剩余阶段之前完成一个阶段,但它们会等到所有线程都完成一个阶段后再继续执行剩余阶段。

还有更多。。。

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

  • arrive():此方法通知移相器一个参与者已完成实际阶段,但不应等待其他参与者继续执行。使用此方法时要小心,因为它不会与其他线程同步。
  • awaitAdvance(int``phase)如果我们作为参数传递的数字等于相量的实际相位,则此方法将当前线程置于休眠状态,直到相量的所有参与者完成相量的当前相位。如果参数和移相器的实际相位不相等,则该方法立即返回。
  • awaitAdvanceInterruptibly(int``phaser)此方法与前面解释的方法相同,但如果在此方法中休眠的线程被中断,则抛出InterruptedException异常。

在相位器中注册参与者

当您创建一个Phaser对象时,您将指示有多少参与者将拥有该相位器。但是Phaser类有两种方法来增加相位器的参与者数量。这些方法如下:

  • register():此方法在Phaser中增加一个新的参与者。该新参与者将被视为未进入实际阶段。
  • bulkRegister(int``Parties)此方法将指定数量的参与者添加到相位器中。这些新参与者将被视为未进入实际阶段。

Phaser类提供的减少参与者数量的唯一方法是arriveAndDeregister()方法,该方法通知移相器线程已经完成了实际阶段,并且它不想继续进行阶段操作。

强制终止移相器

当相位器的参与者为零时,它将进入终止表示的状态。Phaser类提供forceTermination()来更改相位器的状态,并使其进入终止状态,与相位器中注册的参与者数量无关。当其中一个参与者出现错误情况时,该机制可能有用,以强制终止相位器。

当移相器处于终止状态时,awaitAdvance()arriveAndAwaitAdvance()方法立即返回负数,而不是正常返回的正数。如果您知道您的移相器可能会被终止,您应该验证这些方法的返回值,以了解移相器是否已终止。

另见

  • 第 8 章测试并发应用中的监控移相器配方

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

Phaser类提供了一种方法,每次移相器改变相位时都会执行该方法。这是onAdvance()方法。接收两个参数:当前阶段的数量和注册参与者的数量;如果移相器继续执行,则返回一个Boolean值;false如果移相器已完成且必须进入终止状态,则返回true

如果注册的参与者数量为零,则该方法的默认实现返回true,否则返回false。但是,如果扩展Phaser类并重写此方法,则可以修改此行为。通常,当您从一个阶段推进到下一个阶段时,必须执行某些操作时,您会对此感兴趣。

在本配方中,您将学习如何在一个正在实现您自己版本的Phaser类的移相器中控制相位变化,该类重写onAdvance()方法以在每个相位变化中执行一些操作。您将实现一个模拟考试,其中将有一些学生必须做三个练习。所有的学生都必须完成一个练习才能进行下一个。

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

  1. 创建一个名为MyPhaser的类,并指定它从Phaser类扩展而来。

    public class MyPhaser extends Phaser {
  2. 覆盖onAdvance()方法。根据不同阶段的属性,我们调用不同阶段的辅助值。如果相位等于零,则必须调用studentsArrived()方法。如果相位等于 1,则必须调用finishFirstExercise()方法。如果相位等于 2,则需要调用finishSecondExercise()方法,如果相位等于 3,则需要调用finishExam()方法。否则,我们返回true值,表示移相器已终止。

       @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;
        }
      }
  3. 执行辅助方法studentsArrived()。它向控制台写入两条日志消息并返回false值,以指示移相器继续执行。

      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;
      }
  4. 执行辅助方法finishFirstExercise()。它向控制台写入两条消息并返回false值,以指示移相器继续执行。

      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;
      }
  5. 执行辅助方法finishSecondExercise()。它向控制台写入两条消息并返回false值,以指示移相器继续执行。

      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;
      }
  6. 执行辅助方法finishExam()。向控制台写入两条消息并返回true值,表示移相器已完成工作。

      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;
      }
  7. 创建一个名为Student的类,并指定它实现Runnable接口。这门课将模拟参加考试的学生。

    public class Student implements Runnable {
  8. 声明一个名为phaserPhaser对象。

      private Phaser phaser;
  9. 实现初始化Phaser对象的类的构造函数。

      public Student(Phaser phaser) {
        this.phaser=phaser;
      }
  10. 实现模拟考试实现的run()方法。

```java
   @Override
  public void run() {
```
  1. 首先,该方法在控制台中写入一条消息,指示该学生已到达考试,并调用 phaser 的arriveAndAwaitAdvance()方法等待其余线程。
```java
    System.out.printf("%s: Has arrived to do the exam. %s\n",Thread.currentThread().getName(),new Date());
    phaser.arriveAndAwaitAdvance();
```
  1. 然后,向控制台写一条消息,调用模拟考试第一个练习实现的私有doExercise1()方法,再向控制台和相位器的arriveAndAwaitAdvance()方法写一条消息,等待其余学生完成第一个练习。
```java
    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. 第二次练习和第三次练习执行相同的代码。
```java
    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()。此方法使线程在一段随机时间内处于休眠状态。
```java
  private void doExercise1() {
    try {
      long duration=(long)(Math.random()*10);
      TimeUnit.SECONDS.sleep(duration);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
```
  1. 执行辅助方法doExercise2()。此方法使线程在一段随机时间内处于休眠状态。
```java
  private void doExercise2() {
    try {
      long duration=(long)(Math.random()*10);
      TimeUnit.SECONDS.sleep(duration);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
```
  1. 执行辅助方法doExercise3()。此方法使线程在随机时间段内休眠。
```java
  private void doExercise3() {
    try {
      long duration=(long)(Math.random()*10);
      TimeUnit.SECONDS.sleep(duration);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
```
  1. 通过创建名为Main的类来实现示例的主类,并向其添加main()方法。
```java
public class Main {

  public static void main(String[] args) {
```
  1. 创建一个MyPhaser对象。
```java
    MyPhaser phaser=new MyPhaser();
```
  1. 创建五个Student对象,并使用register()方法在相位器中注册它们。
```java
    Student students[]=new Student[5];
    for (int i=0; i<students.length; i++){
      students[i]=new Student(phaser);
      phaser.register();
    }
```
  1. 创建五个线程运行students并启动它们。
```java
    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. 等待五个线程的完成。
```java
    for (int i=0; i<threads.length; i++){
      try {
        threads[i].join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
```
  1. 使用isTerminated()方法编写一条消息,表明相位器处于终止状态。
```java
    System.out.printf("Main: The phaser has finished: %s.\n",phaser.isTerminated());
```

它是如何工作的。。。

此练习模拟一个包含三个练习的考试的实现。所有学生都必须完成一个练习,然后才能开始下一个练习。为了实现这个同步需求,我们使用Phaser类,但是您已经实现了自己的 phaser,它扩展了原始类以覆盖onAdvance()方法。

在进行相变之前,以及在唤醒在arriveAndAwaitAdvance()方法中睡眠的所有线程之前,移相器会调用此方法。此方法接收实际阶段的数量作为参数,其中0是第一阶段的数量和注册参与者的数量。最有用的参数是实际相位。如果根据实际阶段执行不同的操作,则必须使用替代结构(if/elseswitch来选择要执行的操作。在本例中,我们使用switch结构为每个相位变化选择不同的方法。

onAdvance()方法返回一个Boolean值,指示移相器是否已终止。如果 phaser 返回一个false值,则表示它尚未终止,因此线程将继续执行其他阶段。如果移相器返回一个true值,那么移相器仍然会唤醒挂起的线程,但会将移相器移动到终止状态,因此将来所有对移相器任何方法的调用都会立即返回,isTerminated()方法返回true值。

Core类中,当您创建MyPhaser对象时,没有指定 phaser 中的参与者数量。您为创建的每个Student对象调用register()方法,以在相位器中注册参与者。此调用不会在Student对象或执行该对象的线程与移相器之间建立关系。实际上,相位器中的参与者数量只是一个数字。相位器和参与者之间没有关系。

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

How it works...

你可以看到学生如何在不同的时间完成第一个练习。当所有学生都完成该练习后,移相器调用onAdvance()方法,该方法在控制台中写入日志消息,然后所有学生同时开始第二个练习。

另见

  • 第 3 章线程同步工具中的运行并发阶段任务配方
  • 第 8 章测试并发应用中的监控移相器配方

在并发任务之间更改数据

Java 并发 API 提供了一个同步工具,允许在两个并发任务之间交换数据。更详细地说,Exchanger类允许定义两个线程之间的同步点。当两个线程到达这一点时,它们交换一个数据结构,因此第一个线程的数据结构转到第二个线程,第二个线程的数据结构转到第一个线程。

在类似于生产者-消费者问题的情况下,此类可能非常有用。这是一个典型的并发问题,其中有一个公共数据缓冲区、一个或多个数据生产者和一个或多个数据消费者。由于Exchanger类只同步两个线程,如果一个生产者和一个消费者出现生产者-消费者问题,您可以使用它。

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

准备好了吗

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

怎么做。。。

按照以下步骤来实现该示例:

  1. 首先,让我们从实现生产者开始。创建一个名为Producer的类,并指定它实现Runnable接口。

    public class Producer implements Runnable {
  2. 声明一个名为bufferList<String>对象。这将是生产者与消费者交换的数据结构。

      private List<String> buffer;
  3. 声明一个名为exchangerExchanger<List<String>>对象。这将是用于同步生产者和消费者的交换器对象。

      private final Exchanger<List<String>> exchanger;
  4. 实现初始化这两个属性的类的构造函数。

      public Producer (List<String> buffer, Exchanger<List<String>> exchanger){
        this.buffer=buffer;
        this.exchanger=exchanger;
      }
  5. 执行run()方法。在它内部,实现 10 个交换周期。

      @Override
      public void run() {
        int cycle=1;
    
        for (int i=0; i<10; i++){
          System.out.printf("Producer: Cycle %d\n",cycle);
  6. 在每个循环中,向缓冲区添加 10 个字符串。

          for (int j=0; j<10; j++){
            String message="Event "+((i*10)+j);
            System.out.printf("Producer: %s\n",message);
            buffer.add(message);
          }
  7. 调用exchange()方法与使用者交换数据。由于此方法可能引发InterruptedException异常,您必须添加代码来处理它。

          try {
            buffer=exchanger.exchange(buffer);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println("Producer: "+buffer.size());
          cycle++;
        }
  8. 现在,让我们实现消费者。创建一个名为Consumer的类,并指定它实现Runnable接口。

    public class Consumer implements Runnable {
  9. 声明一个名为bufferList<String>对象。这将是生产者与消费者交换的数据结构。

      private List<String> buffer;
  10. 声明一个名为exchangerExchanger<List<String>>对象。这将是用于同步生产者和消费者的交换器对象。

```java
  private final Exchanger<List<String>> exchanger;
```
  1. 实现初始化这两个属性的类的构造函数。
```java
  public Consumer(List<String> buffer, Exchanger<List<String>> exchanger){
    this.buffer=buffer;
    this.exchanger=exchanger;
  }
```
  1. 执行run()方法。在它内部,实现 10 个交换周期。
```java
  @Override
  public void run() {
    int cycle=1;

    for (int i=0; i<10; i++){
      System.out.printf("Consumer: Cycle %d\n",cycle);
```
  1. 在每个周期中,首先调用exchange()方法与生产者同步。消费者需要数据来消费。由于此方法可能引发InterruptedException异常,您必须添加代码来处理它。
```java
      try {
        buffer=exchanger.exchange(buffer);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
```
  1. 将生产者在其缓冲区中发送的 10 个字符串写入控制台,并将其从缓冲区中删除,使其为空。
```java
      System.out.println("Consumer: "+buffer.size());

      for (int j=0; j<10; j++){
        String message=buffer.get(0);
        System.out.println("Consumer: "+message);
        buffer.remove(0);
      }

      cycle++;
    }
```
  1. 现在,通过创建一个名为Core的类来实现示例的主类,并向其添加main()方法。
```java
public class Core {

  public static void main(String[] args) {
```
  1. 创建生产者和消费者将使用的两个缓冲区。
```java
    List<String> buffer1=new ArrayList<>();
    List<String> buffer2=new ArrayList<>();
```
  1. 创建用于同步生产者和消费者的Exchanger对象。
```java
    Exchanger<List<String>> exchanger=new Exchanger<>();
```
  1. 创建Producer对象和Consumer对象。
```java
    Producer producer=new Producer(buffer1, exchanger);
    Consumer consumer=new Consumer(buffer2, exchanger);
```
  1. 创建线程以执行生产者和消费者并启动线程。
```java
    Thread threadProducer=new Thread(producer);
    Thread threadConsumer=new Thread(consumer);

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

它是如何工作的。。。

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

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

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

还有更多。。。

Exchanger类有另一个版本的交换方法:exchange(V``data,``long``time,``TimeUnit``unit),其中VPhaser声明中用作参数的类型(List<String>在本例中)。该线程将一直处于休眠状态,直到它被中断、另一个线程到达或经过指定的时间。TimeUnit类是具有以下常量的枚举:DAYSHOURSMICROSECONDSMILLISECONDSMINUTESNANOSECONDSSECONDS