《Java7并发编程实战手册》学习笔记(五)——线程执行器

302 阅读28分钟

此篇博客为个人学习笔记,如有错误欢迎大家指正

本次内容

  1. 创建线程执行器
  2. 创建固定大小的线程执行器
  3. 在执行器中执行任务并返回结果
  4. 运行多个任务并处理第一个结果
  5. 运行多个任务并处理所有结果
  6. 在执行器中延时执行任务
  7. 在执行器中周期性执行任务
  8. 在执行器中取消任务
  9. 在执行器中控制任务的完成
  10. 在执行器中分离任务的启动与结果的处理
  11. 处理在执行器中被拒绝的任务

之前,我们在编写一个Java并发程序时,通常使用的方法是创建Runnable对象并将其作为参数传入Thread类的构造函数中,通过Thread对象来执行它们。这种方式在小型并发程序中并没有什么问题,但是如果应用在大型并发程序中它的弊端就会逐渐显现出来:

  1. 我们必须要手动实现所有和Thread对象管理相关的代码,包括创建、运行、结束、以及获取结果,这些都较大的增加了编程人员的工作量
  2. 虽然创建一个Thread对象并不会消耗太多的时间,但当我们为海量的任务都创建一个Thread对象时,这仍然会影响程序的处理能力
  3. 无节制的创建线程,会导致系统负荷过重

因此,我们在大型并发程序中可以使用Java为我们提供的执行器框架(Executor Framework)来更好的解决上述问题。ExecutorExecutorService这两个接口的实现类ThreadPoolExecutor类是我们将要经常使用的类。这个机制可以帮助我们将任务的创建和执行分离开,我们仅需要创建实现了Runnable接口的任务,至于运行我们可以交给执行器。执行器内部使用了线程池,这可以有效避免上述第二和第三个问题。执行器框架的另一个优势在于可以执行实现了Callable接口的任务,它类似于Runnable接口但更强大。实现了Callable接口的类需要重写可以有返回值的call()方法而不是run()方法。当我们发送一个Callable对象给执行器时,会得到一个Future对象,我们可以使用此对象来管理提交的Callable对象。

1.创建线程执行器

想要使用执行器框架,我们首先需要得到一个ThreadPoolExecutor对象。这个类有四种构造方法,但是因为其较为复杂,所以Java提供了对应的工厂方法来创建ThreadPoolExecutor对象,通常情况下我们推荐使用工厂类Executors中的方法来创建。但是构造方法中的几个参数我们仍然需要了解一下:

  • corePoolSize:线程池核心线程数最大值
  • maximumPoolAize:线程池最大线程数大小
  • keepAliveTime:线程池中非核心线程空闲的存活时间大小
  • unit:线程空闲存活时间单位
  • workQueue:存放任务的阻塞队列
  • threadFactory:创建线程的工厂,自定义的工厂可以为线程设置有意义的名称,方便调试
  • handler:线程池的饱和(线程池达到最大线程数且阻塞队列已满)策略事件

执行器中有如下几个方法我们需要了解:

  1. execute(Runnable task):调用此方法后可以向执行器发送一个Runnable类型的任务
  2. submit():此方法可以将Runnable、Callable对象作为参数传入,得到的是一个Future类型的对象,我们可以使用Future对象控制管理我们的任务
  3. getPoolSize():返回线程池中实际存在的线程数(包括暂时没有被使用的)
  4. getActiveCount():返回线程池中正在执行任务的线程数
  5. getCompletedTaskCount():返回执行器已经完成的任务数
  6. getLargestPoolSize():获取曾经线程池中拥有的最大线程数
  7. shutdown():调用此方法后,相应的执行器便不再接收新的任务并在执行完已提交的任务后关闭,如果再次尝试向执行器发送任务,执行器会拒绝接受任务并抛出RejectedExecutionException异常。
  8. shutdownNow():这个方法和shutdown()方法的区别应该从名字上就可以看出。调用此方法后,执行器将会试图立刻关闭,也就是不再接收新任务,不再执行处于等待状态的任务,向正在执行的任务发起中断请求
  9. isTerminated():如果执行器已经关闭了,则返回true
  10. isTerminating():返回布尔类型的值表示执行器是否正在关闭但还未完成
  11. isShutdown():如果调用了shutdown()方法,则此方法返回true
  12. awaitTermination(long timeout,TimeUint unit):调用此方法的线程会进入阻塞状态,直到到达指定时间或执行器中的所有任务均执行完毕
  13. getTaskCount():得到当前时刻已经向执行器发送的任务数量

