第四章线程执行器

Java 7 并发编程实战手册目录


代码下载(https://github.com/Wang-Jun-Chao/java-concurrency)


第四章线程执行器


4.1简介

  通常,使用Java来开发一个简单的并发应用程序时,会创建一些Runnable对象,然后创建对应的Thread对象来执行它们。但是,如果需要开发一个程序来运行大量的并发任务,这个方法将突显以下劣势:

  ♦必须实现所有与Thread对象管理相关的代码,比如线程的创建、结束以及结果获取;

  ♦需要为每一个任务创建一个Thread对象。如果需要执行大量的任务,这将大大地影响应用程序的处理能力;

  ♦计算机的资源需要高效地进行控制和管理,如果创建过多的线程,将会导致系统负荷过重。

  自从Java5开始,Java并发API提供了一套意在解决这些问题的机制。这套机制称之为执行器框架(Executor Framework ),围绕着Executor接口和它的子接口 ExecutorService,以及实现这两个接口的ThreadPoolExecutor类展开。

  这套机制分离了任务的创建和执行。通过使用执行器,仅需要实现Runnable接口的对象,然后将这些对象发送给执行器即可。执行器通过创建所需的线程,来负责这些 Runnable对象的创建、实例化以及运行。但是执行器功能不限于此,它使用了线程池来 提高应用程序的性能。当发送一个任务给执行器时,执行器会尝试使用线程池中的线程来 执行这个任务,避免了不断地创建和销毁线程而导致系统性能下降。

  执行器框架另一个重要的优势是Callable接口。它类似于Runnable接口,但是却提供了两方面的增强。

  ♦这个接口的主方法名称为call(),可以返回结果。

  ♦当发送一个Callable对象给执行器时,将获得一个实现了 Future接口的对象。 可以使用这个对象来控制Callable对象的状态和结果。

4.2创建线程执行器

  使用执行器框架(Executor Framework)的第一步是创建ThreadPoolExecutor对象。 可以ThreadPoolExecutor类提供的四个构造器或者使用Executors工厂类来创建 ThreadPoolExecutor对象。一旦有了执行器,就可以将Runnable或Callable对象发送给它去执行了。
在本节,我们将学习如何使用两种操作来实现一个范例,这个范列将模拟一个Web服务器来应对来自不同客户端的请求。

package com.concurrency.task;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 任务类
 */
public class Task implements Runnable {
    /**
     * 任务创建的时间
     */
    private Date initDate;
    /**
     * 任务的名称
     */
    private String name;

    /**
     * 构造函数,初始化属性
     *
     * @param name 任务的名称
     */
    public Task(String name) {
        this.initDate = new Date();
        this.name = name;
    }

    /**
     * 核心类,执行任务,等待一个随机的时间完成任务
     */
    @Override
    public void run() {
        System.out.printf("%s: Task %s: Created on: %s\n", Thread.currentThread().getName(), name, initDate);
        System.out.printf("%s: Task %s: Started on: %s\n", Thread.currentThread().getName(), name, new Date());

        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s: Task %s: Doing a task during %d seconds\n", Thread.currentThread().getName(), name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.printf("%s: Task %s: Finished on: %s\n", Thread.currentThread().getName(), name, new Date());
    }
}
package com.concurrency.task;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 服务类,模拟一个服务器
 */
public class Server {
    /**
     * 线程池执行器,管理请求线程的执行
     */
    private ThreadPoolExecutor executor;

    /**
     * 构造函数,创建线程执行器对象
     */
    public Server() {
        executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); // 本质是一个缓冲线程池对象
    }

    /**
     * 任务执行方法 接收Task对象作为参数并将其提交到执行者
     *
     * @param task Task对象
     */
    public void executeTask(Task task) {
        // 首先,写入一条信息到控制台,表明有一个新的任务到达。
        System.out.printf("Server: A new task has arrived\n");
        // 然后,调用执行者的execute()方法来提交这个任务
        executor.execute(task);
        // 最后,将执行者的数据写入到控制台来看它们的状态
        System.out.printf("Server: Pool Size: %d\n", executor.getPoolSize());
        System.out.printf("Server: Active Count: %d\n", executor.getActiveCount());
        System.out.printf("Server: Completed Tasks: %d\n", executor.getCompletedTaskCount());
    }

    /**
     * 结束执行的方法
     */
    public void endServer() {
        // 调用执行者的shutdown()方法来结束任务执行
        executor.shutdown();
    }
}
package com.concurrency.core;

import com.concurrency.task.Server;
import com.concurrency.task.Task;

public class Main {
    public static void main(String[] args) {
        Server server = new Server();

        // 发送100个任务到服务器对象,并且完成任务
        for (int i = 0; i < 100; i++) {
            Task task = new Task("Task " + i);
            server.executeTask(task);
        }

        server.endServer();
    }
}

这里写图片描述

图4.2-1 部分运行结果

  这个范例的核心在于Server类,这个类创建和使用ThreadPoolExecutor执行器来执行任务。
第一个关键点是在Server类的构造器中创建ThreadPoolExecutor对象。 ThreadPoolExecutor类有4个不同的构造器,但是,由于这些构造器在使用上的复杂性, Java并发API提供Executors工厂类来构造执行器和其他相关的对象。虽然可以直接通过 ThreadPoolExecutor其中之一的构造器来创建ThreadPoolExecutor对象,但是推荐使用 Executors工厂类来创建它。

  在这个示例中,通过使用Executors工厂类的newCachedThreadPool()方法创建了一个缓存线程池。这个方法返回一个ExecutorService对象,因此它将被强制转换为 ThreadPoolExecutor类型,并拥有所有的方法。如果需要执行新任务,缓存线程池就会创建新线程;如果线程所运行的任务执行完成后并且这个线程可用,那么缓存线程池将会重用这些线程。线程重用的优点是减少了创建新线程所花费的时间。然而,新任务固定会依赖线程来执行,因此缓存线程池也有缺点,如果发送过多的任务给执行器,系统的负荷将会过载。

  备注:仅当线程的数量是合理的或者线程只会运行很短的时间时,适合采用Executors 工厂类的newCachedThreadPool()方法来创建执行器。

  一旦创建了执行器,就可以使用执行器的execute()方法来发送Runnable或Callable类型的任务。这个范例发送实现了 Riirniabto接口的Task类型的对象给执行器。
