Runnable和Callable接口的区别是什么?多线程返回值与异常处理详解

 更新时间:2026年10月07日 11:28:47   作者:yyangqqian  
还在为多线程任务无法返回结果而烦恼吗?本文深入对比Runnable和Callable接口的核心区别,教你如何通过Callable和Future获取线程执行结果,并利用ExecutorCompletionService优化任务完成顺序,避免不必要的等待,掌握这些技巧,让你的多线程程序更高效、更可控

Callable接口

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

Runnable接口

public interface Runnable {
    public abstract void run();
}

Runnable和Callable的区别

相同点

1、两者都是接口;(废话)

2、两者都可用来编写多线程程序;

3、两者都需要调用Thread.start()启动线程;

不同点

1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;

2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

Callable工作的Demo

package com.callable.runnable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Created on 2016/5/18.
 */
public class CallableImpl implements Callable<String> {

    public CallableImpl(String acceptStr) {
        this.acceptStr = acceptStr;
    }

    private String acceptStr;

    @Override
    public String call() throws Exception {
        // 任务阻塞 1 秒
        Thread.sleep(1000);
        return this.acceptStr + " append some chars and return it!";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new CallableImpl("my callable test!");
        FutureTask<String> task = new FutureTask<>(callable);
        long beginTime = System.currentTimeMillis();
        // 创建线程
        new Thread(task).start();
        // 调用get()阻塞主线程,反之,线程不会阻塞
        String result = task.get();
        long endTime = System.currentTimeMillis();
        System.out.println("hello : " + result);
        System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
    }
}

ExcutorService中的excutor和submit方法的区别

两者都是将一个线程任务添加到线程池中并执行;

1、excutor没有返回值,submit有返回值,并且返回执行结果Future对象

2、excutor不能提交Callable任务,只能提交Runnable任务,submit两者任务都可以提交

3、在submit中提交Runnable任务,会返回执行结果Future对象,但是Future调用get方法将返回null(Runnable没有返回值)

使用场景

使用多线程任务校验被保人数据;

1、每个被保人都是一个线程任务,

2、每个线程任务执行完都需要告诉主线程执行成功还是失败

3、这里需要submit提交Callable任务返回Future对象,并通过Future.get方法来获取执行结果

FutureTask

FutureTask实现了RunnableFuture,RunnableFuture既实现了Runnbale又实现了Futrue这两个接口;它两者的结合体

FutureTask又包装了Callable(如果是Runnable最终会转化为Callable);

FutureTask可以通过Thread包装来直接执行,也可以提交给ExcutorService来执行,并可以直接通过get方法来获取执行结果;

ExecutorCompletionService

使用ExecutorCompletionService提交任务后会将执行结果放到阻塞队列中,使用take方法会得到结果,哪个任务先执行完成就先获取到这个任务的执行结果;

原理:

在ExecutorCompletionService维护了一个QueueingFuture(队列任务),当通过ExecutorCompletionService提交的任务执行完成后,将结果放入QueueingFuture中;然后通过take和poll方法获取执行结果时会阻塞线程,直到当QueueingFuture中有结果时就会立即返回;

如果自己维护一个list来存放future执行结果,会导致的问题是:

这样通过future.get方法来获取执行结果只能一个一个阻塞取出执行结果,如果后面的任务可能会先执行完成,后面的任务只有等待前面的任务执行完成得到结果后才能获取到结果,这样就浪费一定的时间在等待执行任务的结果上了。可以用ExecutorCompletionService来解决这个问题的。

总结:

1、自己创建一个集合来保存Future存根并循环调用其返回结果的时候,主线程并不能保证首先获得的是最先完成任务的线程返回值。它只是按加入线程池的顺序返回。因为take方法是阻塞方法,后面的任务完成了,前面的任务却没有完成,主程序就那样等待在那儿,只到前面的完成了,它才知道原来后面的也完成了。

2、使用CompletionService来维护处理线程的返回结果时,主线程总是能够拿到最先完成的任务的返回值,而不管它们加入线程池的顺序。

3、CompletionService的实现是维护了一个保存Future的BlockingQueque。只有当这个Future的任务状态是结束的时候,才会加入到这个Queque中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。也就是先完成的必定先被取出,这样就减少了不必要的等待时间。

注意:

使用Future获取多个任务的执行结果时,如果其中一个任务出现异常,就会直接中断不会获取后面任务的执行结果了,但是不会中断其他任务,其他任务会正常运行,只是结果无法获取了;

