Skip to content

Latest commit

 

History

History
1426 lines (1000 loc) · 56.1 KB

File metadata and controls

1426 lines (1000 loc) · 56.1 KB

八、测试并发应用

在本章中,我们将介绍:

  • 监控Lock接口
  • 监控Phaser
  • 监视执行器框架
  • 监视 Fork/Join 池
  • 编写有效的日志消息
  • 用 FindBugs 分析并发代码
  • 配置 Eclipse 以调试并发代码
  • 配置 NetBeans 以调试并发代码
  • 用多线程技术测试并发代码

导言

测试应用是一项关键任务。在为最终用户准备好应用之前,您必须证明其正确性。您使用一个测试过程来证明实现了正确性,并且纠正了错误。测试阶段是任何软件开发中的常见任务,也是质量保证过程。您可以找到大量关于测试过程的文献,以及可以应用于开发的不同方法。还有很多库,比如JUnit和应用,比如 ApacheJMetter,您可以使用它们以自动化的方式测试 Java 应用。在并发应用开发中,它甚至更为关键。

并发应用有两个或多个共享数据结构并相互交互的线程,这一事实给测试阶段增加了更多的难度。在测试并发应用时,您将面临的最大问题是线程的执行是不确定的。您无法保证线程的执行顺序,因此很难重现错误。

在本章中,您将学习:

  • 如何获取有关并发应用中的元素的信息。这些信息可以帮助您测试并发应用。
  • 如何使用 IDE(集成开发环境)和其他工具(如 FindBugs)来测试并发应用。
  • 如何使用库(如 MultithreadedTC)自动化测试。

监控锁接口

Lock接口是 Java 并发 API 提供的获取代码块同步的基本机制之一。它允许定义临界截面。临界段是访问共享资源的代码块,不能由多个线程同时执行。该机制由Lock接口和ReentrantLock类实现。

在本食谱中,您将了解您可以获得关于Lock对象的哪些信息以及如何获得这些信息。

准备好了吗

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

怎么做。。。

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

  1. 创建一个名为MyLock的类来扩展ReentrantLock类。

    public class MyLock extends ReentrantLock {
  2. 执行getOwnerName()方法。此方法返回使用LockgetOwner()的受保护方法控制锁(如果有)的线程的名称。

      public String getOwnerName() {
        if (this.getOwner()==null) {
          return "None";
        }
        return this.getOwner().getName();
      }
  3. 执行getThreads()方法。此方法返回使用LockgetQueuedThreads()的受保护方法在锁中排队的线程列表。

      public Collection<Thread> getThreads() {
        return this.getQueuedThreads();
      }
  4. 创建一个名为Task的类,该类实现Runnable接口。

    public class Task implements Runnable {
  5. 声明名为lock的私有Lock属性。

      private Lock lock;
  6. 实现类的构造函数以初始化其属性。

      public Task (Lock lock) {
        this.lock=lock;
      }
  7. 执行run()方法。创建一个包含五个步骤的循环。

      @Override
      public void run() {
        for (int i=0; i<5; i++) {
  8. 使用lock()方法获取锁并打印消息。

          lock.lock();
          System.out.printf("%s: Get the Lock.\n",Thread.currentThread().getName());
  9. 将线程休眠 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();
          }
            }
      }
    }
  10. 通过使用main()方法创建名为Main的类来创建示例的主类。

```java
public class Main {

  public static void main(String[] args) throws Exception {
```
  1. 创建一个名为lockMyLock对象。
```java
    MyLock lock=new MyLock();
```
  1. 为五个Thread对象创建数组。
```java
    Thread threads[]=new Thread[5];
```
  1. 创建并启动五个线程来执行五个Task对象。
```java
    for (int i=0; i<5; i++) {
      Task task=new Task(lock);
      threads[i]=new Thread(task);
      threads[i].start();
    }
```
  1. 创建一个包含 15 个步骤的循环。
```java
    for (int i=0; i<15; i++) {
```
  1. 在控制台中写入锁所有者的名称。
```java
      System.out.printf("Main: Logging the Lock\n");
      System.out.printf("************************\n");
      System.out.printf("Lock: Owner : %s\n",lock.getOwnerName());
```
  1. 显示排队等待锁定的线程的编号和名称。
```java
.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对象的公平性和状态信息。
```java
      System.out.printf("Lock: Fairness: %s\n",lock.isFair());
      System.out.printf("Lock: Locked: %s\n",lock.isLocked());
      System.out.printf("************************\n");