范例中也打印了一些执行器相关的日志信息,专门使用了如下方法。

  ♦ getPoolSize():返回执行器线程池中实际的线程数。

  ♦ getActiveCount():返回执行器中正在执行任务的线程数。

  ♦ getCompletedTaskCount():返回执行器已经完成的任务数。

  执行器以及ThreadPoolExecutor类一个重要的特性是,通常需要显示地去结束它。如果不这样做,那么执行器将继续执行,程序也不会结束。如果执行器没有任务可执行了,它将继续等待新任务的到来,而不会结束执行。Java应用程序不会结束直到所有非守护线程结束它们的运行,因此,如果有终止执行器,应用程序将永远不会结束。

  为了完成执行器的执行,可以使用ThreadPoolExecutor类的shutdown()方法。当执行器执行完成所有待运行的任务后,它将结束执行。调用shutdown()方法之后,如果尝试再发送另一个任务给执行器,任务将被拒绝,并且执行器也将抛出 RejectedExecutionException 异常。

  ThreadPoolExecutor类提供了许多方法来获取自身状态的信息。在范例中,己经使用了 getPoolSize()方法来获取线程池的大小,用getActiveCount()方法来获取线程池中活动线程的数量,用getCompletedTaskCount()方法来获取执行器完成的任务数量。也可以使用getLargestPoolSize()方法来返回曾经同时位于线程池中的最大线程数。

  ThreadPoolExecutor类也提供了结束执行器的相关方法。

  ♦shutDownNow():这个方法会立即关闭执行器。执行器将不再执行那些正在等待执行的任务。这个方法将返回等待执行的任务列表。调用时,正在运行的任务将继续运行, 但是这个方法并不等待这些任务完成。

  ♦ isTerminated():如果调用了 shutdown()或shutdownNow()方法,并且执行器完成了关闭的过程,那么这个方法将返回true。

  ♦ isShutdown():如果调用了 shutdown()方法,那么这个方法将返回true。

  ♦ awaitTermination(long timeout, TimeUnit unit):这个方法将阻塞所调用的线程,直到执行器完成任务或者达到所指定的timeout值。

  TimeUnit 是一个枚举类,有如下的常量:DAYS、 HOURS、 MICROSECONDS、 MILLISECONDS、MINUTES、 NANOSECONDS 和 SECONDS。

  备注:如果想等待任务的结束,而不管任务的持续时间,可以使用一个大的超时时间, 比如DAYS。

4.3创建固定大小的线程执行器

  当使用 Executors 类的newCachedThreadPool()方法创建基本的 ThreadPoolExecutor时,执行器运行过程中将碰到线程数量的问题。如果线程池里没有空闲的线程可用,那么执行器将为接收到的每一个任务创建一个新线程,当发送大量的任务给执行器并且任务需要持续较长的时间时,系统将会超负荷,应用程序也将随之性能不佳。

  为了避免这个问题,Executors工厂类提供了一个方法来创建一个固定大小的线程执行器.这个执行器有一个线程数的最大值,如果发送超过这个最大值的任务给执行器,执行器将不再创建额外的线程,剩下的任务将被阻塞直到执行器有空闲的线程可用。这个特性可以保证执行器不会给应用程序带来性能不佳的问题。

  在本节,我们将通过修改本章4.2节的范例来学习如何创建固定大小的线程执行器。

package com.concurrency.task;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 任务类
 */
public class Task implements Runnable {
    /**
     * 任务创建的时间
     */
    private Date initDate;
    /**
     * 任务的名称
     */
    private String name;

    /**
     * 构造函数,初始化属性
     *
     * @param name 任务的名称
     */
    public Task(String name) {
        this.initDate = new Date();
        this.name = name;
    }

    /**
     * 核心类,执行任务,等待一个随机的时间完成任务
     */
    @Override
    public void run() {
        System.out.printf("%s: Task %s: Created on: %s\n", Thread.currentThread().getName(), name, initDate);
        System.out.printf("%s: Task %s: Started on: %s\n", Thread.currentThread().getName(), name, new Date());

        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s: Task %s: Doing a task during %d seconds\n", Thread.currentThread().getName(), name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.printf("%s: Task %s: Finished on: %s\n", Thread.currentThread().getName(), name, new Date());
    }
}
package com.concurrency.task;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 服务类,模拟一个服务器
 */
public class Server {
    /**
     * 线程池执行器,管理请求线程的执行
     */
    private ThreadPoolExecutor executor;

    /**
     * 构造函数,创建线程执行器对象
     */
    public Server() {
        executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5); // 本质是一个固定大小的线程池对象
    }

    /**
     * 任务执行方法 接收Task对象作为参数并将其提交到执行者
     *
     * @param task Task对象
     */
    public void executeTask(Task task) {
        // 首先,写入一条信息到控制台,表明有一个新的任务到达。
        System.out.printf("Server: A new task has arrived\n");
        // 然后,调用执行者的execute()方法来提交这个任务
        executor.execute(task);
        // 最后,将执行者的数据写入到控制台来看它们的状态
        System.out.printf("Server: Pool Size: %d\n", executor.getPoolSize());
        System.out.printf("Server: Active Count: %d\n", executor.getActiveCount());
        System.out.printf("Server: Task Count: %d\n",executor.getTaskCount());
        System.out.printf("Server: Completed Tasks: %d\n",executor.getCompletedTaskCount());
    }

    /**
     * 结束执行的方法
     */
    public void endServer() {
        // 调用执行者的shutdown()方法来结束任务执行
        executor.shutdown();
    }
}
package com.concurrency.core;

import com.concurrency.task.Server;
import com.concurrency.task.Task;

public class Main {
    public static void main(String[] args) {
        Server server = new Server();

        // 发送100个任务到服务器对象,并且完成任务
        for (int i = 0; i < 100; i++) {
            Task task = new Task("Task " + i);
            server.executeTask(task);
        }

        server.endServer();
    }
}

这里写图片描述

图4.3-1 部分运行结果

  在这个示例中,使用Executors工厂类的newFixedThreadPool〇方法来创建执行器。这个方法创建了具有线程最大数量值的执行器。如果发送超过线程数的任务给执行器,剩余的任务将被阻塞直到线程池里有空闲的线程来处理它们,newFixedThreadPool()方法接收执行器将拥有的线程数量的最大值作为参数。这个例子创建了一个线程数量的最大值为 5的执行器。

  为了在程序中输出相关信息,己经使用的ThreadPoolExecutor类的一些方法如下。

  ♦ getPoolSize():返回执行器中线程的实际数量。

  ♦ getActiveCount():返回执行器正在执行任务的线程数量。

  将看到,控制台输出的信息是5,表示执行器拥有5个线程,并且执行器不会超过这个最大的线程连接数。
当发送最后一个任务给执行器时,由于执行器只有S个活动的线程,所以剩余的95 个任务只能等待空闲线程。getTaskCommit()方法可以用来显示有多少个任务已经发送给执行器。

  Executors工厂类也提供newSingleThreadExecutorO方法。这是一个创建固定大小线程执行器的极端场景,它将创建一个只有单个线程的执行器。因此,这个执行器只能在同一时间执行一个任务。

4.4在执行器中执行任务并返回结果

  执行器框架(ExecuterFramework)的优势之一是,可以运行并发任务并返回结果。 Java并发API通过以下两个接口来实现这个功能。

  Callable:这个接口声明了 call()方法。可以在这个方法里实现任务的具体逻辑操作。 Callable接口是一个泛型接口,这就意味着必须声明Call〇方法返回的数据类型。

  future:这个接口声明了一些方法来获取由Callable对象产生的结果,并管理它们的状态。

  在本节,我们将学习如何实现任务的返回结果,并在执行器中运行任务。

