Java中的Runnable与Callable

2023/06/07

1. 概述

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

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

2. 执行机制

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

3. 返回值

让我们更深入地了解一下这些接口处理返回值的方式。

3.1 Runnable

Runnable接口是一个函数接口,只有一个run()方法,它不接收任何参数,也不返回任何值。

这适用于我们不需要执行线程返回结果的情况,例如,对传入的事件做日志记录:

public interface Runnable {
    void run();
}

让我们通过一个例子来理解这一点:

public class EventLoggingTask implements Runnable {
    private final Logger logger = LoggerFactory.getLogger(EventLoggingTask.class);

    @Override
    public void run() {
        String message = "Message read from the event queue";
        logger.info("Message read from event queue is " + message);
    }
}

在本例中,线程将从队列中读取一条消息,并将其记录在日志文件中。任务没有返回任何值;

我们可以使用ExecutorService启动该任务:

public class TaskRunner {

    public static void main(String[] args) {
        executeTask();
    }

    private static void executeTask() {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        EventLoggingTask task = new EventLoggingTask();
        Future<?> future = executorService.submit(task);
        executorService.shutdown();
    }
}

在这种情况下,submit()方法返回的Future对象将不包含任何值。

3.2 Callable

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

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

让我们来看一个计算数字阶乘的例子:

public class FactorialTask implements Callable<Integer> {
    int number;

    public FactorialTask(int number) {
        this.number = number;
    }

    public Integer call() throws InvalidParameterException {
        int fact = 1;
        if (number < 0)
            throw new InvalidParameterException("Number must be positive");

        for (int count = number; count > 1; count--)
            fact = fact * count;

        return fact;
    }

    private static class InvalidParameterException extends Exception {
        public InvalidParameterException(String message) {
            super(message);
        }
    }
}

call()方法的结果将在Future对象中返回:

class FactorialTaskManualTest {
    private ExecutorService executorService;

    @BeforeEach
    void setup() {
        executorService = Executors.newSingleThreadExecutor();
    }

    @Test
    void whenTaskSubmitted_ThenFutureResultObtained() throws ExecutionException, InterruptedException {
        FactorialTask task = new FactorialTask(5);
        Future<Integer> future = executorService.submit(task);
		
        assertEquals(120, future.get().intValue());
    }

    @AfterEach
    void cleanup() {
        executorService.shutdown();
    }
}

4. 异常处理

让我们看看它们在异常处理方面的不同。

4.1 Runnable

由于方法签名没有指定“throws”子句,因此无法传播进一步受检异常

4.2 Callable

Callable的call()方法包含“throws Exception”子句,因此我们可以轻松地进一步传播受检异常:

public class FactorialTask implements Callable<Integer> {

    public Integer call() throws InvalidParamaterException {
        int fact = 1;
        if (number < 0)
            throw new InvalidParamaterException("Number must be positive");
        for (int count = number; count > 1; count--)
            fact = fact * count;
        return fact;
    }
}

如果使用ExecutorService运行Callable,则会在Future对象中收集异常。可以通过调用Future.get()方法来检查该对象,这将抛出一个ExecutionException,它包装了原始异常:

@Test
void whenException_ThenCallableThrowsIt() throws ExecutionException, InterruptedException {
    FactorialTask task = new FactorialTask(-5);
    Future<Integer> future = executorService.submit(task);
	
    assertThrows(ExecutionException.class, future::get);
}

在上面的测试中,当我们传递一个无效的数字时,会抛出ExecutionException。我们可以在此异常对象上调用getCause()方法来获取原始的受检异常。

如果我们不调用Future类的get()方法,那么call()方法引发的异常将不会被引出,并且任务仍将标记为已完成:

@Test
void whenException_ThenCallableDoesntThrowsItIfGetIsNotCalled() {
    FactorialTask task = new FactorialTask(-5);
    Future<Integer> future = executorService.submit(task);
	
    assertFalse(future.isDone());
}

尽管我们已经在FactorialTask抛出了InvalidParameterException异常,但上述测试仍将成功通过。

5. 总结

在本文中,我们探讨了Runnable和Callable接口之间的区别。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章