侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 2 条评论

Runnable vs Callable

翻斗
2020-03-21 / 0 评论 / 0 点赞 / 575 阅读 / 2,908 字

Baeldung 翻译系列之Java并发基础:

Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程

1. 概览

自Java早期以来,多线程一直是该语言的主要方面。Runnable是为表示多线程任务提供的核心接口,而Java 1.5提供了Callable作为Runnable的改进版本。

在本教程中,我们将探讨这两个接口的区别和应用。

2. 执行机制

这两个接口都设计用来表示可以由多个线程运行的任务。我们可以使用Thread类或ExecutorService来运行Runnable任务,而我们只能使用后者来运行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;

    // standard constructors

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

        return fact;
    }
}

call()方法的返回一个Future对象, 这个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的情况

由于方法签名没有指定"throws"子句,我们没有办法进一步传播受检异常。

4.2. 使用Callable的情况

Callablecall()方法包含"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. 总结

在这篇文章中,我们探讨了RunnableCallable接口之间的差异。

和往常一样,这篇文章的完整代码可以在GitHub或者Gitee上找到。

0

评论区