package com.concurrency.task;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 阶乘计算类,实现Callable接口,并参数化为Integer类型
 */
public class FactorialCalculator implements Callable<Integer> {
    /**
     * 声明一个私有的,类型为Integer,名为number的属性,用来存储任务将要计算的数
     */
    private Integer number;

    /**
     * 构造函数,初始化
     * @param number  将要计算的数
     */
    public FactorialCalculator(Integer number) {
        this.number = number;
    }

    /**
     * 核心方法,返回阶乘计算的结果
     * @return 阶乘计算的结果
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        int num, result;
        num = number;
        result = 1;

        // 如果数是1或0,则返回1。否则,计算这个数的阶乘。为了演示效果,在两次乘之间,令这个任务睡眠20毫秒。
        if (num == 0 || num == 1) {
            return 1;
        } else {
            for (int i = 2; i <= number; i++) {
                result *= i;
                TimeUnit.MICROSECONDS.sleep(20);
            }

        }
        // 操作结果的信息写入控制台。
        System.out.printf("%s: %d\n", Thread.currentThread().getName(), result);
        // 返回操作结果。
        return result;
    }
}
package com.concurrency.core;

import com.concurrency.task.FactorialCalculator;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

public class Main {
    public static void main(String[] args) {
        // 创建固定大小的线程池执行器,最多可以同时执行2个线程。
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
        // 创建一个存储未来象的列表,未来对象关联着任务的任务的执行,并且可以获取执行的结果
        List<Future<Integer>> resultList = new ArrayList<>();
        // 创建一个随机数生成对象
        Random random = new Random();

        // 创建10个任务并且将它们送到执行器中
        for (int i = 0; i < 10; i++) {
            // 生成[0, 10)之间的10个随机数
            Integer number = random.nextInt(10);
            FactorialCalculator calculator = new FactorialCalculator(number);
            // 调用执行器的submit()方法来提交FactorialCalculator任务给执行者。
            // 这个方法返回Future<Integer>对象来管理任务,并且最终获取它的结果。
            Future<Integer> result = executor.submit(calculator);
            // 将结果存储到队列当中
            resultList.add(result);
        }
        // 创建一个do循环来监控执行者的状态,等待10个线程都完成任务。
        do {
            // 首先,写入信息到控制台,使用执行器的getCompletedTaskNumber()方法获得的已完成的任务数。
            System.out.printf("Main: Number of Completed Tasks: %d\n", executor.getCompletedTaskCount());
            // 然后,对于队列中的10个Future对象,使用isDone()方法,将信息写入(到控制台)表明它们所管理的任务是否已经完成
            for (int i = 0; i < resultList.size(); i++) {
                Future<Integer> result = resultList.get(i);
                System.out.printf("Main: Task %d: %s\n", i, result.isDone());
            }

            try {
                // 主线程休眠50毫秒
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 如果执行器中的已完成任务数小于10,重复这个循环。
        } while (executor.getCompletedTaskCount() < resultList.size());

        // 将获得的每个任务的结果写入控制台。对于每个Future对象,通过它的任务使用get()方法获取返回的Integer对象。
        System.out.printf("Main: Results\n");
        for (int i = 0; i < resultList.size(); i++) {
            Future<Integer> result = resultList.get(i);
            Integer number = null;
            try {
                number = result.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            // 在控制台输出结果
            System.out.printf("Core: Task %d: %d\n", i, number);
        }

        // 最后,调用执行器的shutdown()方法来结束这个执行器。
        executor.shutdown();

    }
}

这里写图片描述

图4.4-1 运行结果

  在本节,我们学习了如何使用Callable接口来启动并发任务并返回结果。我们编写了 FactorialCalculator类,它实现了带有泛型参数Integer类型的Callable接口。因此,这个Integer类型将作为在调用call()方法时返回的类型。

  范例的另一个关键点在Main主类中。我们通过submit()方法发送一个Callable对象给执行器去执行,这个submit()方法接收Callable对象作为参数,并返回Future对象。Future对象可以用于以下两个主要目的。

  ♦控制任务的状态:可以取消任务和检查任务是否已经完成。为了达到这个目的,可使用isDone()方法来检査任务是否己经完成。

  ♦通过call()方法获取返回的结果。为了达到这个目的,可使用get()法。这个方法一直等待直到Callable对象的call()方法执行完成并返回结果。如果get()方法在等待结果时线程中断了,则将抛出一个InterruptedException异常。如果call()方法抛出异常那么get()方法将随之抛出ExecutionException异常。
在调用Future对象的get()方法时,如果Future对象所控制的任务并未完成,那么这个方法将一直阻塞到任务完成。Future接口也提供了 get()方法的其他调用方式。

  ♦ get(long timeout, TimeUnit unit):如果调用这个方法时,任务的结果并未准备好,则方法等待所指定的timeout时间。如果等待超过了指定的时间而任务的结果还没有准备好,那么这个方法将返回null。
TimeUnit 是一个枚举类,有如下的常量:DAYS、 HOURS、 MICROSECONDS、 MILLISECONDS、MINUTES、 NANOSECONDS 和 SECONDS。

4.5运行多个任务并处理第一个结果

  并发编程比较常见的一个问题是,当采用多个并发任务来解决一个问题时,往往只关心这些任务中的第一个结果。比如,对一个数组进行排序有很多种算法,可以并发启动所有算法,但是对于一个给定的数组,第一个得到排序结果的算法就是最快的排序算法。

  在本节,我们将学习如何使用ThreadPoolExecutor类来实现这个场景。范例允许用 户可以通过两种验证机制进行验证,但是,只要有一种机制验证成功,那么这个用户就被 验证通过了。

package com.concurrency.task;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 用户检验类,实现用户验证过程
 */
public class UserValidator {
    /**
     * 声明一个私有的、类型为String、名为name的属性,用来存储系统验证用户的名称。
     */
    private String name;

    /**
     * 构造函数,初始化用户名称
     *
     * @param name 用户名称
     */
    public UserValidator(String name) {
        this.name = name;
    }

    /**
     * 验证方法,根据用户名和密码进行验
     *
     * @param name     用户名
     * @param password 密码
     * @return true验证通过,false验证失败
     */
    public boolean validate(String name, String password) {
        // 创建Random对象,名为random。
        Random random = new Random();

        // 等待一个随机时间,用来模拟用户验证的过程
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("Validator %s: Validating a user during %d seconds\n", this.name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            return false;
        }

        // 返回一个随机Boolean值。如果用户验证通过,这个方法将返回true,否则,返回false。
        return random.nextBoolean();
    }

