03_concurrency_08_callable_and_future

第四十二章 Java异步执行机制和Callable/Future接口

1 概述

在单线程程序中,程序都是同步执行(Synchronous Execution)的。即:当生成一个任务,并开始执行后,主线程必须等待任务执行完毕之后,才能执行下一个任务。然而,在多线程环境下,因为两个或者多个线程可以并行执行,这也就产生了另一种执行方式:异步执行(Asynchronous Execution)。

异步执行(Asynchronous Execution)方式是指当一个任务生成后,主线程可以将其提交到另一个线程上去执行。与此同时,主线程不必等待该任务的运行,主线程可以立即着手去做另一项任务。等过一段时间后,主线程可以检查提交的任务是否已执行完毕。如已完毕,主线程可以获得任务的结果。这种运行方式被称为Java异步执行方式。这种方式是由Callable和Future接口实现的。

异步执行方式被广泛的用于大型应用程序中。Web服务器和图形用户界面程序(GUI Application)是两个较为典型的例子。在这两个例子中,当用户提交一个计算量较大的任务时,如果该任务是同步运行的话,只有当任务结束之后,应用程序才将结果返回给用户。因此,在这个过程中,用户需要等待较长的时间才能直到结果。然而,如果使用了异步执行的方式,当任务提交后,系统可以给用户先返回一个任务提交成功的消息。用户看到此消息后,可以转去做其他的事情,不必一直守候在系统前。任务的结果可以稍后来取。因此,实际上,异步执行方式提供了更加友好的服务,节省了用户的等待时间。

2 Callable和Future接口

Callable和Future是Java标准库为实现异步执行而设计的两个重要概念。Callable和Future是两个接口。Callable只包含一个方法call();Future定义了一系列方法用户检查任务是否执行成功,以及获取执行结果。Callable和Future都是在java.util.concurrent包中。

2.1 Callable接口

Callable接口用于描述一项任务。开发人员需要在具体类中实现Callable接口;即通过覆盖call()方法,描述任务的内部逻辑。call()方法执行的结果由返回对象描述。

Callable接口和Runnable接口十分相似,我们在下面简单的总结一下它们的相同点和不同点。

  1. Callable和Runnable都是接口;都用于描述一个任务,该任务在独立的线程中运行。
  2. Callable和Runnable都只定义了一个方法,所以它们都是Functional Interface
  3. Runnable可以提交给Thread对象,在独立线程中运行;Callable和Runnable都可以提交给ExecutorService,在独立线程中运行。
  4. Runnable的run()方法没有返回值;Callable的call()方法有返回值,表示任务运行的结果。
  5. 在Runnable的run()方法中不能抛出异常;而Callable接口的call()方法中可以抛出异常。

与Runnable类似,Callable对象可由以下三种方式创建。

第一种方法是创建一个新类,实现Callable接口。这是最为常见的一种方法。开发人员可以自定义一个新类,并实现Callable接口。例如:下面的CallableTaskExample类实现了Callable接口,在call()方法中返回一个字符串"This is running in a new thread"。因为Callable是一个泛型接口,其接收的泛型类就是call()方法返回的对象类型。因此,在下例中,该泛型类类型是String。

import java.util.concurrent.Callable;

public class CallableTaskExample implements Callable<String> {
    @Override
    public String call() {
        return "This is running in a new thread";
    }
}

第二种方法是使用匿名类,实现Callable接口。当任务的执行逻辑较为简单时,使用匿名类是一个不错的选择。在如下的例子中,并没有定义一个全新的类来表示新线程的运行逻辑。Java虚拟机会为其自动创建一个匿名类。

public class AnonymousCallableTaskExample {
    public static void main(String[] args) {
        // 创建一个匿名类,实现Callable接口
        Callable<String> anonymousTask = new Callable<String>() {
            @Override
            public String call() {
                return "This is running in a new thread";
            }
        };
    }
}

