Java 多线程之Callable Future以及FutureTask

在以前的文章中介绍过创建线程的两种方式:一种直接继承Thread类,另外一种就是实现Runnable接口。这两种方式也是从接触Java以来接触到的最常见的创建线程的方式,但是这两种方式创建线程都有一个缺陷:在执行时完成后无法返回执行结果,就是没有返回值,这也是最致命的缺陷。如果真的需要执行结果,一般情况下或者使用一个共享变量或者线程间通信,使用相当麻烦。除此之外,创建的线程无法抛出任何的异常。

从JDK1.5开始,又引入了另外一种创建线程的方式,使用Callable和Future或者FutureTask,Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是Runnable不会返回结果,并且无法抛出经过检查的异常

Callable以及相关类的简单介绍

首先看一下Callable与Runnable,Runnable接口位于java.lang包下面,只有一个run()方法,方法无返回值。

public interface Runnable {
    
    public abstract void run();
	
} 

Callable也是一个接口类型,该接口位于java.util.concurrent包下,接口中只有一个方法call(),方法有返回值,返回值定义为Callable的泛型类型,并且抛出一个Exception。

public interface Callable {
    
    V call() throws Exception;
	
}

虽然Callable中定义的call()方法有返回值,但是在多线程开发中很少直接使用call()方法来获取运行结果,而是使用Future,跟Runnable中run()方法使用类似,在多线程开发中不会直接调用run方法。线程开发中一个常见的问题就是真正启动一个线程是run()方法还是start()方法。

Callable和Runnable的区别如下:

  • Callable定义的方法是call(),而Runnable定义的方法是run()。
  • Callable的call()方法可以有返回值,而Runnable的run()方法不能有返回值。
  • Callable的call()方法可抛出异常,而Runnable的run()方法不能抛出异常。

在多线程开发中使用Callable一般是配合线程池ExecutorService的使用,在该接口中定义了submit()方法,该方法返回一个Future对象,方法如下:

public interface ExecutorService extends Executor {

    /**
     * Submits a value-returning task for execution and returns a
     * Future representing the pending results of the task. The
     * Future's {@code get} method will return the task's result upon
     * successful completion.
     *
     * 

* If you would like to immediately block waiting * for a task, you can use constructions of the form * {@code result = exec.submit(aCallable).get();} * *

Note: The {@link Executors} class includes a set of methods * that can convert some other common closure-like objects, * for example, {@link java.security.PrivilegedAction} to * {@link Callable} form so they can be submitted. * * @param task the task to submit * @param the type of the task's result * @return a Future representing pending completion of the task * @throws RejectedExecutionException if the task cannot be * scheduled for execution * @throws NullPointerException if the task is null */ Future submit(Callable task); Future submit(Runnable task, T result); Future submit(Runnable task); }

线程池ExecutorService部分并不在本文讨论的范畴之内,后续会另起博文介绍。下面是SDK中对Future的简要介绍。

Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,该方法是阻塞方法。取消则由 cancel方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。
public interface Future {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

在ExecutorService中,所有的submit()方法都会返回一个Future实例,从而将Callable或者Runnable提交给Executor,并得到一个Future用于获取执行结果或者取消任务。

由于Future中定义的get()方法是一个阻塞方法,所以开发过程中除非必要否则Future一定要在子线程中获取执行结果,此处是个人意见。get()方法的返回取决于任务的状态(未开始、正在运行、已经完成),如果已经完成则会返回一个结果或者抛出一个Exception,如果正在运行,则get()方法会阻塞知道方法完成,如果任务抛出了异常,那么get()方法会将该异常封装为ExecutionException并重新抛出,如果任务已经取消了,那么get()方法也会抛出异常,该异常是CallcellationException。如果抛出了ExecutionException,可以使用getCause来获取封装的初始异常。

上面介绍了Runnable和Callable都是接口,而且方法定义中一个是无返回值的run方法,一个是有返回值的call方法,这种涉及在使用的时候兼容性则成了很大问题,如果后续定义一个工具类,在工具类某些情况使用的是run()方法,某些情况下使用call方法则更为妥当。事实上Java考虑到了这种情况,有一个即实现了Runnable接口又实现了Callable接口的类FutureTask。

在线程池中除了submit()方法,还有一个常用的void execute(Runnable command)方法,因此FutureTask也可以交给execute()方法执行,对于Android开发者来说AsyncTask这个类一定都不陌生,该类中就是使用了FutureTask,更详细的可以查看源码。

下面FutureTask的定义,它实现了RunnableFuture接口,而实际上RunnableFuture实现了Runnable和Future接口。

public class FutureTask implements RunnableFuture {
	...
	public FutureTask(Callable callable) {
		...
	}
	public FutureTask(Runnable runnable, V result) {
		...
	}
}
public interface RunnableFuture extends Runnable, Future {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

Callable使用示例

Callable结合Future使用

首先实现一个Callable接口,在call方法中实现累加逻辑,并且每累加一次就睡眠500ms。

public class MyTask implements Callable{

	int sum=0;
	@Override
	public Integer call() throws Exception {
		System.out.println("Thread:"+Thread.currentThread().getName());
		for(int i=0;i<5;i++){
			sum+=i;
			Thread.sleep(500);
		}
		return sum;
	}
	
}
ExecutorService executor = Executors.newSingleThreadExecutor();
MyTask call = new MyTask();
Future future = executor.submit(call);
executor.shutdown();

System.out.println("main thread");
try {
	System.out.println(future.get());
} catch (InterruptedException e) {
	e.printStackTrace();
} catch (ExecutionException e) {
	e.printStackTrace();
}
System.out.println("all end");

Callable结合FutureTask使用

这里跟Future不同的地方就是我们不需要通过ExecutorService的submit()方法的返回获取一个Future了,而是直接在new一个FutureTask的时候讲Future传入即可,然后通过FutureTask的get()方法既可以获取到计算结果。

ExecutorService executor = Executors.newSingleThreadExecutor();
MyTask call = new MyTask();
FutureTask task = new FutureTask(call);

executor.submit(task);
executor.shutdown();
System.out.println("main thread");
try {
	System.out.println(task.get());
} catch (Exception e) {
	e.printStackTrace();
}
System.out.println("all end");

由于FutureTask也实现了Runnable接口,所以FutureTask也可以不通过线程池构建一个异步执行任务,只需要按照Runnable新建并启动一个线程的思路实现即可。

MyTask call = new MyTask();
FutureTask task = new FutureTask(call);

Thread thread=new Thread(task);
thread.start();

System.out.println("main thread");
try {
	System.out.println(task.get());
} catch (InterruptedException | ExecutionException e) {
	e.printStackTrace();
}
System.out.println("all end");

结束语

虽然Callable可以有返回值并且可以抛出异常,甚至与Future结合在一起使用还可以很好的控制一个任务的生命周期,但是它是一个阻塞方法,这也是Callable的一个很大弊端。平常开发中处理阻塞任务最常用的一个处理方式就是使用回调,这样就可以在事件发生时执行任何想要的代码。但是回调的缺点是,在处理回调事件时,代码会变得凌乱。而且回调特别难调试,因为控制流与应用程序中的代码顺序不匹配。为了解决这种可以说是致命弊端,Java8提供了CompletableFuture同时支持阻塞和非阻塞的事件处理方法,包括常规回调。

评论

您确定要删除吗?删除之后不可恢复