范例实现

在这个范例中我们将使用执行器来执行我们实现了Runnable接口的任务类。此次使用的执行器是缓存线程池执行器,这种执行器在线程池中线程不够时会创建新的线程来执行任务;当需要执行的任务数小于当前线程池内存在的线程数时,执行器会减少线程数量。这种执行器仅适合在任务完成时间较短或任务数量较少的情况下使用,否则线程池内将会出现过多的线程,系统负荷将会过载
任务类:

package day05.code_01;

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

public class Task implements Runnable {

    //任务创建的时间
    private Date initDate;

    //任务名称
    private String name;

    public Task(String name) {
        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());
    }
}

Server类(此类中存在一个私有的执行器):

package day05.code_01;

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

public class Server {

    //线程池执行器
    private ThreadPoolExecutor executor;

    public Server() {
        /*
         * 通过Executors的工厂方法创建一个缓存线程执行器
         * 此方法返回的是一个ExecutorService,因此我们要向下转型
         * */
        executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
    }

    public void executeTask(Task task) {
        //打印任务到达提示语
        System.out.printf("Server: A new task has arrived\n");
        //使用execute方法发送Runnable类型的任务
        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() {
        //结束线程池执行器
        executor.shutdown();
    }
}

main方法:

package day05.code_01;

public class Main {

    public static void main(String[] args) {
        //创建一个Server类对象
        Server server = new Server();
        //创建100个任务并使用server对象执行
        for (int i = 0; i < 100; i++) {
            Task task = new Task("Task " + i);
            server.executeTask(task);
        }
        //关闭server
        server.endServer();
    }

}

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

第一个范例中我们使用的缓存处理器存在一定的弊端,我们可以使用拥有最大线程数量限制的执行器来解决这个问题。调用Executors类的newFixedThreadPool()方法可以得到拥有最大线程数量限制的执行器。当线程池中的线程数量未到达最大限制时且任务数量大于当前线程数量时,执行器将创建新的线程。线程数量达到最大限制后,如果任务数大于线程数,剩余的任务将被阻塞直到有空闲的线程来处理。另外,Executors类的newSingleThreadPool()方法可以创建一个只有一个线程的执行器。

范例实现

在这个范例中,我们仅将第一小节的Server类进行了小的改动,使用的了拥有最大线程数量限制的执行器,其他类均相同,就不再给出
新Server类:

package day05.code_02;

import day05.code_01.Task;

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

public class Server {

    //线程池执行器
    private ThreadPoolExecutor executor;

    public Server() {
        /*
         * 通过Executors的工厂方法创建一个固定大小为5的线程执行器
         * 此方法返回的是一个ExecutorService,因此我们要向下转型
         * */
        executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
    }

    public void executeTask(Task task) {
        //打印任务到达提示语
        System.out.printf("Server: A new task has arrived\n");
        //使用execute方法发送Runnable类型的任务
        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() {
        //结束线程池执行器
        executor.shutdown();
    }
}

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

执行器框架的一个强大之处在于它可以运行一个Callable对象并返回一个Future对象,我们可以通过这个对象来查看任务状态、获得结果。我们通过执行器的submit()方法来向执行器发送任务。Future对象有以下几个方法值得我们注意:

  1. get():尝试得到任务的结果,也就是等待call()方法运行结束并给出返回值。如果任务还未完成,则线程阻塞直到获得结果。在线程阻塞时,如果线程中断,get()方法将抛出InterruptedException异常;如果call()方法抛出异常,get()方法将抛出ExecutionException异常
  2. get(long timeout,TimeUnit unit):尝试得到任务的结果,如果任务还未完成,线程将阻塞直到结果返回或到达指定时间,到达指定时间会返回null
  3. isDone():返回任务是否完成。完成可能是因为正常结束、异常、取消

范例实现

在这个范例中,我们向执行器发送一些Callable对象作为任务,这些任务通过执行器执行完毕后会返回随机数的阶乘结果
阶乘任务类:

package day05.code_03;

import java.util.concurrent.Callable;

public class FactorialCalculator implements Callable<Integer> {

