baeldung 2024-05-11
1. 概述
自 Java 诞生之初,多线程一直是该语言的重要特性之一。Runnable 是 Java 提供的核心接口,用于表示多线程任务;而从 Java 1.5 开始,引入了 Callable 接口作为 Runnable 的增强版本。
在本教程中,我们将深入探讨这两个接口之间的区别及其适用场景。
2. 执行机制
这两个接口都用于表示可由多个线程执行的任务。我们可以使用 Thread 类或 ExecutorService 来运行 Runnable 任务,而 Callable 任务只能通过 ExecutorService 来执行。
3. 返回值
让我们更深入地看看这两个接口如何处理返回值。
3.1 使用 Runnable
Runnable 是一个函数式接口,它只包含一个不接受参数、也不返回任何值的 run() 方法:
public interface Runnable {
public void run();
}
这适用于我们不需要获取线程执行结果的场景,例如记录传入事件的日志:
public class EventLoggingTask implements Runnable {
private Logger logger = LoggerFactory.getLogger(EventLoggingTask.class);
@Override
public void run() {
logger.info("Message");
}
}
在此示例中,线程仅从队列中读取消息并将其记录到日志文件中,任务不会返回任何值。
我们可以使用 ExecutorService 启动该任务:
public void executeTask() {
executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new EventLoggingTask());
executorService.shutdown();
}
在这种情况下,Future 对象不会包含任何返回值。
3.2 使用 Callable
Callable 是一个泛型接口,包含一个返回泛型值 V 的 call() 方法:
public interface Callable<V> {
V call() throws Exception;
}
下面是一个计算数字阶乘的例子:
public class FactorialTask implements Callable<Integer> {
int number;
// 标准构造函数
public Integer call() throws InvalidParamaterException {
int fact = 1;
// ...
for (int count = number; count > 1; count--) {
fact = fact * count;
}
return fact;
}
}
call() 方法的结果会封装在 Future 对象中返回:
@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
由于 run() 方法签名中没有声明 throws 子句,因此无法传播受检异常(checked exceptions)。
4.2 使用 Callable
Callable 的 call() 方法声明了 throws Exception,因此可以轻松地将受检异常向上抛出:
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,其中包装了原始异常:
@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() 方法来获取原始的受检异常。
如果我们不调用 Future 的 get() 方法,那么 call() 方法中抛出的异常将不会被报告,且该任务仍会被标记为已完成:
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled() {
FactorialCallableTask task = new FactorialCallableTask(-5);
Future<Integer> future = executorService.submit(task);
assertEquals(false, future.isDone());
}
即使我们在 FactorialCallableTask 中对负数参数抛出了异常,上述测试仍然会成功通过。
5. 结论
在本文中,我们探讨了 Runnable 与 Callable 接口之间的主要区别:
- 返回值:
Runnable无返回值,Callable可返回泛型结果。 - 异常处理:
Runnable无法抛出受检异常,而Callable可以。 - 执行方式:
Runnable可通过Thread或ExecutorService执行,Callable仅能通过ExecutorService执行,并通过Future获取结果和异常。
根据具体需求选择合适的接口,能够使多线程编程更加灵活和健壮。