java并发系列-Runnable vs. Callable in Java

1 简介

从Java早期开始,多线程就是它主要的组成部分。Runnable是用于表示多线程任务的核心接口,Callable在Java1.5引入,是Runnable的增强版。

这篇文章来探索一下两个接口的应用差异。

2 执行机制

两个接口都是被设计成表示多线程执行的任务。Runnable任务可以由Thread类或者ExecutorService执行,而Callable任务只能由ExecutorService执行。

3 返回值

让我们深入看看这两个接口如何处理返回值。

3.1 使用Runnable

Runnable是一个函数式接口类,只有一个run()方法。不接收传参,也不返回结果。

这适用于不需要线程执行结果的场景,比如事件日志。

1
2
3
public interface Runnable {
public void run();
}

通过下述例子理解下:

1
2
3
4
5
6
7
8
9
public class EventLoggingTask implements  Runnable{
private Logger logger
= LoggerFactory.getLogger(EventLoggingTask.class);

@Override
public void run() {
logger.info("Message");
}
}

在这个例子中,线程仅仅从队列读取消息并记录到日志文件中。这个任务没有返回值,可以由ExecutorService执行:

1
2
3
4
5
public void executeTask() {
executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new EventLoggingTask());
executorService.shutdown();
}

这里的Future对象不会持有任何值。

3.2 使用Callable

Callable是一个泛型接口类,含有一个call()方法,返回泛型值V:

1
2
3
public interface Callable<V> {
V call() throws Exception;
}

看看一个数值阶乘的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FactorialTask implements Callable<Integer> {
int number;

// standard constructors

public Integer call() throws InvalidParamaterException {
int fact = 1;
// ...
for(int count = number; count > 1; count--) {
fact = fact * count;
}

return fact;
}
}

下面测试中call()方法返回值由Future对象持有。

1
2
3
4
5
6
7
@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
FactorialTask task = new FactorialTask(5);
Future<Integer> future = executorService.submit(task);

assertEquals(120, future.get().intValue());
}

4 异常处理

接下来看看怎样处理异常合适。

4.1 使用Runnable

因为它的方法声明没有抛出异常,所以没有办法传递异常出去。

4.2 使用Callable

Callable’s call()方法含有“throws Exception”,因此可以抛异常出去。

1
2
3
4
5
6
7
8
9
10
public class FactorialTask implements Callable<Integer> {
// ...
public Integer call() throws InvalidParamaterException {

if(number < 0) {
throw new InvalidParamaterException("Number should be positive");
}
// ...
}
}

如果使用ExecutorService执行Callable,异常会存在Future对象中,可通过调用Future.get()方法来核实。实际上,它会抛出ExecutionException, 封装了原来的异常。

1
2
3
4
5
6
7
@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {

FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
Integer result = future.get().intValue();
}

上面的测试中,我们传进去了一个不合法的整数导致抛了ExecutionException。可以通过调用这个异常对象的getCause()方法,可以获取原来的异常。

如果没有调用Futureget()方法 ,由call()方法抛出的异常就不会抛到外层,而任务也被标记为完成。

1
2
3
4
5
6
7
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);

assertEquals(false, future.isDone());
}

上面的测试会正常通过,尽管任务会因为传入了非法的整数抛了异常。

5 总结

这篇文章探索了RunnableCallable接口类之间的差异。


本文为译文,作者通过翻译达到学习目的。 原文链接 | 原文源码链接 | 本站源码链接