    //要进行阶乘的数字
    private Integer number;

    public FactorialCalculator(Integer number) {
        this.number = number;
    }

    //重写的可以返回结果的call方法
    @Override
    public Integer call() throws Exception {
        //将结果预置为1
        int result = 1;
        //如果需要阶乘的数为0或1
        if (number == 0 || number == 1) {
            //直接返回结果1
            result = 1;
        } else {
            //否则进行阶乘,每次阶乘后休眠20毫秒
            for (int i = 2; i <= number; i++) {
                result *= i;
                Thread.sleep(20);
            }
        }
        //打印线程名称和阶乘结果
        System.out.printf("%s: %d\n",
                Thread.currentThread().getName(), result);
        //返回结果
        return result;
    }
}

main方法:

package day05.code_03;

import java.util.ArrayList;
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 excutor =
                (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
        //装载返回的Future对象的集合
        ArrayList<Future<Integer>> resultList = new ArrayList<>();
        //创建随机数生成器对象
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            //生成一个随机数
            int number = random.nextInt(10);
            //创建一个计算阶乘的任务
            FactorialCalculator calculator = new FactorialCalculator(number);
            //将任务发送给执行器并得到一个Future对象
            Future<Integer> result = excutor.submit(calculator);
            //将Future对象装入集合
            resultList.add(result);
        }
        do {
            //打印执行器已完成的任务数量
            System.out.printf("Main: Number of Completed Tasks:%d\n",
                    excutor.getCompletedTaskCount());
            //通过Future对象判断任务是否已经结束并打印
            for (int i = 0; i < resultList.size(); i++) {
                Future<Integer> result = resultList.get(i);
                System.out.printf("Main: Task %d: %s\n",
                        i, result.isDone());
            }
            //每检查一次后线程休眠50毫秒
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //一直循环直到所有的任务均完成
        } while (excutor.getCompletedTaskCount() < resultList.size());
        //打印结果提示信息
        System.out.printf("Main: Results\n");
        //遍历集合
        for (int i = 0; i < resultList.size(); i++) {
            Future<Integer> result = resultList.get(i);
            Integer number = null;
            //从Future对象中得到返回的结果
            try {
                number = result.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            //打印
            System.out.printf("Main: Task %d: %d\n",
                    i, number);
        }
        //关闭执行器
        excutor.shutdown();
    }
}

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

我们可以使用执行器的invokeAny()方法向执行器发送一个任务列表并得到一个和call()方法返回值相同类型的结果。虽然我们提交了一个任务列表,但是方法的返回值是最早正确执行完的任务的结果,得到这个结果后,执行器将向其他线程发起中断请求。invokeAny()方法还有另一种版本,我么可以在传入任务列表的同时传入时间,显然这样可以让此方法在到达指定时间时直接返回而不是继续阻塞线程。

范例实现

在这个范例中,我们模拟了一个用户验证的场景。有两个验证机制,程序将返回最快正确执行完的验证结果
UserValidator类:

package day05.code_04;

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

public class UserValidator {

    //登录名
    private String name;

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

    public String getName() {
        return name;
    }

    public boolean validate() {
        //创建一个随机数生成器对象
        Random random = new Random();
        //休眠随机时间
        long duration = (long) (Math.random() * 10);
        System.out.printf("Validator %s: Validating a user during %d seconds\n",
                this.name, duration);
        //如果线程在休眠期间被中断就返回false
        try {
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            return false;
        }
        //休眠结束后随机返回一个结果
        return random.nextBoolean();
    }
}

TaskValidator类:

package day05.code_04;

import java.util.concurrent.Callable;

public class TaskValidator implements Callable<String> {

    //用户验证类
    private UserValidator validator;

    public TaskValidator(UserValidator validator) {
        this.validator = validator;
    }

    //返回字符串的call方法
    @Override
    public String call() throws Exception {
        //如果方法返回false
        if (!validator.validate()) {
            //打印验证失败信息
            System.out.printf("%s: The user has not been found\n",
                    validator.getName());
            //抛出异常
            throw new Exception("Error validating user");
        }
        //否则打印验证成功信息
        System.out.printf("%s: The user has been found\n",
                validator.getName());
        //返回结果
        return validator.getName();
    }
}

main方法:

package day05.code_04;

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

public class Main {

