1. 概述

从Java早期开始,多线程就是语言的核心特性。Runnable是表示多线程任务的基础接口,而Java 1.5引入的Callable则是Runnable的改进版。本文将深入探讨两者的区别及应用场景。

2. 执行机制

两个接口都设计用于表示可被多线程执行的任务:

  • Runnable可通过Thread类或ExecutorService执行
  • Callable只能通过ExecutorService执行
  • ⚠️ 直接用Thread启动Callable会编译报错

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是泛型接口,其call()方法返回泛型值V

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;
    }
}

结果通过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子句:

  • ❌ 无法传播受检异常
  • ⚠️ 只能通过try-catch内部处理
  • 常见踩坑:未捕获异常会导致线程静默终止

4.2 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执行时,异常会被收集到Future对象:

  • 调用Future.get()会抛出ExecutionException(包装原始异常)
  • 不调用get()则异常不会报告,任务仍标记为完成
@Test(expected = ExecutionException.class)
public void whenException_ThenCallableThrowsIt() {
    FactorialCallableTask task = new FactorialCallableTask(-5);
    Future<Integer> future = executorService.submit(task);
    Integer result = future.get().intValue();
}
@Test
public void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled(){
    FactorialCallableTask task = new FactorialCallableTask(-5);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(false, future.isDone());
}

5. 总结

特性 Runnable Callable
返回值 ❌ 无 ✅ 有(通过Future获取)
异常处理 ❌ 无法传播受检异常 ✅ 支持异常传播
执行方式 ✅ Thread/ExecutorService ❌ 仅ExecutorService
Java版本 1.0+ 1.5+

简单粗暴的选择建议:

  • 需要返回结果或处理异常 → 用Callable
  • 简单异步任务 → 用Runnable

完整代码示例请查看GitHub仓库


原始标题:Runnable vs. Callable in Java