第三种方法是使用Lambda表达式实现Callable接口。如果还想省略更多的代码,那么,可以使用Lambda表达式来创建一个Callable接口的对象。因为Callable接口只提供了一个方法call()。因此,Callable接口也是一个Functional Interface。所以,Lambda表达式也可以转换成Callable接口的对象。(注:Java 8以上版本才支持Lambda表达式和Functional Interface。)

public class LambdaExpressionCallableTaskExample {
    public static void main(String[] args) {
        // 使用Lambda表达式创建一个Callable对象
        Callable<String> lambdaExpressionTask = 
            () -> {return "This is running in a new thread";};
    }
}

 

3 Future接口

java.util.concurrent.Future接口表达的是一个异步执行的任务的结果。这项任务可能正在执行,也可能已经执行完毕。Future接口提供了一系列的方法,允许开发人员检查任务的状态;若已执行完毕,可以获取任务的结果;若尚在运行,开发人员可以选择取消这项任务。

下面是一个使用Callable和Future的例子。例子中使用了ExecutorService类。它是一个线程池,可以接受Callable对象,并以异步的方式运行这个Callable对象。在使用submit()方法提交Callable对象的时候,submit()方法会立即返回一个Future对象。主线程可以使用这个Future对象来跟踪和查看该任务的执行状态和结果。

Callable对象的执行结果可由Future.get()方法返回。如果任务尚在执行中,Future.get()方法会阻塞当前线程,直到任务执行完毕,并能获取任务的执行结果。如果任务已执行完毕,Future.get()会立即返回执行结果。

Future.get(long, TimeUnit)是获取结果的第二种形式。这个方法允许开发人员提供一个最长的等待时间。如果任务尚在执行,并且等待的时间超过了最长等待时间的话,Future.get(long, TimeUnit)方法会抛出Timeout异常。

import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class FutureExample {
    public static void main(String[] args) {
        // 创建一个Callable对象,计算一个整数的平方和三次方,
        Integer i = 10;
        Callable callable1 = () -> {return i * i;}; 
        Callable callable2 = () -> {return i * i * i;}; 
        
        // 创建一个单线程的线程池,将Callable对象提交给线程池执行
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future1 = executor.submit(callable1);
        Future<Integer> future2 = executor.submit(callable2);
        
        // 循环等待计算结束
        while(!future1.isDone()) {
            System.out.println("Calculating...");
            Thread.sleep(1000);
        }
 
        // 获取异步执行的结果
        Integer result1 = future1.get();
        System.out.println("The result is " + result1);
        
        for (;;) {
            Integer result2 = null;
            try {
                // 尝试获取结果,并最多等待1秒的时间
                result2 = future2.get(1, TimeUnit.SECONDS);
            } catch (TimeoutException ex) {
                // 如果等待超时,则会抛出TimeoutException异常
                continue;
            }
            
            System.out.println("The result is " + result2);
            break;
        }
	}
}

Future对象还允许开发人员在任务尚在运行时取消任务。如下面的代码所示。在任务提交后,可以使用Future.cancel()方法取消任务。如果任务已经完成,或者已被取消,则cancel()方法会执行失败。如果任务尚未开始,则该任务会被取消,永远不会被执行。如果任务正在执行,则该任务会被终止。如果参数是true的话,该任务会被打断(Interrupted)而终止。

import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class FutureExample {
    public static void main(String[] args) {
        // 创建一个Callable对象,计算一个整数的平方,
        Integer i = 10;
        Callable callable = () -> {return i * i;}; 
        
        // 创建一个单线程的线程池,将Callable对象提交给线程池执行
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(callable);
        
        // 取消任务
        future.cancel(true);
	}
}

4 总结

本章节介绍了Java异步运行机制中的两个重要概念Callable和Future。Callable描述的是一项任务,而Future则描述的是如何跟踪和获取一项任务执行的状态和结果。这两个概念非常重要,我们会在后续章节介绍线程池ExecutorService时再次使用Callable和Future的概念。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.