    /**
     * 返回name属性值
     *
     * @return name属性值
     */
    public String getName() {
        return name;
    }
}
package com.concurrency.task;

import java.util.concurrent.Callable;

/**
 * 任务验证类,用来执行UserValidation对象作为并发任务的验证过程。指定它实现Callable接口,并参数化为String类型。
 */
public class TaskValidator implements Callable<String> {
    /**
     * 声明一个私有的、类型为UserValidator、名为validator的属性。
     */
    private UserValidator validator;
    /**
     * 声明私有的、类型为String、名为user的属性。
     */
    private String user;
    /**
     * 声明私有的、类型为String、名为password的属性。
     */
    private String password;

    /**
     * 构造函数,初始化属性
     *
     * @param validator 用户验证对象
     * @param user      用户名
     * @param password  用户密码
     */
    public TaskValidator(UserValidator validator, String user, String password) {
        this.validator = validator;
        this.user = user;
        this.password = password;
    }

    /**
     * 核心方法,使用用户验证对象进行用户名和密码验证,如果验证通过就返回验证的名字,否则就抛出异常
     *
     * @return 验证的名字
     * @throws Exception 验证不通过就抛出异常
     */
    @Override
    public String call() throws Exception {
        // 如果用户没有通过UserValidator对象验证,写入一条信息到控制台,表明这种情况,并且抛出一个Exception异常
        if (!validator.validate(user, password)) {
            System.out.printf("%s: The user has not been found\n", validator.getName());
            throw new Exception("Error validating user");
        }

        // 否则,写入一条信息到控制台表明用户已通过验证,并返回UserValidator对象的名称。
        System.out.printf("%s: The user has been found\n", validator.getName());
        return validator.getName();
    }
}
package com.concurrency.core;

import com.concurrency.task.TaskValidator;
import com.concurrency.task.UserValidator;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        // 创建两个String对象,一个名为name,另一个名为password,使用”test”值初始化它们。
        String username = "test";
        String password = "test";

        // 创建两个UserValidator对象,一个名为ldapValidator,另一个名为dbValidator。
        UserValidator ldapValidator = new UserValidator("LDAP");
        UserValidator dbValidator = new UserValidator("DataBase");

        // 创建两个TaskValidator对象,分别为ldapTask和dbTask。分别使用ldapValidator和dbValidator初始化它们。
        TaskValidator ldapTask = new TaskValidator(ldapValidator, username, password);
        TaskValidator dbTask = new TaskValidator(dbValidator, username, password);

        // 创建TaskValidator队列,添加两个已创建的对象(ldapTask和dbTask)
        List<TaskValidator> taskList = new ArrayList<>();
        taskList.add(ldapTask);
        taskList.add(dbTask);

        // 使用Executors类的newCachedThreadPool()方法创建一个新的ThreadPoolExecutor对象和一个类型为String,
        // 名为result的变量。
        ExecutorService executor = (ExecutorService) Executors.newCachedThreadPool();
        String result;
        try {
            // 调用executor对象的invokeAny()方法。该方法接收taskList参数,返回String类型。
            // 同样,它将该方法返回的String对象写入到控制台。
            result = executor.invokeAny(taskList);
            System.out.printf("Main: Result: %s\n", result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // 使用shutdown()方法结束执行者,写入一条信息到控制台,表明程序已结束。
        executor.shutdown();
        System.out.printf("Main: End of the Execution\n");
    }
}

这里写图片描述

图4.5-1 运行结果

这里写图片描述

图4.5-2 运行结果

  这个范例的关键点在Main主类中,ThreadPoolExecutor的invokeAny方法接收到一个任务列表,然后运行任务,并返回第一个完成任务并且没有抛出异常的任务的执行结果。这个方法返回的类型与任务里的call方法返回的类型相同,在这个范例中,它将返回String类型值。

  范例中有两个UserVulidator对象,它们返回随机的boolean值。每一个UserValidator 对象被TuskValidator对象使用,TaskValidator对象实现了 Callable接口。如果 UserValidator 类的 validate()方法返回 false 值,那么 TaskValidator 类将抛出 Exception 异常,否则,返回true值。

  因此,我们有两个任务可以返回true值或抛出Exception异常。从而,可以有如下4 种可能性。

  ♦如果两个任务都返回true值,那么invokeAny()方法的结果就是首先完成任务的名称。

  ♦如果第一个任务返回true值,第二个任务抛出Exception异常,那么invokeAny()方法的结果就是第一个任务的名称。

  ♦如果第一个任务抛出Exception异常,第二个任务返回true值,那invokeAny()方法的结果就是第二个任务的名称。

  ♦ 如果两个任务都抛出Exception异常,那么invokeAny()方法将抛出 ExecutionException 异常。
将这个范例多运行几次,那么将得到如上所述的四种可能的结果。

  ThreadPoolExecutor类还提供了 invokeAny()方法的其他版本:

  invokeAny(Collection

4.6运行多个任务并处理所有结果

  执行器框架(Executor Framework)允许执行并发任务而不需要去考虑线程创建和执行。它还提供了可以用来控制在执行器中执行任务的状态和获取任务运行结果的 Future 类。

  如果想要等待任务结束,可以使用如下两种方法。

  ♦如果任务执行结束,那么Future接口的isDone()方法将返回true。

  ♦在调用 shutdown〇方法后,ThreadPoolExecutor 类的 awaitTermination〇方法会将线程休眠,直到所有的任务执行结束。

  这两个方法有一些缺点:第一个方法,仅可以控制任务的完成与否;第二个方法,必须关闭执行器来等待一个线程,否则调用这个方法线程将立即返回。

  ThreadPoolExecutor类还提供一个方法,它允许发送一个任务列表给执行器,并等待 列表中所有任务执行完成。在本节,我们将编写范例,执行三个任务,当它们全部执行结束后打印出结果信息,用来学习如何使用这个特性。

package com.concurrency.task;

/**
 * 结果类,存储中并发任务产生的结果
 */
public class Result {
    /**
     * 产生结果的任务的名字
     */
    private String name;
    /**
     * 产生的结果值
     */
    private int value;

    /**
     * 返回任务的名字
     *
     * @return 任务的名字
     */
    public String getName() {
        return name;
    }

    /**
     * 设备任务的名字
     *
     * @param name 任务的名字
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 返回结果
     *
     * @return 结果值
     */
    public int getValue() {
        return value;
    }

    /**
     * 设置结果
     *
     * @param value 结果值
     */
    public void setValue(int value) {
        this.value = value;
    }
}
package com.concurrency.task;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 任务类,实现Callable接口,参数化为Result类型。
 */
public class Task implements Callable<Result> {
    /**
     * 任务的名称
     */
    private String name;

    /**
     * 构造函数
     *
     * @param name 初始化任务的名称
     */
    public Task(String name) {
        this.name = name;
    }

    /**
     * 核心方法,等待一个随机时间,并且计算5个随机数的和
     *
     * @return 个随机数的和
     * @throws Exception
     */
    @Override
    public Result call() throws Exception {
        // 向控制台输出信息
        System.out.printf("%s: Staring\n", this.name);

        // 等待一个随机的时间
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s: Waiting %d seconds for results.\n", this.name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 计算随机数的和
        int value = 0;
        for (int i = 0; i < 5; i++) {
            value += (int) (Math.random() * 100);

        }

        // 创建一个结果对象
        Result result = new Result();
        result.setName(this.name);
        result.setValue(value);
        System.out.printf("%s: Ends\n", this.name);

        // 返回结果对象
        return result;
    }
}
package com.concurrency.core;

import com.concurrency.task.Result;
import com.concurrency.task.Task;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 使用Executors类的newCachedThreadPool()方法,创建ThreadPoolExecutor对象。
        ExecutorService executor = Executors.newCachedThreadPool();

        // 创建三个Task对象,并且将他们存储在一个链表当中
        List<Task> taskList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Task task = new Task("Task-" + i);
            taskList.add(task);
        }