如果需要中断其他任务就需要在捕获到异常后执行ExecutorService.shutdownNow方法来关闭线程,也可以通过Future.cancle方法来中断

ExecutorService中shutdownNow和shutdown的区别:

  • shutdownNow表示立刻关闭线程池并中断正在执行的任务;
  • shutdown表示线程池中的任务全部执行完成后再关闭线程池

示例代码

使用CompletionService维护结果

System.out.println("start...");
        long begin = System.currentTimeMillis();
        // 创建一个线程池
        int taskSize = 5;
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<String>(pool);
        try {
            for (int i = 1; i <= taskSize; i++) {
                MyCallable callable = new MyCallable(new Integer(i));
                completionService.submit(callable);
            }
            pool.shutdown();
            for (int i = 1; i <= taskSize; i++) {
                System.out.println(completionService.take().get());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 关闭线程池
        long end = System.currentTimeMillis();
        System.out.println("end..excute time:" + (end - begin) + "ms");
        return;

自己创建list维护执行结果

System.out.println("start...");
                long begin = System.currentTimeMillis();
                ExecutorService pool = null;
                try {
                    int taskSize = 5;
                    // 创建一个线程池
                    pool = Executors.newFixedThreadPool(taskSize);
                    // 创建多个返回值的任务
                    List<Future> list = Lists.newArrayList();
                    for (int i = 1; i <= taskSize; i++) {
                        MyCallable callable = new MyCallable(new Integer(i));
                        Future<String> future = pool.submit(callable);
                        list.add(future);
                        System.out.println("已添加" + i);
                    }
                    for (int i = 0; i < list.size(); i++) {
                        System.out.println(list.get(i).get().toString());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 关闭线程池
                pool.shutdown();
                long end = System.currentTimeMillis();
                System.out.println("end..excute time:" + (end - begin) / 1000 + "s");
                return;

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Spring Boot指定外部配置文件简单示例

    Spring Boot指定外部配置文件简单示例

    Spring Boot可以让你将配置外部化,这样你就可以在不同的环境中使用相同的应用程序代码,这篇文章主要给大家介绍了关于Spring Boot指定外部配置文件的相关资料,需要的朋友可以参考下
    2024-01-01
  • Java日期时间调整的几种方式汇总

    Java日期时间调整的几种方式汇总

    Calendar类是一个抽象类,在实际使用时实现特定的子类的对象,创建对象的过程对程序员来说是透明的,只需要使用getInstance方法创建即可,这篇文章主要介绍了Java日期时间调整的几种方式,需要的朋友可以参考下
    2023-05-05
  • 通过简单步骤实现SpringMVC文件上传

    通过简单步骤实现SpringMVC文件上传

    这篇文章主要介绍了通过简单步骤实现SpringMVC文件上传,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • Spring注解之@Import使用方法讲解

    Spring注解之@Import使用方法讲解

    @Import是Spring基于Java注解配置的主要组成部分,下面这篇文章主要给大家介绍了关于Spring注解之@Import的简单介绍,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • 在idea中为注释标记作者日期操作

    在idea中为注释标记作者日期操作

    这篇文章主要介绍了在idea中为注释标记作者日期操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • Spring计划任务用法实例详解

    Spring计划任务用法实例详解

    这篇文章主要介绍了Spring计划任务用法,结合实例形式详细分析了spring计划任务相关原理、配置、使用方法及操作注意事项,需要的朋友可以参考下
    2019-11-11
  • Java实现使用Websocket发送消息详细代码举例

    Java实现使用Websocket发送消息详细代码举例

    这篇文章主要给大家介绍了关于Java实现使用Websocket发送消息的相关资料,WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-05-05
  • 关于JSONObject.toJSONString出现地址引用问题

    关于JSONObject.toJSONString出现地址引用问题

    这篇文章主要介绍了关于JSONObject.toJSONString出现地址引用问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • MyBatis实现递归查询的方法详解

    MyBatis实现递归查询的方法详解

    在项目开发过程中,往往会遇到多级菜单、分类等多层级结构数据的查询。本文就来为大家讲讲MyBatis实现递归查询的方法,感兴趣的可以动手尝试一下
    2022-08-08
  • Springboot实现视频上传及压缩功能

    Springboot实现视频上传及压缩功能

    这篇文章主要介绍了Springboot实现视频上传及压缩功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03

最新评论