```
  1. 让线程休眠 1 秒,然后关闭循环和类。
```java
      TimeUnit.SECONDS.sleep(1);
    }

  }

}
```

它是如何工作的。。。

在此配方中,您实现了MyLock类,该类扩展了ReentrantLock类以返回否则不可用的信息—它是ReentrantLock类的受保护数据。MyLock类实现的方法有:

  • getOwnerName():只有一个线程可以执行受Lock对象保护的临界段。锁存储执行临界段的线程。此线程由ReentrantLock类的受保护getOwner()方法返回。此方法使用getOwner()方法返回该线程的名称。
  • getThreads():当线程正在执行临界段时,尝试进入该临界段的其他线程将处于休眠状态,直到它们可以继续执行该临界段。ReentrantLock类的受保护方法getQueuedThreads()返回等待执行临界段的线程列表。此方法返回由方法和getQueuedThreads()方法返回的结果。

我们还使用了在ReentrantLock类中实现的其他方法:

  • hasQueuedThreads():此方法返回一个Boolean值,指示是否有线程等待获取此锁
  • getQueueLength():此方法返回等待获取此锁的线程数
  • isLocked():此方法返回一个Boolean值,指示此锁是否属于线程
  • isFair():此方法返回一个Boolean值,指示此锁是否激活了公平模式

还有更多。。。

ReentrantLock类中还有其他方法可用于获取Lock对象的信息:

  • getHoldCount():返回当前线程获取锁的次数
  • isHeldByCurrentThread():返回一个Boolean值,指示锁是否属于当前线程

另见

  • 第 2 章基本线程同步中的用锁同步代码块配方
  • 第 7 章定制并发类实现自定义锁类配方

监控相量等级

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

在本食谱中,您将了解有关Phaser类状态的哪些信息以及如何获取这些信息。

准备好了吗

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

怎么做。。。

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

  1. 创建一个名为Task的类来实现Runnable接口。

    public class Task implements Runnable {
  2. 声明名为time的私有int属性。

      private int time;
  3. 声明名为phaser的私有Phaser属性。

      private Phaser phaser;
  4. 实现类的构造函数以初始化其属性。

      public Task(int time, Phaser phaser) {
        this.time=time;
        this.phaser=phaser;
      }
  5. 执行run()方法。首先,指示phaser属性任务使用arrive()方法开始执行。

        @Override
      public void run() {
    
        phaser.arrive();
  6. 在控制台中写入指示第一阶段开始的消息,按照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();
  7. 对第二和第三阶段重复该行为。在第三阶段结束时,使用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();
  8. 通过使用main()方法创建名为Main的类,实现示例的主类。

    public class Main {
    
      public static void main(String[] args) throws Exception {
  9. 创建一个名为phaser的新Phaser对象,其中有三名参与者。

        Phaser phaser=new Phaser(3);
  10. 创建并启动三个线程以执行三个任务对象。

```java
    for (int i=0; i<3; i++) {
      Task task=new Task(i+1, phaser);
      Thread thread=new Thread(task);
      thread.start();
    }
```
  1. 创建一个包含 10 个步骤的循环,以写入关于phaser对象的信息。
```java
    for (int i=0; i<10; i++) {
```
  1. 填写有关注册方、相位器相位、到达方和未到达方的信息。
```java
    for (int i=0; i<10; i++) {
      System.out.printf("********************\n");
      System.out.printf("Main: Phaser Log\n");
      System.out.printf("Main: Phaser: Phase: %d\n",phaser.getPhase());
      System.out.printf("Main: Phaser: Registered Parties: %d\n",phaser.getRegisteredParties());
      System.out.printf("Main: Phaser: Arrived Parties: %d\n",phaser.getArrivedParties());
      System.out.printf("Main: Phaser: Unarrived Parties: %d\n",phaser.getUnarrivedParties());
      System.out.printf("********************\n");
```
  1. 让线程休眠 1 秒,然后关闭循环和类。
```java
        TimeUnit.SECONDS.sleep(1);
    }
  }
}
```

它是如何工作的。。。

在这个配方中,我们在Task类中实现了一个分阶段任务。此分阶段任务有三个阶段,使用Phaser接口与其他Task对象同步。main 类启动三个任务,当这些任务执行其阶段时,它将有关phaser对象状态的信息打印到控制台。我们使用以下方法获取phaser对象的状态:

  • getPhase():此方法返回phaser对象的实际相位
  • getRegisteredParties():此方法返回使用phaser对象作为同步机制的任务数
  • getArrivedParties():此方法返回实际阶段结束时到达的任务数
  • getUnarrivedParties():此方法返回实际阶段结束时尚未到达的任务数

以下屏幕截图显示了程序的部分输出:

How it works...

另见

  • 第 3 章线程同步工具中的运行并发阶段任务配方

监控执行器框架

Executor 框架提供了一种机制,将任务的执行与执行这些任务的线程创建和管理分离开来。如果使用执行器,则只需实现Runnable对象并将其发送给执行器即可。执行者负责管理线程。当您向执行者发送任务时,它会尝试使用池线程来执行此任务,以避免创建新线程。该机制由Executor接口及其实现类ThreadPoolExecutor类提供。

在本食谱中,您将了解您可以获得关于ThreadPoolExecutor执行人状态的哪些信息以及如何获得这些信息。

准备好了吗

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

怎么做。。。

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

  1. 创建一个名为Task的类,该类实现Runnable接口。

    public class Task implements Runnable {
  2. 声明名为milliseconds的私有long属性。

      private long milliseconds;
  3. 实现类的构造函数以初始化其属性。

      public Task (long milliseconds) {
        this.milliseconds=milliseconds;
      }
  4. 执行run()方法。按照milliseconds属性指定的毫秒数休眠线程。

      @Override
      public void run() {
    
        System.out.printf("%s: Begin\n",Thread.currentThread().getName());
        try {
          TimeUnit.MILLISECONDS.sleep(milliseconds);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.printf("%s: End\n",Thread.currentThread().getName());
    
      }
  5. 通过使用main()方法创建名为Main的类来实现示例的主类。

    public class Main {
    
      public static void main(String[] args) throws Exception {
  6. 使用Executors类的newCachedThreadPool()方法创建新Executor对象。

        ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newCachedThreadPool();
  7. 创建并向执行者提交 10 个Task对象。使用随机数初始化对象。

        Random random=new Random();
        for (int i=0; i<10; i++) {
          Task task=new Task(random.nextInt(10000));
          executor.submit(task);
        }
  8. 创建一个包含五个步骤的循环。在每个步骤中,写入有关调用showLog()方法的执行器的信息,并使线程休眠一秒钟。

        for (int i=0; i<5; i++){
          showLog(executor);
          TimeUnit.SECONDS.sleep(1);
        }
  9. 使用shutdown()方法关闭执行器。

        executor.shutdown();
  10. 在每个步骤中创建另一个包含五个步骤的循环,写入有关调用showLog()方法的执行器的信息,并使线程休眠一秒钟。

```java
    for (int i=0; i<5; i++){
      showLog(executor);
      TimeUnit.SECONDS.sleep(1);
    }
