写在前面
Java中创建线程的方式有很多种(本质上只有一种),其中,使用 Runnable
接口和 Callable
接口都可以实现线程任务的定义,但二者在使用上有一些区别,比如是否有返回值、是否能抛出异常等。本文从接口定义、区别、示例代码等方面,对两个接口进行介绍。
接口定义
Runnable
接口:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface {@code Runnable} is used
* to create a thread, starting the thread causes the object's
* {@code run} method to be called in that separately executing
* thread.
* <p>
* The general contract of the method {@code run} is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Callable
接口:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
注意到:
① 二者均有FunctionalInterface
注解,即二者都是函数式接口,都可以使用lambda表达式简化对象的创建。
② Callable接口是一个泛型接口,有一个泛型参数V作为抽象方法call
的返回值。
二者区别
Runnable
接口是一个普通接口,Callable
接口是一个泛型接口,有一个泛型参数V。Runnable
接口中的run方法没有返回值,Callable
接口中的Call方法有返回值。Runnable
接口中的run方法没有抛出异常,所有的可检查异常(Checked Exception)都需要在run方法中自行处理,而Callable
接口的Call方法使用throws抛出了异常,可以在Call方法的外部捕捉到异常进行处理。- 在创建线程时,
Runnable
可以直接传入Thread
类的构造方法,而Callable
需要通过FutureTask
包装后再交给线程或线程池执行。
示例代码
创建两个线程,分别输出1-5。
Runnable
接口:
public class RunnableTest {
public static void main(String[] args) {
// 使用匿名内部类的方式创建实现了Runnable接口的实例
Runnable task = new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for(int num = 1; num <= 5; num++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadName + ":" + num);
}
}
};
// 创建两个线程执行任务
Thread t1 = new Thread(task, "t1");
Thread t2 = new Thread(task, "t2");
t1.start();
t2.start();
}
}
Callable
接口:
public class CallableTest {
public static void main(String[] args) {
// 创建 Callable 实例
Callable<String> task = new Callable<String>() {
@Override
public String call() throws Exception {
String threadName = Thread.currentThread().getName();
for (int num = 1; num <= 5; num++) {
Thread.sleep(1000);
System.out.println(threadName + ":" + num);
}
return threadName + " 执行完毕";
}
};
// 用 FutureTask 包装 Callable
FutureTask<String> futureTask1 = new FutureTask<>(task);
FutureTask<String> futureTask2 = new FutureTask<>(task);
// 创建线程并启动
Thread t1 = new Thread(futureTask1, "t1");
Thread t2 = new Thread(futureTask2, "t2");
t1.start();
t2.start();
try {
// 获取返回结果(会阻塞直到任务完成)
String result1 = futureTask1.get();
String result2 = futureTask2.get();
System.out.println(result1);
System.out.println(result2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
对Callable接口的进一步分析
FutureTask类是什么?
FutureTask
是concurrent包中提供的一个类,实现了RunnableFuture
接口(RunnableFuture
接口继承自Runnable
接口和Future
接口),内部包装了一个Callable
对象,它的run方法会调用Callable.call()
,并把返回值或异常保存起来。
换句话说,FutureTask
既是任务(实现了Runnable
接口),也是结果的容器。(实现了Future
接口)
为什么需要用FutureTask?
本质上是因为Thread
类的构造方法只接受实现了Runnable
接口的任务对象,为了在Thread
中执行Callable
,我们需要一个适配器,这个适配器就是FutureTask。
执行流程大概如下:
Thread.start() → Thread.run() → FutureTask.run() → Callable.call()
总结
简单来说,Runnable
接口和Callable
接口的区别在于:Callable
接口是一个泛型接口、其中的call
方法有返回值,会抛出异常。在创建线程时需要一个实现了Runnable
接口的对象,因此在使用Callable
接口创建线程时,需要用FutureTask
类包装一下。