    public static void main(String[] args) {
        //创建两个用户验证对象
        UserValidator ldapValidator = new UserValidator("LDAP");
        UserValidator dbValidator = new UserValidator("DataBase");
        //创建两个任务验证对象,并将用户验证对象作为参数传入
        TaskValidator ldapTask = new TaskValidator(ldapValidator);
        TaskValidator dbTask = new TaskValidator(dbValidator);
        //将两个任务验证对象放入集合中
        ArrayList<TaskValidator> taskLists = new ArrayList<>();
        taskLists.add(ldapTask);
        taskLists.add(dbTask);
        //创建一个缓存执行器
        ThreadPoolExecutor executor =
                (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //声明字符串
        String result;
        try {
            //取得一个结果
            result = executor.invokeAny(taskLists);
            System.out.printf("Main: Result: %s\n", result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //关闭执行器并打印关闭信息
        executor.shutdown();
        System.out.printf("Main: End of the Execution\n");
    }

}

我们来看一下这个程序主要的运行可能:

  1. 休眠时间较短的线程返回了true,call()方法正常执行,返回了登录名。这样一来执行器中就出现了第一个正确执行完毕的任务,执行器将向其他线程发起中断请求。仍处于休眠阶段的线程将会响应中断抛出异常,这个异常被我们捕获并返回结果false,call()方法得到结果false后会抛出异常。但此时我们的invokeAny()方法并不会抛出异常
  2. 休眠时间较短的线程返回了false,call()方法得到结果false后抛出异常,这个任务虽然最早执行结束但并不是正确的。因此执行器不会向其他线程发起中断请求。由于其他任务的结果未知,所以此时invokeAny()方法也不会抛出异常。如果第二个结束的任务返回的结果也为false导致call()方法抛异常,那相当于执行器中的所有任务均执行失败了,invokeAny()方法会抛出ExecutionException异常。反之,如果第二个任务执行成功了,那么执行器将返回第二个任务的执行结果。

因此我们可以得出这样的结论:

  1. 只有当执行器中所有的线程均抛出异常后,invokeAny()方法才会抛出ExecutionException异常
  2. 当得到正确结果后,执行器会向其他线程发起中断请求,但也只是请求而已,线程是否响应是未知的

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

之前我们使用invokeAny()方法向执行器发送一个任务列表并阻塞线程直到得到一个执行结果。当我们想要得到所有提交任务的结果时我们可以使用invokeAll()方法。和invokeAny()方法一样,invokeAll()方法也需要将一个任务列表作为参数传入,同样它的另一种版本也是在到达指定时间时直接返回。不同的是,invokeAll()方法会阻塞线程直到所有任务都执行完成后返回一个Future对象的集合,并且此方法只会抛出InterruptedException异常。如果任务中的call方法在运行时抛出异常那么ExecutionException异常会在我们调用Future对象的get()方法时抛出。

范例实现

在这个范例中,我们将提交三个任务并且在它们执行完成后一并获取与其一一对应的Future对象
Result类:

package day05.code_05;

public class Result {

    //结果的名字
    private String name;

    //结果的值
    private int value;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

Task类:

package day05.code_05;

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

public class Task implements Callable<Result> {

    //任务的名字
    private String name;

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

    //这里的call方法返回一个Result对象
    @Override
    public Result call() throws Exception {
        //打印任务开始信息
        System.out.printf("%s: Starting\n", this.name);
        //休眠随机时间
        long duration = (long) (Math.random() * 10);
        System.out.printf("%s: Waiting %d seconds for results\n",
                this.name, duration);
        try {
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建Result对象并为其成员变量赋值
        Result result = new Result();
        result.setName(this.name);
        result.setValue((int) Math.random() * 100);
        //返回result对象
        return result;
    }
}

main方法:

package day05.code_05;

import java.util.ArrayList;
import java.util.List;
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) {
        //创建缓存执行器
        ThreadPoolExecutor executor =
                (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //创建一个容器
        ArrayList<Task> taskList = new ArrayList<>();
        //创建三个任务并装入容器
        for (int i = 0; i < 3; i++) {
            Task task = new Task(Integer.toString(i));
            taskList.add(task);
        }
        //声明一个装有Future对象的集合
        List<Future<Result>> resultList = null;
        try {
            //向执行器发送任务列表
            resultList = executor.invokeAll(taskList);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //关闭执行器
        executor.shutdown();
        //打印结果
        System.out.println("Main: Printing the results");
        //遍历装有Future对象的集合
        for (int i = 0; i < resultList.size(); i++) {
            //得到每一个Future对象
            Future<Result> future = resultList.get(i);
            try {
                //得到结果
                Result result = future.get();
                //打印结果
                System.out.printf("%s : %s\n",
                        result.getName(), result.getValue());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

}

6.在执行器中延时执行任务

有时我们需要延时执行一些任务,Java为我们提供的ScheduledThreadPoolExecutor类可以帮助我们更简单的实现这个需求。我们可以通过Executors类的newScheduledThreadPool(int corePoolSize)方法获取一个ScheduledThreadPool对象,此对象可以帮助我们简单的实现延时任务的执行。ScheduledThreadPool类中有两个方法与延时执行任务有关:

  1. schedule(Runnable command,long delay,TimeUnit unit):调用此方法可以使我们的执行器延时执行一个Runnable类型的任务。此方法还有另一种版本让我们可以传入一个Callable类型的任务并延时执行
  2. setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean value):调用此方法并传入false后,执行器在调用shutdown()方法后将不再执行处于等待状态的任务

另外,在我们调用newScheduledThreadPool(int corePoolSize)方法时需要传入一个int型参数,我们在此将几个常见的和线程数量有关的参数一并记录:

  1. corePoolSize:线程池的基本大小,也就是当没有任务需要执行时线程池的大小,并且,只有当工作队列已满时线程池才会创建新的线程,这时线程数量会超过线程池的基本大小
  2. maximumPoolSize:线程池的最大线程数,线程池中的当前线程数量不会超过最大线程数
  3. poolSize:当前线程池中存在的线程数量

范例实现

在这个范例中,我们通过定时执行器执行了5个延时任务
Task类:

package day05.code_06;

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

public class Task implements Callable<String> {

    //任务名称
    private String name;

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

    @Override
    public String call() throws Exception {
        //打印任务开始信息
        System.out.printf("%s: Starting at : %s\n",
                this.name, new Date());
        return "Hello, world";
    }
}

main方法:

package day05.code_06;

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

public class Main {

    public static void main(String[] args) {
        //创建一个定时执行器并设定基本线程量为1
        ScheduledThreadPoolExecutor executor =
                (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
        //打印程序开始提示信息
        System.out.printf("Main: Starting at: %s\n", new Date());
        //创建5个任务并延时执行
        for (int i = 0; i < 5; i++) {
            Task task = new Task("Task " + i);
            //使用执行器延时执行此任务
            executor.schedule(task, i + 1, TimeUnit.SECONDS);
        }
        //关闭执行器
        executor.shutdown();
        //当前线程阻塞直到执行器执行完所有已提交的任务
        try {
            executor.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印程序结束信息
        System.out.printf("Main: Ends at: %s\n", new Date());
    }
}

虽然ScheduledThreadPool类实现了ExecutorService接口的所有方法,但是Java推荐仅在开发延时、定时任务程序中使用ScheduledThreadPool

7.在执行器中周期性执行任务

我们也可以使用ScheduledThreadPool类来执行定时任务

  1. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):我们可以使用此方法定时执行一个任务。第一个参数是Runnable类型的任务;第二个为第一次开始任务时延迟的时间;第三个为执行周期(这里的周期为任务上一次开始执行的时间和下一次开始执行时的时间间隔);最后一个参数为时间类型
  2. scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):此方法与第一个方法功能大致相同。区别在于第三个参数所表达的意义,这里表示上一次任务执行结束的和下一次任务执行开始之间的时间间隔
  3. setContinueExistingPeriodicTasksAfterShutdownPolicy(boolean value):调用此方法并传入参数为true,执行shutdown()方法后定时任务仍将继续按周期执行,默认情况下定时任务将会结束

范例实现

在这个范例中,我们通过定时执行器按周期执行了一个定时任务。可以看到,在调用执行器的scheduleAtFixedRate方法后,我们得到了一个实现了ScheduledFuture接口的对象,经过调试我们可以发现这是一个ScheduledFutureTask类对象(ScheduledThreadPoolExecutor类的内部类),我们可以通过其getDelay(TimeUnit timeunit)方法得到距离下一次任务执行的时间。
Task类:

package day05.code_07;

import java.util.Date;

public class Task implements Runnable {

    //任务名称
    private String name;

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

    @Override
    public void run() {
        //打印任务开始信息
        System.out.printf("%s: Starting at : %s\n",
                this.name, new Date());
    }
}

main方法:

package day05.code_07;

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

public class Main {

    public static void main(String[] args) {
        //打印程序开始的时间
        System.out.printf("Main: Starting at: %s\n", new Date());
        //创建任务
        Task task = new Task("Task");
        //创建一个定时执行器
        ScheduledThreadPoolExecutor executor =
                (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(1);
        //定时执行任务
        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));
            //休眠0.5秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //关闭执行器
        executor.shutdown();
    }
}

8.在执行器中取消任务

有时我们需要取消已发送给执行器的任务,Future接口提供的方法可以让我们简单的实现这个需求,以下是我们在取消任务时常用的方法:

  1. cancel(boolean mayInterruptIfRunning):此方法需要传入一个布尔类型的参数。如果任务已经完成、已被取消、或者由于某些原因不能取消,那么此方法将返回false且任务不被取消;如果任务处于等待状态,那么任务会被直接取消;如果任务已经执行,那么具体行为将取决于传入的参数。如果为true,执行的任务将被取消;否则将继续执行
  2. isCancelled():返回一个布尔类型的值表示任务是否已经被取消

范例实现

在这个范例中我们将在发送一个任务后取消该任务,并打印出相关信息
Task类:

package day05.code_08;

import java.util.concurrent.Callable;

public class Task implements Callable<String> {
    @Override
    public String call() throws Exception {
        //每隔0.1秒打印一次信息
        while (true) {
            System.out.printf("Task: Test\n");
            Thread.sleep(100);
        }
    }
}

main方法:

package day05.code_08;

import java.util.concurrent.*;

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对象
        Future<String> result = executor.submit(task);
        //当前线程休眠2秒
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印取消任务提示信息
        System.out.printf("Main: Canceling 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");
    }
}

注意,当我们取消了Future所控制的任务后,再次调用get()方法想要获取结果时会抛出CancellationException异常

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

有些情况下,我们需要在任务执行结束后进行一些后期处理操作。FutureTask类提供的done()方法会在任务完成后被自动调用。这个方法默认的实现为空,我们可以继承FutureTask类并重写done()方法

范例实现

在这个范例中,我们将FutureTask对象发送到执行器来执行;另一种方法是调用FutureTask的run()方法
任务类:

package day05.code_09;

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

public class ExecutableTask implements Callable<String> {

    //任务名称
    private String name;

    public String getName() {
        return name;
    }

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

    @Override
    public String call() throws Exception {
        //获取休眠时间
        long duration = (long) (Math.random() * 10);
        //打印相关信息
        System.out.printf("%s: Waiting %d seconds for results\n",
                this.name, duration);
        //休眠
        TimeUnit.SECONDS.sleep(duration);
        //返回字符串类型的结果
        return "Hello, world. I'm " + this.name;
    }
}

重写了done()方法的FutureTask类:

package day05.code_09;

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

public class ResultTask extends FutureTask<String> {

    //名称
    private String name;

    //此构造方法必须给出
    public ResultTask(Callable<String> callable) {
        super(callable);
        this.name = ((ExecutableTask) callable).getName();
    }

    //重写done方法
    @Override
    protected void done() {
        //如果任务是被取消的,则打印取消信息
        if (isCancelled()) {
            System.out.printf("%s: Has been canceled\n", name);
        } else {
            //否则打印执行结束信息
            System.out.printf("%s: Has finished\n", name);
        }
    }
}

main方法:

package day05.code_09;

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

public class Main {

    public static void main(String[] args) {
        //创建缓存执行器
        ThreadPoolExecutor executor =
                (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //创建继承了FutureTask类型的数组
        ResultTask[] resultTasks = new ResultTask[5];
        //遍历数组
        for (int i = 0; i < resultTasks.length; i++) {
            //创建任务类对象
            ExecutableTask executableTask = new ExecutableTask("Task " + i);
            //创建ResultTask对象并将任务类对象作为参数
            resultTasks[i] = new ResultTask(executableTask);
            //发送到执行器
            executor.submit(resultTasks[i]);
        }
        //当前线程休眠5秒
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //取消所有任务
        for (int i = 0; i < resultTasks.length; i++) {
            resultTasks[i].cancel(true);
        }
        //打印未取消任务的结果
        for (int i = 0; i < resultTasks.length; i++) {
            try {
                if (!resultTasks[i].isCancelled()) {
                    System.out.printf("%s\n", resultTasks[i].get());
                }
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        //关闭执行器
        executor.shutdown();
    }

}

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

在某些情况下,我们希望任务由一个对象发送到执行器,由另外一个对象从执行器中接收结果。CompletionService接口为我们提供了一些方法,便于我们实现上述需求。在实际编码过程中,我们直接使用CompletionService接口的实现类ExecutorCompletionService即可,这个类仍然使用执行器来执行任务,因此我们在创建此类时仍需传入一个执行器对象的引用。以下为此类常用方法:

  1. poll():从队列中获取第一个已经执行完毕的任务的Future对象并将此任务删除,如果队列中暂时没有执行结束的任务,该方法会立即返回null而不会阻塞
  2. poll(long timeout, TimeUnit unit):和第一个方法的区别在于,如果暂时没有执行结束的任务,那么该方法将阻塞线程直到线程被中断或超过指定时间后返回null
  3. submit(Callable<V> task):提交Callable类型任务的方法,返回值是一个Future对象
  4. take():同样是从队列中获取执行完毕的任务的Future对象,区别在于该方法会一直阻塞线程直到获取到Future对象或被中断

以上方法需要注意的是,四种方法均可以获得Future对象,但是只有submit()方法可以在发送任务之后直接获得一个Future对象,其他三个方法只能在任务执行结束之后才可以获得对应的Future对象

范例实现

在这个范例中,我们将创建两个线程负责向ExecutorCompletionService对象发送任务,一个线程尝试提取已完成任务的Future对象
任务类:

package day05.code_10;

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

public class ReportGenerator implements Callable<String> {

    private String sender;

    private String title;

    public ReportGenerator(String sender, String title) {
        this.sender = sender;
        this.title = title;
    }

    @Override
    public String call() throws Exception {
        //打印休眠时间后进入休眠状态
        long duration = (long) (Math.random() * 10);
        System.out.printf("%s_%s: ReportGenerator: Generating a " +
                        "report during %d seconds\n",
                this.sender, title, duration);
        TimeUnit.SECONDS.sleep(duration);
        //拼接成员变量作为返回值
        String ret = sender + ": " + title;
        return ret;

    }
}

发送任务类(用于创建线程):

package day05.code_10;

import java.util.concurrent.CompletionService;

public class ReportRequest implements Runnable {

    private String name;

    private CompletionService<String> service;

    public ReportRequest(String name, CompletionService<String> service) {
        this.name = name;
        this.service = service;
    }

    @Override
    public void run() {
        //创建一个任务
        ReportGenerator reportGenerator = new ReportGenerator(name, "Report");
        //将其发送给CompletionService对象
        service.submit(reportGenerator);
    }
}

提取任务类(用于创建线程):

package day05.code_10;

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

public class ReportProcessor implements Runnable {

    //CompletionService对象
    private CompletionService<String> service;

    //线程是否跳出死循环的标志
    private boolean end;

    public void setEnd(boolean end) {
        this.end = end;
    }

    public ReportProcessor(CompletionService<String> service) {
        this.service = service;
        //end默认为false
        end = false;
    }

    @Override
    public void run() {
        //end不为true时,始终尝试取出Future对象
        while (!end) {
            try {
                //取出Future对象
                Future<String> result =
                        service.poll(20, TimeUnit.SECONDS);
                //如果成功取出
                if (result != null) {
                    //打印任务返回的结果
                    String report = result.get();
                    System.out.printf("ReportReceiver: Report Received:%s\n",
                            report);
                }
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印当前线程结束提示语
        System.out.println("ReportSender: End");
    }
}

main方法:

package day05.code_10;

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

public class Main {

    public static void main(String[] args) {
        //创建一个缓存执行器
        ThreadPoolExecutor executor =
                (ThreadPoolExecutor) Executors.newCachedThreadPool();
        /*
         * 创建一个ExecutorCompletionService对象
         * 因为执行任务需要依赖于执行器,所以将创建好的执行器的引用传入
         * */
        ExecutorCompletionService<String> service =
                new ExecutorCompletionService<>(executor);
        /*
         * 创建了两个ReportRequest对象并作为参数传入线程的构造函数中
         * 这两个线程用于向ExecutorCompletionService对象中发送任务
         * */
        ReportRequest faceRequest = new ReportRequest("Face", service);
        Thread faceThread = new Thread(faceRequest);
        ReportRequest onlineRequest = new ReportRequest("Online", service);
        Thread onlineThread = new Thread(onlineRequest);
        /*
         * 创建一个ReportProcessor对象并作为参数传入线程的构造函数中
         * 这个线程用于从ExecutorCompletionService中取出Future对象
         * */
        ReportProcessor processor = new ReportProcessor(service);
        Thread senderThread = new Thread(processor);
        //程序开始提示语
        System.out.println("Main: Starting the Threads");
        //启动创建好的三个线程
        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: Shutting down the executor\n");
        //关闭执行器
        executor.shutdown();
        //等待执行器执行完所有任务
        try {
            executor.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //结束用于获取Future对象的线程
        processor.setEnd(true);
        //打印程序结束提示语
        System.out.println("Main: Ends");
    }
}

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

在我们调用了执行器的shutdown()方法后,如果再次向执行器发送任务默认情况下是会抛出RejectedExecutionException异常的。有时我们希望采用我们自己的方案来处理这些被执行器拒绝的任务,我们可以创建一个类使其实现RejectedExecutionHandler接口并重写rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。方法在被调用时会自动传入两个参数,第一个是被拒绝的任务,实际接收到的是一个FutureTask对象;第二个为拒绝接收任务的执行器。这些工作完成后,我们调用执行器的setRejectedExecutionHandler(RejectedExecutionHandler handler)方法,传入我们实现了RejectedExecutionHandler接口的自定义类。这样当执行器接收到一个任务时,会先判断自身的shutdown()方法是否已经被调用了,如果没有调用则接收;否则将检查是否有设置被拒绝任务的处理程序,存在则执行,不存在就抛出RejectedExecutionException异常

范例实现

自定义类:

package day05.code_11;

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

public class RejectedTaskController implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //打印被拒绝任务的信息
        System.out.printf("RejectedTaskController: " +
                "The task %s has been rejected\n", r);
        //打印执行器信息
        System.out.printf("RejectedTaskController: %s\n",
                executor.toString());
        //打印执行器是否正在关闭
        System.out.printf("RejectedTaskController: Terminating: %s\n",
                executor.isTerminating());
        //打印执行器是否已经关闭
        System.out.printf("RejectedTaskController: Terminated: %s\n",
                executor.isTerminated());
    }
}

任务类:

package day05.code_11;

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.println("Task " + name + " Starting");
        //休眠
        long duration = (long) (Math.random() * 10);
        System.out.printf("Sleeping %d seconds\n", duration);
        try {
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印任务结束信息
        System.out.printf("Task %s: Ending\n", name);
    }
}

main方法:

package day05.code_11;

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

public class Main {

    public static void main(String[] args) {
        //创建自定义类
        RejectedTaskController controller = new RejectedTaskController();
        //创建缓存执行器
        ThreadPoolExecutor executor =
                (ThreadPoolExecutor) Executors.newCachedThreadPool();
        //设置被拒绝任务的处理程序
        executor.setRejectedExecutionHandler(controller);
        //打印程序开始的提示信息
        System.out.println("Main: Starting");
        //向执行器发送三个任务
        for (int i = 0; i < 3; i++) {
            Task task = new Task("Task " + i);
            System.out.println(task);
            executor.submit(task);
        }
        //打印提示信息并关闭执行器
        System.out.println("Main: Shutting down the executor");
        executor.shutdown();
        //打印提示信息并再次提交一个任务
        System.out.println("Main: Sending another Task");
        Task task = new Task("RejectedTask");
        executor.submit(task);
        //打印当前线程结束信息
        System.out.println("Main: End");
    }
}