```
  1. 使用awaitTermination()方法等待执行器的最终确定。
```java
      executor.awaitTermination(1, TimeUnit.DAYS);
```
  1. 显示有关程序结束的消息。
```java
    System.out.printf("Main: End of the program.\n");
  }
```
  1. 执行接收Executor作为参数的showLog()方法。写入有关池大小、任务数和执行器状态的信息。
```java
  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值。

另见

  • 第 4 章线程执行器中的创建线程执行器配方
  • 第 7 章定制并发类中的定制 ThreadPoolExecutor 类配方
  • 第 7 章定制并发类实现基于优先级的执行器类配方

监控 Fork/Join 池

Executor 框架提供一种机制,允许将任务实现与执行这些任务的线程的创建和管理分离。Java 7 包括针对特定类型问题的 Executor 框架的扩展,该问题将提高其他解决方案的性能(如直接使用Thread对象或 Executor 框架)。这是 Fork/Join 框架。

该框架旨在通过使用fork()join()操作的分治技术来解决那些可以分解为更小任务的问题。实现此行为的主类是ForkJoinPool类。

在本食谱中,您将了解您可以获得关于ForkJoinPool类的哪些信息以及如何获得这些信息。

准备好了吗

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

怎么做。。。

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

  1. 创建一个名为Task的类来扩展RecursiveAction类。

    public class Task extends RecursiveAction{
  2. 声明一个名为array的私有int数组属性来存储要递增的元素数组。

    private int array[];
  3. 声明两个名为startend的私有int属性,以存储此任务必须处理的元素块的开始和结束位置。

      private int start;
      private int end;
  4. 实现类的构造函数以初始化其属性。

      public Task (int array[], int start, int end) {
        this.array=array;
        this.start=start;
        this.end=end;
      }
  5. 使用任务的主要逻辑执行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();
  6. 如果任务必须处理 100 个或更少的元素,则在每次操作后将线程休眠 5 毫秒,以增加这些元素。

        } else {
          for (int i=start; i<end; i++) {
            array[i]++;
    
            try {
              Thread.sleep(5);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
      }
    }
  7. 通过使用main()方法创建名为Main的类来实现示例的主类。

    public class Main {
    
      public static void main(String[] args) throws Exception {
  8. 创建一个名为poolForkJoinPool对象。

        ForkJoinPool pool=new ForkJoinPool();
  9. 创建一个名为array的整数数组,包含 10000 个元素。

        int array[]=new int[10000];
  10. 创建一个新的Task对象来处理整个数组。

```java
    Task task1=new Task(array,0,array.length);
```
  1. 使用execute()方法在池中发送要执行的任务。
```java
    pool.execute(task1);
```
  1. 当任务未完成执行时,调用showLog()方法写入ForkJoinPool类的状态信息,并让线程休眠一秒钟。
```java
    while (!task1.isDone()) {
      showLog(pool);
      TimeUnit.SECONDS.sleep(1);
    }
```
  1. 使用shutdown()方法关闭池。
```java
    pool.shutdown();
```
  1. 使用awaitTermination()方法等待池的最终确定。
```java
      pool.awaitTermination(1, TimeUnit.DAYS);
```
  1. 调用showLog()方法写入ForkJoinPool类的状态信息,并在控制台中写入一条消息,表示程序结束。
```java
    showLog(pool);
    System.out.printf("Main: End of the program.\n");
```
  1. 执行showLog()方法。它接收一个ForkJoinPool对象作为参数,并写入有关其状态以及正在执行的线程和任务的信息。
```java
  private static void showLog(ForkJoinPool pool) {
    System.out.printf("**********************\n");
    System.out.printf("Main: Fork/Join Pool log\n");
    System.out.printf("Main: Fork/Join Pool: Parallelism: %d\n",pool.getParallelism());
    System.out.printf("Main: Fork/Join Pool: Pool Size: %d\n",pool.getPoolSize());
    System.out.printf("Main: Fork/Join Pool: Active Thread Count: %d\n",pool.getActiveThreadCount());
    System.out.printf("Main: Fork/Join Pool: Running Thread Count: %d\n",pool.getRunningThreadCount());
    System.out.printf("Main: Fork/Join Pool: Queued Submission: %d\n",pool.getQueuedSubmissionCount());
    System.out.printf("Main: Fork/Join Pool: Queued Tasks: %d\n",pool.getQueuedTaskCount());
    System.out.printf("Main: Fork/Join Pool: Queued Submissions: %s\n",pool.hasQueuedSubmissions());
    System.out.printf("Main: Fork/Join Pool: Steal Count: %d\n",pool.getStealCount());
    System.out.printf("Main: Fork/Join Pool: Terminated : %s\n",pool.isTerminated());
    System.out.printf("**********************\n");
  }
```

它是如何工作的。。。

在这个配方中,您已经实现了一个任务,该任务使用ForkJoinPool类和扩展RecursiveAction类的Task类来增加数组的元素;您可以在ForkJoinPool类中执行的任务之一。当任务处理数组时,您可以将有关ForkJoinPool类状态的信息打印到控制台。您已使用以下方法获取ForkJoinPool类的状态:

  • getPoolSize():此方法返回一个int值,即 fork-join 池内部池的工作线程数
  • getParallelism():此方法返回为池建立的所需并行级别
  • getActiveThreadCount():此方法返回当前正在执行任务的线程数
  • getRunningThreadCount():此方法返回任何同步机制中未阻塞的工作线程数
  • getQueuedSubmissionCount():此方法返回已提交到尚未开始执行的池的任务数
  • getQueuedTaskCount():此方法返回已提交到已开始执行的池的任务数
  • hasQueuedSubmissions():此方法返回一个Boolean值,指示此池是否已将尚未开始执行的任务排入队列
  • getStealCount():此方法返回一个long值,该值表示工作线程从另一个线程窃取任务的次数
  • isTerminated():此方法返回一个Boolean值,指示 fork/join 池是否已完成执行

另见

  • 第 5 章Fork/Join 框架中的创建 Fork/Join 池配方
  • 实现 ThreadFactory 接口为第 7 章定制并发类中的 Fork/Join 框架配方生成自定义线程
  • 第 7 章定制并发类定制运行在 Fork/Join 框架配方中的任务

写入有效日志消息

日志系统是允许您将信息写入一个或多个目的地的机制。记录器具有以下组件:

  • 一个或多个处理程序:一个处理程序将确定日志消息的目的地和格式。您可以将日志消息写入控制台、文件或数据库。
  • 名称:通常,基于类名及其包名的类中使用的记录器的名称。
  • 级别:日志消息有一个关联的级别,表示其重要性。记录器还有一个级别,用于决定要写入哪些消息。它只写与其级别同等重要或更重要的消息。

您应使用日志系统,主要用于以下两个目的:

  • 捕获异常时,尽可能多地编写信息。这将有助于定位错误并解决它。
  • 编写有关程序正在执行的类和方法的信息。

在本教程中,您将学习如何使用java.util.logging包提供的类向并发应用添加日志系统。

准备好了吗

此配方的示例已使用 EclipseIDE 实现。如果您使用 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();
      }
  2. 创建一个名为MyLogger的类。

    public class MyLogger {
  3. 声明一个名为handler的私有静态Handler属性。

      private static Handler handler;
  4. 实现 public static 方法getLogger()来创建用于写入日志消息的Logger对象。它接收一个名为nameString参数。

      public static Logger getLogger(String name){
  5. 使用Logger类的getLogger()方法获取与作为参数接收的名称关联的java.util.logging.Logger

        Logger logger=Logger.getLogger(name);
  6. 建立日志级别,使用和setLevel()方法写入所有日志消息。

        logger.setLevel(Level.ALL);
  7. 如果 handler 属性具有null值,则创建一个新的FileHandler对象,将日志消息写入recipe8.log文件中。使用setFormatter()对象将MyFormatter对象指定给该处理程序作为格式化程序。

        try {
          if (handler==null) {
            handler=new FileHandler("recipe8.log");
            Formatter format=new MyFormatter();
            handler.setFormatter(format);
          }
  8. 如果Logger对象没有关联的处理程序,请使用addHandler()方法分配该处理程序。

          if (logger.getHandlers().length==0) {
            logger.addHandler(handler);
          }
        } catch (SecurityException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        }
  9. 返回创建的Logger对象。

        return logger;
      }
  10. 创建一个名为Task的类来实现Runnable接口。这将是用来测试你的Logger对象的任务。

```java
public class Task implements Runnable {
```
  1. 执行run()方法。
```java
  @Override
  public void run() {
```
  1. 首先,声明一个名为loggerLogger对象。使用MyLogger类的getLogger()方法将该类的名称作为参数进行初始化。
```java
    Logger logger= MyLogger.getLogger(this.getClass().getName());
```
  1. 使用entering()方法写入日志消息,指示开始执行该方法。
```java
    logger.entering(Thread.currentThread().getName(), "run()");
Sleep the thread for two seconds.
    try {
      TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
```
  1. 使用exiting()方法写入一条日志消息,指示该方法的执行结束。
```java
    logger.exiting(Thread.currentThread().getName(), "run()",Thread.currentThread());
  }
```
  1. 通过使用main()方法创建名为Main的类来实现示例的主类。
```java
public class Main {

  public static void main(String[] args) {
```
  1. 声明一个名为loggerLogger对象。使用MyLogger类的getLogger()方法将字符串Core作为参数进行初始化。
```java
    Logger logger=MyLogger.getLogger("Core");
```
  1. 写入日志消息,指示使用entering()方法开始执行主程序。
```java
    logger.entering("Core", "main()",args);
```
  1. 创建一个Thread数组来存储五个线程。
```java
    Thread threads[]=new Thread[5];
```
  1. 创建五个Task对象和五个线程来执行它们。编写日志消息以指示您将要启动一个新线程,并指示您已经创建了该线程。
```java
    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. 写一条日志消息,表明您已经创建了线程。
```java
    logger.log(Level.INFO,"Ten Threads created."+
"Waiting for its finalization");
```
  1. 等待使用join()方法完成五个线程。完成每个线程后,编写一条日志消息,指示线程已完成。
```java
    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()方法写入日志消息,以指示主程序的执行结束。
```java
    logger.exiting("Core", "main()");
  }
```

它是如何工作的。。。

在此配方中,您使用了为 Java 日志 API 提供的Logger类在并发应用中编写日志消息。首先,您已经实现了MyFormatter类,为日志消息提供了一种格式。此类扩展了声明抽象方法format()Formatter类。此方法接收包含日志消息所有信息的LogRecord对象,并返回格式化的日志消息。在您的类中,您使用了LogRecord类的以下方法来获取有关日志消息的信息:

  • getLevel():返回消息的级别
  • getMillis():返回向Logger对象发送消息的日期
  • getSourceClassName():返回向记录器发送消息的类的名称
  • getSourceMessageName():返回向记录器发送消息的方法的名称

getMessage()返回日志消息。MyLogger类实现静态方法getLogger(),该方法创建Logger对象并分配Handler对象,以使用MyFormatter格式化程序将应用的日志消息写入recipe8.log文件。您使用该类的静态方法getLogger()创建Logger对象。此方法为作为参数传递的每个名称返回不同的对象。您只创建了一个Handler对象,因此所有Logger对象将在同一文件中写入其日志消息。您还将记录器配置为写入所有日志消息,无论其级别如何。

最后,您实现了一个Task对象和一个在日志文件中写入不同日志消息的主程序。您使用了以下方法:

  • entering():写一条FINER级别的消息,表示方法开始执行
  • exiting():写入一条FINER级别的消息,表示方法结束执行
  • log():写一条指定级别的报文

还有更多。。。

当您使用日志系统时,您必须考虑两个要点:

  • 写下必要的信息:如果你写的信息太少,记录器将不会有用,因为它无法实现其目的。如果您写入的信息太多,将生成太大的日志文件,这些文件将无法管理,并且很难获取必要的信息。
  • 对消息使用适当的级别:如果您使用较高级别编写信息消息或使用较低级别编写错误消息,您将混淆查看日志文件的用户。要知道在错误情况下发生了什么会更加困难,或者您将有太多的信息无法知道错误的主要原因。

还有其他提供比java.util.logging包更完整的日志系统的库,例如 Log4j 或 slf4j 库。但是java.util.logging包是 JavaAPI 的一部分,它的所有方法都是多线程安全的,因此我们可以在并发应用中使用它而不会出现问题。

另见

  • 第 6 章并发集合中的使用非阻塞线程安全列表配方
  • 第 6 章并发集合中的使用阻塞线程安全列表配方
  • 第 6 章并发集合中的使用按优先级排序的阻塞线程安全列表
  • 第 6 章并发集合中的使用带延迟元素的线程安全列表配方
  • 第 6 章并发集合中的使用线程安全导航地图配方
  • 第 6 章并发集合中的生成并发随机数配方

使用 FindBugs 分析并发代码

静态代码分析工具是一组分析应用源代码以查找潜在错误的工具。这些工具,如 Checkstyle、PMD 或 FindBugs 具有一组预定义的良好实践规则,并解析源代码以查找违反这些规则的情况。目标是在生产中执行之前,尽早发现导致性能低下的错误或位置。编程语言通常提供这样的工具,Java 也不例外。分析 Java 代码的工具之一是 FindBugs。它是一个开源工具,包含一系列分析 Java 并发代码的规则。

在本教程中,您将学习如何使用此工具分析 Java 并发应用。

准备好了吗

在说明此配方之前,您应该从项目的网页(下载 FindBugshttp://findbugs.sourceforge.net/ )。您可以下载独立应用或 Eclipse 插件。在此配方中,您将使用独立版本。

怎么做。。。

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

  1. 创建一个名为Task的类来扩展Runnable接口。

    public class Task implements Runnable {
  2. 声明名为Lock的私有ReentrantLock属性。

      private ReentrantLock lock;
  3. 实现类的构造函数。

      public Task(ReentrantLock lock) {
        this.lock=lock;
      }
  4. 执行run()方法。控制锁,使线程休眠 2 秒钟,然后释放锁。

      @Override
      public void run() {
        lock.lock();
        try {
          TimeUnit.SECONDS.sleep(1);
          lock.unlock();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
  5. 通过使用main()方法创建名为Main的类来创建示例的主类。

    public class Main {
    
      public static void main(String[] args) {
  6. 声明并创建一个名为lockReentrantLock对象。

        ReentrantLock lock=new ReentrantLock();
  7. 创建 10 个Task对象和 10 个线程来执行这些任务。启动调用run()方法的线程。

        for (int i=0; i<10; i++) {
          Task task=new Task(lock);
          Thread thread=new Thread(task);
          thread.run();
        }
      }
  8. 将项目导出为jar文件。叫它recipe8.jar。使用 IDE 的菜单选项或javacjar命令编译和压缩应用。

  9. 启动 FindBugs 独立应用,在 Windows 中运行findbugs.bat命令,或在 Linux 中运行findbugs.sh命令。 *** Create a new project with the New Project option of the File menu in the menu bar.

    How to do it...

    • The FindBugs application shows a window to configure the project. In the Project Name field introduce the text Recipe08. In the Classpath for analysis field add the jar file with the project and in the Source directories field add the directory with the source code of the example. Refer to the following screenshot:

    How to do it...

    • 点击分析按钮创建新项目并分析其代码。* FindBugs应用显示了代码分析的结果。在本例中,它发现了两个 bug。* 点击其中一个 bug,您将在右侧面板中看到该 bug 的源代码,并在屏幕底部的面板中看到该 bug 的描述。**

**## 它是如何工作的。。。

以下屏幕截图显示 FindBugs 的分析结果:

How it works...

分析检测到应用中存在以下两个潜在错误:

  • Taskrun()方法中的一个。如果抛出了一个InterruptedExeption异常,任务不会释放锁,因为它不会执行unlock()方法。这可能会导致应用出现死锁情况。
  • 另一个是在Main类的main()方法中,因为您直接调用了线程的run()方法,而不是start()方法来开始执行线程。

如果您双击两个 bug 中的一个,您将看到关于它的详细信息。由于您已经在项目的配置中包含了源代码参考,您还将看到检测到错误的源代码。下面的屏幕截图显示了一个示例:

How it works...

还有更多。。。

请注意,FindBugs 只能检测一些有问题的情况(与并发代码相关或不相关)。例如,如果您删除Task类的run()方法中的unlock()调用并重复分析,FindBugs 不会提醒您在任务中获得锁,但您永远不会释放它。

使用静态代码分析工具帮助提高代码质量,但不要期望检测到代码中的所有错误。

另见

  • 第 8 章测试并发应用中的配置 NetBeans 调试并发代码配方

配置 Eclipse 调试并发代码

如今,几乎每一位程序员,不管使用哪种编程语言,都会用 IDE 创建他们的应用。它们提供了许多集成在同一应用中的有趣功能,例如:

  • 项目管理
  • 自动代码生成
  • 自动文档生成
  • 与控制版本系统的集成
  • 用于测试应用的调试器
  • 创建应用项目和元素的不同向导

IDE 最有用的特性之一是调试器。您可以一步一步地执行应用,并分析程序中所有对象和变量的值。

如果您使用 Java 编程语言,Eclipse 是最流行的 IDE 之一。它有一个集成的调试器,允许您测试应用。默认情况下,当调试并发应用并且调试器找到断点时,它只停止具有该断点的线程,而其余线程继续执行。

在此配方中,您将学习如何更改该配置以帮助您测试并发应用。

准备好了吗

您必须已经安装了 EclipseIDE。打开它并选择一个项目,其中实现了一个并发应用,例如,书中实现的一个配方。

怎么做。。。

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

  1. 选择菜单选项窗口****首选项

  2. 在左侧菜单中,展开Java选项。

  3. In the left-hand side menu, select the Debug option. The following screenshot shows the appearance of that window:

    How to do it...

  4. 将新断点的默认挂起策略的值从挂起线程更改为挂起 VM(屏幕截图中用红色标记)。

  5. 点击确定按钮确认变更。

它是如何工作的。。。

正如我们在本配方介绍中提到的,默认情况下,当您在 Eclipse 中调试并发 Java 应用时,调试进程发现断点,它只挂起首先命中断点的线程,而其他线程继续执行。以下屏幕截图显示了该情况的示例:

How it works...

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

How it works...

通过更改,您可以看到所有线程都被挂起。您可以继续调试任意线程。选择最适合您需要的挂起策略。

配置 NetBeans 调试并发代码

在当今世界,软件对于开发能够正常工作、符合公司质量标准、且在未来可以在有限的时间内以尽可能低的成本轻松修改的应用是必不可少的。为了实现这一目标,必须使用一个 IDE,该 IDE 在一个公共接口下集成多个促进应用开发的工具(编译器和调试器)。

如果您使用 Java 编程语言,NetBeans 是最流行的 IDE 之一。它有一个集成的调试器,允许您测试您的应用。

在此配方中,您将学习如何更改该配置以帮助您测试并发应用。

准备好了吗

您应该安装 NetBeans IDE。打开它并创建一个新的 Java 项目。

怎么做。。。

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

  1. 创建一个名为Task1的类,并指定它实现Runnable接口。

    public class Task1 implements Runnable {
  2. 声明两个名为lock1lock2的私有Lock属性。

        private Lock lock1, lock2;
  3. 实现类的构造函数以初始化其属性。

        public Task1 (Lock lock1, Lock lock2) {
            this.lock1=lock1;
            this.lock2=lock2;
        }
  4. 执行run()方法。首先,使用lock()方法获得lock1对象的控制,并在控制台中写入一条消息,表明您已经获得了它。

     @Override
        public void run() {
            lock1.lock();
            System.out.printf("Task 1: Lock 1 locked\n");
  5. 然后,使用lock()方法获得lock2对象的控制,并在控制台中写入一条消息,表明您已经获得了它。

            lock2.lock();
            System.out.printf("Task 1: Lock 2 locked\n");
    Finally, release the two lock objects. First, the lock2 object and then the lock1 object.
            lock2.unlock();
            lock1.unlock();
        }
  6. 创建一个名为Task2的类,并指定它实现Runnable接口。

    public class Task2 implements Runnable{
  7. 声明两个名为lock1lock2的私有Lock属性。

        private Lock lock1, lock2;
  8. 实现类的构造函数来初始化其属性。

        public Task2(Lock lock1, Lock lock2) {
            this.lock1=lock1;
            this.lock2=lock2;
        }
  9. 执行run()方法。首先,使用lock()方法获得lock2对象的控制权,并在控制台中写入一条消息,表明您已经获得了它。

     @Override
        public void run() {
            lock2.lock();
            System.out.printf("Task 2: Lock 2 locked\n");
  10. 然后,使用lock()方法获得lock1对象的控制,并在控制台中写入一条消息,表明您已经获得了它。

```java
        lock1.lock();
        System.out.printf("Task 2: Lock 1 locked\n");
```
  1. 最后,释放两个锁定对象。首先是lock1对象,然后是lock2对象。
```java
        lock1.unlock();
        lock2.unlock();
    }
```
  1. 通过创建一个名为Main的类并向其添加main()方法来实现示例的主类。
```java
public class Main {
```
  1. 创建两个名为lock1lock2的锁对象。
```java
        Lock lock1, lock2;
        lock1=new ReentrantLock();
        lock2=new ReentrantLock();
```
  1. 创建一个名为task1Task1对象。
```java
        Task1 task1=new Task1(lock1, lock2);
```
  1. 创建一个名为task2Task2对象。
```java
        Task2 task2=new Task2(lock1, lock2);
```
  1. 使用两个线程执行这两个任务。
```java
        Thread thread1=new Thread(task1);
        Thread thread2=new Thread(task2);

        thread1.start();
        thread2.start();
```
  1. 当这两个任务尚未完成执行时,每 500 毫秒在控制台中写入一条消息。使用isAlive()方法检查线程是否已完成执行。
```java
        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()方法的println()方法的第一次调用中添加断点。
  2. Debug the program. You will see the Debugging window in the top left-hand side corner of the main NetBeans window. The next screenshot presents the appearance of that window with the thread that executes the Task1 object slept because they have arrived at the breakpoint and the other threads running:
![How to do it...](img/7881_08_09.jpg)
  1. Pause the execution of the main thread. Select that thread, right-click, and select the Suspend option. The following screenshot shows the new appearance of the Debugging window. Refer to the following screenshot:
![How to do it...](img/7881_08_10.jpg)
  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类的代码与此类似,但会更改锁的顺序。如果再次调试示例,将再次获得死锁,但在这种情况下,死锁检测器会检测到死锁,如下图所示:

How it works...

还有更多。。。

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

There's more...

在窗口上有两个选项控制前面描述的行为:

  • 新断点挂起:使用此选项,您可以配置 NetBeans 的行为,它在线程中查找断点。只能挂起具有断点的线程或应用的所有线程。
  • 步骤恢复:使用此选项,您可以在恢复线程时配置 NetBeans 的行为。您只能恢复当前线程或所有线程。

这两个选项都已在前面显示的屏幕截图中标记。

另见

  • 第 8 章测试并发应用中的配置 Eclipse 调试并发代码配方

用多线程测试并发代码

MultithreadedTC 是一个用于测试并发应用的 Java 库。它的主要目标是解决并发应用不确定性的问题。你无法控制他们的执行顺序。为此,它包括一个内部节拍器,用于控制构成应用的不同线程的执行顺序。这些测试线程被实现为类的方法。

在本配方中,您将学习如何使用多线程 EDTC 库实现对LinkedTransferQueue的测试。

准备好了吗

您还必须从下载多线程数据库 http://code.google.com/p/multithreadedtc/ 和 JUnit 库,来自的版本 4.10http://www.junit.org/ 。将文件junit-4.10.jarMultithreadedTC-1.01.jar添加到项目的库中。

怎么做。。。

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

  1. 创建一个名为ProducerConsumerTest的类来扩展MultithreadedTestCase类。

    public class ProducerConsumerTest extends MultithreadedTestCase {
  2. 声明一个私有的LinkedTransferQueue属性,该属性由名为queueString类参数化。

      private LinkedTransferQueue<String> queue;
  3. 执行initialize()方法。此方法不会接收任何参数,也不会返回任何值。它调用其父类的initialize()方法,然后初始化队列属性。

       @Override
      public void initialize() {
        super.initialize();
        queue=new LinkedTransferQueue<String>();
        System.out.printf("Test: The test has been initialized\n");
      }
  4. 执行thread1()方法。它将实现第一个使用者的逻辑。调用队列的take()方法,然后将返回值写入控制台。

      public void thread1() throws InterruptedException {
        String ret=queue.take();
        System.out.printf("Thread 1: %s\n",ret);
      }
  5. thread2()实现方法。它将实现第二个消费者的逻辑。首先,使用waitForTick()方法等待第一个线程在take()方法中休眠。然后调用队列的take()方法,将返回值写入控制台。

      public void thread2() throws InterruptedException {
        waitForTick(1);
        String ret=queue.take();
        System.out.printf("Thread 2: %s\n",ret);
      }
  6. thread3()实现方法。它将实现生产者的逻辑。首先,使用waitForTick()方法两次等待take()方法阻塞两个消费者。然后调用队列的put()方法,在队列中插入两个Strings

      public void thread3() {
        waitForTick(1);
        waitForTick(2);
        queue.put("Event 1");
        queue.put("Event 2");
        System.out.printf("Thread 3: Inserted two elements\n");
      }
  7. 最后,实施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");
      }
  8. 通过使用main()方法创建名为Main的类来实现示例的主类。

    public class Main {
    
      public static void main(String[] args) throws Throwable {
  9. 创建一个名为testProducerConsumerTest对象。

        ProducerConsumerTest test=new ProducerConsumerTest();
  10. 使用TestFramework类的runOnce()方法执行测试。

```java
     System.out.printf("Main: Starting the test\n");
    TestFramework.runOnce(test);
    System.out.printf("Main: The test has finished\n");
```

它是如何工作的。。。

在此配方中,您已经使用多线程 DC 库为LinkedTransferQueue类实现了一个测试。您可以使用此库及其 metronome 对任何并发应用或类实施测试。在本例中,您已经用两个消费者和一个生产者实现了经典的生产者/消费者问题。您想测试在缓冲区中引入的第一个String对象是否被到达缓冲区的第一个消费者消费,在缓冲区中引入的第二个String对象是否被到达缓冲区的第二个消费者消费。

多线程 DC 库基于 JUnit 库,JUnit 库是 Java 中实现单元测试最常用的库。要使用 MultithreadedTC 库实现基本测试,必须扩展MultithreadedTestCase类。该类扩展了junit.framework.AssertJUnit类,该类包括检查测试结果的所有方法。它没有扩展junit.framework.TestCase类,因此不能将多线程的测试与其他 JUnit 测试集成。

然后,您可以实现以下方法:

  • initialize():此方法的实现是可选的。它在启动测试时执行,因此您可以使用它初始化正在使用测试的对象。
  • finish():此方法的实现是可选的。它在测试完成后执行。您可以使用它来关闭或释放测试期间使用的资源,或者检查测试结果。
  • 实现测试的方法:这些方法具有您实现的测试的主要逻辑。它们必须以thread关键字开头,后跟字符串。例如,thread1()

要控制线程的执行顺序,可以使用waitForTick()方法。此方法接收一个integer类型作为参数,并将执行该方法的线程置于睡眠状态,直到阻塞测试中运行的所有线程。当它们被阻塞时,多线程 DC 库将恢复通过调用waitForTick()方法阻塞的线程。

作为waitForTick()方法的参数传递的整数用于控制执行顺序。多线程的 metronome 库有一个内部计数器。当所有线程都被阻塞时,库会将计数器增加到被阻塞的waitForTick()调用中指定的下一个数字。

在内部,当多线程数据库必须执行测试时,它首先执行initialize()方法。然后,它为每个方法创建一个以thread关键字开头的线程(在您的示例中,是方法thread1()thread2()thread3()),当所有线程都完成执行后,它执行finish()方法。为了执行测试,您使用了TestFramework类的runOnce()方法。

还有更多。。。

如果 MultithreadedTC 库检测到测试的所有线程都被阻塞,但在waitForTick()方法中没有一个线程被阻塞,那么测试将被声明为处于死锁状态,并引发java.lang.IllegalStateException异常。

另见

  • 第 8 章测试并发应用中的使用 FindBugs配方分析并发代码**