        // 创建Future对象列表,参数化为Result类型。
        List<Future<Result>> resultList = null;
        try {
            // 调用ThreadPoolExecutor类的invokeAll()方法。这个类将会返回之前创建的Future对象列表。
            resultList = executor.invokeAll(taskList);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 使用shutdown()方法结束执行器的执行。
        executor.shutdown();

        // 将结果写入控制台
        System.out.printf("Core: Printing the results\n");
        for (Future<Result> future : resultList) {
            try {
                Result result = future.get();
                System.out.printf("%s: %s\n", result.getName(), result.getValue());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

这里写图片描述

图4.6-1 运行结果

  在本节,我们学习了如何发送任务列表给执行器,并且通过invokeAll()方法等待所有任务的完成。这个方法接收一个Callable对象列表,并返回一个Future对象列表。在这个列表中,每一个任务对应一个Future对象。Future对象列表中的第一个对象控制Callable列表中第一个任务,以此类推。

  需要注意的一点是,在存储结果的列表声明中,用在Future接口中的泛型参数的数据类型必须与Callable接口的泛型数据类型相兼容。在这个例子中,我们使用的是相同的数据类型:Result类。

  另一个关于invokeAll()方法重要的地方是,使用Future对象仅用来获取任务的结果。当所有的任务执行结束时这个方法也执行结束了,如果在返回的Future对象上调用isDone()方法,那么所有的调用将返回true值。

  ExecutorService接口还提供了invokeAll()方法的另一个版本:

  ♦invokeAll(Collection

4.7在执行器中延时执行任务

  执行器框架(ExecutorFramework)提供了 ThreadPoolExecutor类并采用线程池来执行Callable和Runnable类型的任务,采用线程池可以避免所有线程的创建操作而提髙应用程序的性能。当发送一个任务给执行器时,根据执行器的相应配置,任务将尽可能快地被执行。但是,如果并不想让任务马上被执行,而是想让任务在过一段时间后才被执行,或者任务能够被周期性地执行。为了达到这个目的,执行器框架提供了ScheduledThreadPoolExecutor类。

  在本节,我们将学习如何创建ScheduledThreadPoolExecutor执行器,以及如何使用它在经过一个给定的时间后开始执行任务。

package com.concurrency.task;

import java.util.Date;
import java.util.concurrent.Callable;

/**
 * 任务类,实现Callable接口,参数化为String类型。
 */
public class Task implements Callable<String> {
    /**
     * 任务名称
     */
    private String name;

    /**
     * 构造函数,初始化任务名称
     *
     * @param name 任务名称
     */
    public Task(String name) {
        this.name = name;
    }

    /**
     * 核心方法,输出任务执行时间
     *
     * @return 执行结果字符串
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        System.out.printf("%s: Starting at : %s\n", name, new Date());
        return "Hello, world";
    }
}
package com.concurrency.core;

import com.concurrency.task.Task;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建一个定时线程池执行器对象
        ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);

        // 使用ScheduledThreadPoolExecutor实例的schedule()方法,初始化和开始一些任务(5个任务)。
        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);
        }

        // 关闭线程执行器对象
        executor.shutdown();

        // 等待线程执行器的完成
        try {
            // 所有线程必须在24小时内完成,否则就终止未完成的线程
            executor.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出完成消息
        System.out.printf("Core: Ends at: %s\n", new Date());
    }
}

这里写图片描述

图4.7-1 运行结果

  这个范例的关键点在于Main主类和ScheduledThreadPoolExecutor执行器的管理器,虽然可以通过ThreadPoolExecutor类来创建定时执行器,但是在Java并发API中则推荐利用Executors工厂类来创建。在这个范例中,必须使用newScheduledThreadPool()方法,并且传递数字1作为方法的参数,这个参数就是线程池里拥有的线程数。为了在定时执行器中等待一段给定的时间后执行一个任务,需要使用schedule()方法。这个方法接收如下的参数:

  ♦即将执行的任务:

  ♦任务执行前所要等待的时间;

  ♦等待时间的单位,由TimeUnit类的一个常量来指定。

  在这个示例中,每个任务将等待N秒(TimeUnit.SECONDS),这个N值则等于任务在数组中的位置加1。

  备注:如果想在一个给定的时间点来定时执行任务,那就需要计算这个给定时间点和 当前时间的差异值,然后用这个差异值作为任务的延迟值。

  从结果可知,每隔1秒钟就有一个任务开始执行;这是因为所有的任务被同时发送到执行器,但每个任务都比前一个任务延迟了1秒钟。

  也可以使用Runnable接口来实现任务,因为ScheduledThreadPoolExecutor类的 schedule()方法可以同时接受这两种类型的任务。

  虽然 ScheduledThreadPoolExecutor 类是 ThreadPoolExecutor 类的子类,因而继承 了 ThreadPooffixecutor类所有的特性。但是,Java推荐仅在开发定时任务程序时采用 ScheduledThreadPoolExecutor类。

  最后,在调用shutdown()方法而仍有待处理的任务需要执行时,可以配置 ScheduledThreadPoolExecutor的行为。默认的行为是不论执行器是否结束,待处理的任务仍将被执行。但是,通过调用 ScheduledThreadPoolExecutor 类的 setExecuteExistingDelayedTasksAfterShutdownPolicy()方法则可以改变这个行为。传递false参数给这个方法,执行shutdown〇方法后,待处理的任务将不会被执行。

4.8在执行器中周期性执行任务

  执行器框架(ExecutorFramework)提供了 ThreadPoolExecutor类,通过线程池来执行并发任务从而避免了所有线程的创建操作。当发送一个任务给执行器后,根据执行器的配置,它将尽快地执行这个任务。当任务执行结束后,这个任务就会从执行器中删除; 如果想再次执行这个任务,则需要再次发送这个任务到执行器。

  但是,执行器框架提供了ScheduledThreadlPoolExecutor类来执行周期性的任务,在本节,我们将学习如何使用这个类的功能来计划执行周期性的任务。

package com.concurrency.task;

import java.util.Date;

/**
 * 任务类,执行任务
 */
public class Task implements Runnable {
    /**
     * 任务的名称
     */
    private String name;

    /**
     * 构造函数,初始化任务名称
     *
     * @param name 任务名称
     */
    public Task(String name) {
        this.name = name;
    }

    /**
     * 核心方法,向控制台输出当前执行的时间
     */
    @Override
    public void run() {
        System.out.printf("%s: Executed at: %s\n",name,new Date());
    }
}
package com.concurrency.core;

import com.concurrency.task.Task;

import java.util.Date;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建一个定时行器服务对象
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        System.out.printf("Main: Starting at: %s\n", new Date());

        // 创建一个执行任务,并且将它放入执行器中,初始延迟是1秒,周期是2秒
        Task task = new Task("Task");
        ScheduledFuture<?> result = executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);

        // 控制任务的执行
        for (int i = 0; i < 10; i++) {
            System.out.printf("Main: Delay: %d\n", result.getDelay(TimeUnit.MILLISECONDS));
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 关闭执行器对象
        executor.shutdown();
        System.out.printf("Main: No more tasks at: %s\n", new Date());

        // 验证在执行器关闭后,周期性任务不会执行
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出完成信息
        System.out.printf("Main: Finished at: %s\n", new Date());
    }
}

这里写图片描述

图4.8-1 运行结果

  想要通过执行器框架来执行一个周期性任务时,需要一个ScheduledExecutorService对象。 同创建执行器一样,在Java中推荐使用Executors工厂类来创建ScheduledExecutorService对 象。Executors类就是执行器对象的工厂。在这个例子中,可以使用newScheduledThreadPool()方法来创建一个ScheduledExecutorService对象。这个方法接收一个表示线程池中的线程数来作为参数。在这个范例中,因为仅有一个任务,所以只需要传递数字1作为参数即可。

  一旦有了可以执行周期性任务的执行器,就可以发送任务给这个执行器。在范例中,我们使用scheduledAtFixedRate()法发送任务。这个方法接收4个参数,分别为将被周期性执行的任务,任务第一次执行后的延时时间,两次执行的时间周期,以及第2个和第3个参数的时间单位。这个单位是TimeUnit枚举的常量。TimeUnit是一个枚举类,有如下 的常量:DAYS、HOURS、MICROSECONDS、 MILLISECONDS、 MINUTES、 NANOSECONDS和SECONDS。

  另一个需要注意的是,两次执行之间的周期是指任务在两次执行开始时的时间间隔。如果有一个周期性的任务需要执行5秒钟,但是却让它每3秒钟执行一次,那么,在任 执行的过程中将会有两个任务实例同时存在。

  scheduleAtFixedRate()方法返回一个ScheduledFuture对象,ScheduledFuture接口则扩展了Future接口,于是它带有了定时任务的相关操作方法。ScheduledFuture是一个泛型参数化的接口。在这个示例中,任务是Runnable对象,并没有泛型参数化,必须通过?符号作为参数来泛型化它们。

  我们己经使用过ScheduledFuture接口中的一个方法。getDelay()方法返回任务到下一次执行时所要等待的剩余时间。这个方法接收一个TimeUnit常量作为时间单位。图4.8-1运行结果,通过控制台的信息.可以看到任务是每2秒执行一次:剩余的延迟时间会每隔500毫秒在控制台上输出,这个500毫秒则是主线程将被休眠的时间。当关闭执行器时,定时任务将结束执行,然后在控制台上也看不到更多的信息了。

  ScheduledExecutorService类还提供了其他方法来安排周期性任务的运行,比如, scheduleWithFixedRate()方法。这个方法与scheduledAtFixedRate()方法具有相同的参数,但是略有一些不同需要引起注意。在scheduledAtFixedRate()方法中,第3个参数表示任务两次执行开始时间的间隔,而在scheduledWithFixedDelay()方法中,第3个参数则是表示任务上一次执行结束的时间与任务下一次开始执行的时间的间隔。

  也可以配置ScheduledThreadPoolExecutor实现shutdown()方法的行为,默认行为是当调用shutdown()方法后,定时任务就结束了。可以通过ScheduledThreadPoolExecutor 类的 setContinueExistingPeriodicTasksAfterShutdownPolicy()方法来改变这个行为,传递参数true给这个方法,这样调用shutdown()方法后,周期性任务仍将继续执行。

4.9在执行器中取消任务

  使用执行器时,不需要管理线程,只需要实现Runnable或Callable任务并发送任务给执行器即可。执行器负责创建线程,管理线程池中的线程,当线程不再需要时就销毁它们。有时候,我们可能需要取消已经发送给执行器的任务。在这种情况下,可以使用 Future接口的cancel()方法来执行取消操作。在本节,我们将学习如何使用这个方法来 取消已经发送给执行器的任务。

package com.concurrency.task;

import java.util.concurrent.Callable;

public class Task implements Callable<String> {
    /**
     * 核心方法,一个无限循环的任务,每100毫秒向控制台写一个消息
     * @return
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        while (true){
            System.out.printf("Task: Test\n");
            Thread.sleep(100);
        }
    }
}
package com.concurrency.core;

import com.concurrency.task.Task;

import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建一个执行器
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();

        // 创建一个任务
        Task task = new Task();

        System.out.printf("Main: Executing the Task\n");

        // 把任务发送到执行器
        Future<String> result = executor.submit(task);

        // 休眠两秒钟
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 取消一个任务完成他的执行
        System.out.printf("Main: Cancelling the Task\n");
        result.cancel(true);

        // 验证任务是否被取消
        System.out.printf("Main: Cancelled: %s\n", result.isCancelled());
        System.out.printf("Main: Done: %s\n", result.isDone());

        // 关闭执行器
        executor.shutdown();
        System.out.printf("Main: The executor has finished\n");
    }
}

这里写图片描述

图4.9-1 运行结果

  如果想取消一个已经发送给执行器的任务,可以使用Future接口的Cancel()方法。 根据调用cancel()方法时所传递的参数以及任务的状态,这个方法的行为有些不同。

  ♦如果任务已经完成,或者之前己经被取消,或者由于某种原因而不能被取消,那么方法将返回false并且任务也不能取消。

  ♦如果任务在执行器中等待分配Thread对象来执行它,那么任务被取消,并且不会开始执行。如果任务已经在运行,那么它依赖于调用cancel()方法时所传递的参数。如果传递的参数为true并且任务正在运行,那么任务将被取消。如果传递的参数为false并且任务正在运行,那么任务不会被取消。

  如果Future对象所控制任务己经被取消,那么使用Future对象的get()方法时将抛出 CancellationException 异常。

4.10在执行器中控制任务的完成

  FutureTask类提供了一个名为done()的方法,允许在执行器中的任务执行结束之后,还可以执行一些代码。这个方法可以被用来执行一些后期处理操作,比如:产生报表, 通过邮件发送结果或释放一些系统资源。当任务执行完成是受FutureTask类控制时,这个方法在内部被FutureTask类调用。在任务结果设置后以及任务的状态己改变为isDone 之后,无论任务是否被取消或者正常结束,done()方法才被调用。

  默认情况下,done()方法的实现为空,即没有任何具体的代码实现。我们可以覆盖 FutureTask类并实现done()方法来改交这种行为。在本节,我们将学习如何覆盖这个方法, 并在任务结朿后执行这些代码。

package com.concurrency.task;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 任务执行类,并指定其实现Callable接口,参数化为String类型。
 */
public class ExecutableTask implements Callable<String> {
    /**
     * 任务名称
     */
    private String name;

    public ExecutableTask(String name) {
        this.name = name;
    }

    /**
     * 核心方法,等待一个随机时间,返回一个结果
     *
     * @return 字符串结果
     * @throws Exception
     */
    @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) {
            //e.printStackTrace();
        }
        return "Hello, world. I'm " + name;
    }

    /**
     * 获取任务名称
     *
     * @return 任务名称
     */
    public String getName() {
        return name;
    }
}
package com.concurrency.task;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * 结果任务类,这个类管理着可执行任务类的执行
 */
public class ResultTask extends FutureTask<String> {
    /**
     * 结果任务的名称
     */
    private String name;

    /**
     * 构造函数
     *
     * @param callable 可调用的接口对象
     */
    public ResultTask(Callable<String> callable) {
        super(callable);
        this.name = ((ExecutableTask) callable).getName();
    }

    /**
     * 当任务完成时调用这个方法
     */
    @Override
    protected void done() {
        if (isCancelled()) {
            System.out.printf("%s: Has been cancelled\n", name);
        } else {
            System.out.printf("%s: Has finished\n", name);
        }
    }
}
package com.concurrency.core;

import com.concurrency.task.ExecutableTask;
import com.concurrency.task.ResultTask;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        // 创建一个执行器对象
        ExecutorService executor = (ExecutorService) Executors.newCachedThreadPool();

        // 创建5个任务
        ResultTask resultTasks[] = new ResultTask[5];
        for (int i = 0; i < 5; i++) {
            ExecutableTask executableTask = new ExecutableTask("Task " + i);
            resultTasks[i] = new ResultTask(executableTask);
            executor.submit(resultTasks[i]);
        }

        // 休眠5秒钟
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        // 取消所有的任务,如果任务在取消之前已经完,取消操作对任务没有任命影响
        for (ResultTask resultTask : resultTasks) {
            resultTask.cancel(true);
        }

        // 输出未被取消的任务的结果
        for (ResultTask resultTask : resultTasks) {
            try {
                if (!resultTask.isCancelled()) {
                    System.out.printf("%s\n", resultTask.get());
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        // 关闭执行器
        executor.shutdown();
    }
}

这里写图片描述

图4.10-1 运行结果

  当任务执行结束时,FutureTask类就会调用done()方法。在这个范例中,我们实现了一个 Callable 类、一个 ExecutableTask 类以及一个 FutureTask 类的子类 ResultTask, 这个子类用来控制ExecutableTask对象的执行。

  在创建好返回值以及改变任务状态为isDone之后,FutureTask类就会在内部调用 done()方法。虽然我们无法改变任务的结果值,也无法改变任务的状态,但是可以通过任务来关闭系统资源、输出日志信息、发送通知等。

4.11在执行器中分离任务的启动与结果的处理

  通常情况下,使用执行器来执行并发任务时,将Runnable或Callable任务发送给执行器,并获得Future对象来控制任务。此外,还会碰到如下情形,需要在一个对象里发送任务给执行器,然后在另一个对象里处理结果。对于这种情况,Java并发API提供了 CompletionService 类。

  CompMonService类有4方法用来发送任务给执行器,还有一个方法为下一个己经执行结束的任务获取Future对象。从内部实现机制来看,CompletionService类使用Executor对象来执行任务。这个行为的优势是可以共享ComplettoiService对象,并发送任务到执行器, 然后其他的对象可以处理任务的结果。第二个方法有一个不足之处,它只能为已经执行结束的任务获取Future对象,因此,这些Future对象只能被用来获取任务的结果。

  在本节,我们将学习如何使用ComptetionService类,在执行器中来分离任务的启动与结果的处理。

package com.concurrency.task;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 报告生成类,模拟生成报告。
 */
public class ReportGenerator implements Callable<String> {
    /**
     * 报告发送者
     */
    private String sender;
    /**
     * 报告的标题
     */
    private String title;

    /**
     * 构造函数,初始化报告发送者,报告的标题
     *
     * @param sender 报告发送者
     * @param title  报告的标题
     */
    public ReportGenerator(String sender, String title) {
        this.sender = sender;
        this.title = title;
    }

    /**
     * 核心方法,等待一个随机时间,产生一个字符串类型的报告
     *
     * @return 字符串类型的报告
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s_%s: ReportGenerator: Generating a report during %d seconds\n", this.sender, this.title, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String ret = sender + ": " + title;
        return ret;
    }
}
package com.concurrency.task;

import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * 报表处理器类,通过CompletionService对象处理报表生成器的结果
 */
public class ReportProcessor implements Runnable {
    /**
     * 完成服务对象
     */
    private CompletionService<String> service;
    /**
     * 任务完成的标记
     */
    private boolean end;

    /**
     * 构造函数,初始化完成服务对象
     *
     * @param service 完成服务对象
     */
    public ReportProcessor(CompletionService<String> service) {
        this.service = service;
    }


    /**
     * 核心方法,如果任务没有完成就一直执行
     */
    @Override
    public void run() {
        while (!end) {
            try {
                // 调用CompletionService接口的poll()方法,获取CompletionService执行的下个已完成任务的Future对象
                Future<String> result = service.poll(20, TimeUnit.SECONDS);
                if (result != null) {
                    // 使用Future对象的get()方法获取任务的结果,并且将这些结果写入到控制台
                    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");
    }

    /**
     * 设置任务完成标记
     * @param end  任务完成标记
     */
    public void setEnd(boolean end) {
        this.end = end;
    }
}
package com.concurrency.task;

import java.util.concurrent.CompletionService;

/**
 * 报告请求类
 */
public class ReportRequest implements Runnable {
    /**
     * 报告请求类的名字
     */
    private String name;
    /**
     * 完成任务对象
     */
    private CompletionService<String> service;

    /**
     * 构造函数,初始化报告请求类的名字,完成任务对象
     *
     * @param name    报告请求类的名字
     * @param service 完成任务对象
     */
    public ReportRequest(String name, CompletionService<String> service) {
        this.name = name;
        this.service = service;
    }

    /**
     * 核心方法,创建一个报告生成对象,并且将其提交到完成任务对象
     */
    @Override
    public void run() {
        ReportGenerator reportGenerator = new ReportGenerator(name, "Report");
        service.submit(reportGenerator);
    }
}
package com.concurrency.core;

import com.concurrency.task.ReportProcessor;
import com.concurrency.task.ReportRequest;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建一个执行器对象和三个完成服务对象,执行器对象供完成服务对象使用
        ExecutorService executor = (ExecutorService) Executors.newCachedThreadPool();
        CompletionService<String> service = new ExecutorCompletionService<>(executor);

        // 创建两报表请求对象和两个线程来执行他们
        ReportRequest faceRequest = new ReportRequest("Face", service);
        ReportRequest onlineRequest = new ReportRequest("Online", service);
        Thread faceThread = new Thread(faceRequest);
        Thread onlineThread = new Thread(onlineRequest);

        // 创建一个报表处理对象和一个执行它的线程
        ReportProcessor processor = new ReportProcessor(service);
        Thread senderThread = new Thread(processor);

        // 启动线程
        System.out.printf("Main: Starting the Threads\n");
        faceThread.start();
        onlineThread.start();
        senderThread.start();

        // 等待报表生成器对象的任务完成
        try {
            System.out.printf("Main: Waiting for the report generators.\n");
            faceThread.join();
            onlineThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭执行器
        System.out.printf("Main: Shuting down the executor.\n");
        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 等待报表发送器对象执行结束
        processor.setEnd(true);
        System.out.printf("Main: Ends\n");
    }
}

这里写图片描述

图4.11-1 运行结果

  在范例的主类中,我们调用Executors工厂类的newCachedThreiidPool()方法创建了 ThreadPoolExecutor执行器对象。然后,使用这个对象初始化了ComparationService对象, 因为完成服务(Completion Service)使用执行器来执行任务。然后,调用ReportRequest类中的submitt()方法,利用“完成服务”来执行任务。

  当“完成服务”任务结束,这些任务中一个任务就执行结束了,“完成服务”中存储着 Future对象,用来控制它在队列中的执行。

  调用poll()方法访问这个队列,査看是否有任务已经完成,如果有,则返回队列中的第一个元素(即一个任务执行完成后的Future对象)。当poll()方法返回Future对象后,它将从队列中删除这个Future对象。在这个示例中,我们在调用poll()方法时传递了两个参数,表示当队列里完成任务的结果为空时,想要等待任务执行结束的时间。

  —旦创建了 CompletionService对象,还要创建两个ReportRequest对象,用来执行在 CompletionService 中的两个 ReportGenerator 任务。Reportprocessor 任务则将处理两个被发送到执行器里的ReportRequest任务所产生的结果。

  CompletionService类可以执行Callable或Runnable类型的任务。在这个范例中,使用的是Callable类型的任务,但是,也可以发送Runnable对象给它。由于Runnable对象不能产生结果,因此CompletionService的基本原则不适用于此。

  ComptetionService类提供了其他两种方法来获取任务已经完成的Future对象。这些方法如下。

  ♦ poll():无参数的poll()方法用于检查队列中是否有Future对象。如果队列为空, 则立即返回null。否则,它将返回队列中的第一个元素,并移除这个元素。

  ♦ take():这个方法也没有参数,它检查队列中是否有Future对象。如果队列为空, 它将阻塞线程直到队列中有可用的元素。如果队列中有元素,它将返回队列中的第一个元 素,并移除这个元素。

4.12 处理在执行器中被拒绝的任务

  当我们想结束执行器的执行时,调用shutdown()方法来表示执行器应当结束.但是, 执行器只有等待正在运行的任务或者等待执行的任务结束后,才能真正结束。

  如果在shutdown()方法与执行器结束之间发送一个任务给执行器,这个任务会被拒绝, 因为这个时间段执行器已不再接受任务了。ThreadPoolExecutor类提供了一套机制,当任务被拒绝时调用这套机制来处理它们。

  在本节,我们将学习如何处理执行器中被拒绝的任务,这些任务实现了 RejectedExecutionHandler 接口。

package com.concurrency.task;

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 拒绝任务处理器类
 */
public class RejectedTaskController implements RejectedExecutionHandler {
    /**
     * 核心方法,在控制台输出已被拒绝的任务的名称和 执行器的状态。
     * @param r
     * @param executor
     */
    @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("RejectedTasksController: Terminated: %s\n", executor.isTerminated());
    }
}
package com.concurrency.task;

import java.util.concurrent.TimeUnit;

/**
 * 任务类,执行一个随机时间的任务
 */
public class Task implements Runnable {
    private String name;

    public Task(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.printf("Task %s: Starting\n", name);
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("Task %s: ReportGenerator: Generating a report during %d seconds\n", name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("Task %s: Ending\n", name);
    }

    @Override
    public String toString() {
        return name;
    }
}
package com.concurrency.core;

import com.concurrency.task.RejectedTaskController;
import com.concurrency.task.Task;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class Main {
    public static void main(String[] args) {
        // 创建拒绝任务控制器对象
        // Create the controller for the Rejected tasks
        RejectedTaskController controller = new RejectedTaskController();
        // 创建执行器对象,并且设置拒绝执行处理器对象
        // Create the executor and establish the controller for the Rejected tasks
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
        executor.setRejectedExecutionHandler(controller);

        // 运行三个任务
        System.out.printf("Main: Starting.\n");
        for (int i = 0; i < 3; i++) {
            Task task = new Task("Task" + i);
            executor.submit(task);
        }

        // 关闭执行器
        System.out.printf("Main: Shuting down the Executor.\n");
        executor.shutdown();

        // 发送另外一个任务
        System.out.printf("Main: Sending another Task.\n");
        Task task = new Task("RejectedTask");
        executor.submit(task);

        // 程序结束
        System.out.printf("Main: End.\n");
    }
}

这里写图片描述

图4.12-1 运行结果

  为了处理在执行器中被拒绝的任务,—要创建一个实现RejectedExecutionHandler接口的处理类。这个接口有一个rejectedExecution()方法,其中有两个参数:

  ♦ 一个Runnable对象,用来存储被拒绝的任务;

  ♦ 一个Executor对象,用来存储任务被拒绝的执行器。

  被执行器拒绝的每一个任务都将调用这个方法。需要先调用Executor类的 setRejectedExecutionHandler()方法来设置用于被拒绝的任务的处理程序。

  当执行器接收一个任务并开始执行时,它先检査shutdown()方法是否已经被调用了。如果是,那么执行器就拒绝这个任务。首先,执行器会寻找通过setRejectedExecutionHandler()方法设置的用于被拒绝的任务的处理程序,如果找到一个处理程序,执行器就调用 rejectedExecution()方法;否则就抛出RejectedExecutionExeption异常。这是一个运行时异常, 因此并不需要catch语句来对其进行处理。

欢迎转载,转载请注明出处http://blog.csdn.net/derrantcm/article/details/48173257

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值