Callable
和 Future
是 Java 在后续版本中引入的,Callable
类似于 Runnable
接口,实现 Callback
接口的类与实现 Runnable
接口的类都可以用于被被线程执行的任务。
以下是两个接口的相关源码:
// /xref/libcore/ojluni/src/main/java/java/util/concurrent/Callable.java
public interface Callable<V> {
V call() throws Exception;
}
// /xref/libcore/ojluni/src/main/java/java/lang/Runnable.java
public interface Runnable {
public abstract void run();
}
通过以上源码可以看出:Callable.call()
方法是有返回值的,并且可以抛出异常;Runable.run()
方法没有返回值而且不会抛出异常。
Callable
接口可以看作是 Runnable
接口的补充,和 Runnable
的区别在于它会返回执行任务的结果,并且可以抛出异常,一般是配合 ThreadPoolExecutor
使用。
Future
是一个接口,它可以对 Runnable
或者 Callable
任务进行取消、判断任务是否取消、查询任务是否完成以及获取任务结果。以下是 Future
的相关源码:
public interface Future<V> {
// 取消任务的执行。任务已经完成或者已经被取消时调用此方法,会返回 false
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否被取消
boolean isCancelled();
// 判断任务是否完成。正常完成、异常以及被取消,都将返回 true
boolean isDone();
// 阻塞地获取任务的执行结果
V get() throws InterruptedException, ExecutionException;
// 在一定时间内,阻塞地获取任务的执行结果
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
但是由于 Future
只是一个接口,为了使用它里面的功能,必须使用它的间接实现类 FutureTask
,代表异步计算的结果。
FutureTask
是 RunnableFuture
接口的实现类,RunnableFuture
接口实现了 Runnable
和 Future
接口。因此 FutureTask
是可以交给 Thread
或者 Executor
执行的。
以下是 FutureTask
的继承关系:
根据 FutureTask.run()
方法被执行的时机,FutureTask
可以处于下面 3 种状态:
- 未启动:
FutureTask.run()
方法还没有被执行之前,就处于未启动状态; - 已启动:
FutureTask.run()
方法在被执行过程中,FutureTask
处于已启动状态; - 已完成:
FutureTask.run()
方法执行完成后正常结束,或者执行FutureTask.cacel(boolean)
,被取消,或者执行FutureTask.run() 方法时抛出异常而结束
,就处于已完成状态;
以下是 FutureTask
的状态变化图:
对于 FutureTask.get()/FutureTask.get(long, TimeUnit)
方法:
- 当
FutureTask
处于未启动或已启动的状态时,执行FutureTask.get()/FutureTask.get(long, TimeUnit)
方法将导致线程阻塞; - 当
FutureTask
处于已完成状态时,执行FutureTask.get()/FutureTask.get(long, TimeUnit)
方法将导致调用线程立即返回结果或者抛出异常;
对于 FutureTask.cancel(boolean)
方法:
- 当
FutureTask
处于未启动状态时,执行FutureTask.cancel(boolean)
方法将导致此任务永远不会被执行; - 当
FutureTask
处于已启动状态时,执行FutureTask.cancel(true)
方法将以中断执行此任务线程的方式来试图停止任务; - 当
FutureTask
处于已启动状态时,执行FutureTask.cancel(false)
方法将不会对正在执行的任务的线程产生影响(让正在执行的任务运行完成); - 当
FutureTask
处于已完成状态时,执行FutureTask.cancel(boolean)
方法将返回false
;
可以把 FutureTask
交给 Executor
执行,也可以通过 <T> Future<T> ExecutorService.submit(Runnable, T)/Future<?> ExecutorService.submit(Runnable)
方法返回一个 Future
对象,但是由于 Future
是一个接口,所以这里一般是返回 FutureTask
对象。之后就可以调用 FutureTask.get()/get(long, TimeUnit)
方法或者 FutureTask.cancel(boolean)
方法。
当一个线程需要等待另一个线程把某个任务执行完成后它才能继续执行,此时就可以使用 FutureTask
。假如有多个线程执行若干个任务。当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完成后才能继续执行。
以下是 FutureTask
的代码:
private final ConcurrentHashMap<Object, Future<String>> taskCache = new ConcurrentHashMap<>();
private String executionTask(final String taskName) {
while (true) {
Future<String> future = taskCache.get(taskName); // 1.1 2.1
if (future == null) {
Callable<String> task = new Callable<String>() {
@Override
public String call() throws Exception {
return taskName;
}
};
FutureTask<String> futureTask = new FutureTask<>(task);
future = taskCache.putIfAbsent(taskName, futureTask); // 1.3
if (future == null) {
future = futureTask;
futureTask.run(); // 1.4 执行任务
}
}
try {
return future.get(); // 1.5 2.2 线程在此等待任务执行完成
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
以下是代码示意图:
当两线程试图同时执行同一个任务时,如果 Thread 1 在执行万 1.3 后,Thread 2 执行 2.1,那么接下来 Thread 2 将在 2.2 等待,直到 Thread 1 执行完 1.4 后,Thread 2 才能从 2.2 返回。
以下是 FutureTask
的用法:
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
Thread.sleep(100);
futureTask.cancel(true); // 1
System.out.println("future is cancel: " + futureTask.isCancelled());
if (!futureTask.isCancelled()) {
System.out.println("future is cancelled");
}
System.out.println("future is done: " + futureTask.isDone());
if (!futureTask.isDone()) {
System.out.println("future get = " + futureTask.get()); // 2
} else {
System.out.println("task is done");
}
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + "-call is running");
Thread.sleep(5000);
return "Hello World";
}
}
以下是执行结果:
在注释 1 处尝试取消任务的执行,如果该方法在任务已完成或者已取消则返回 false
,如果能够取消还未完成的任务,则返回 true
,因为在上面的代码中由于任务还处于休眠状态,所以可以取消任务。在注释 2 处,FutureTask.get()
方法会阻塞当前线程,知道任务执行完成返回结果为止。
对于 Callable
和 Future
的使用:
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<String> call = new MyCallable();
Future<String> future = executorService.submit(call);
executorService.shutdown();
Thread.sleep(5000);
System.out.println("主线程休眠 5 秒,当前时间: " + System.currentTimeMillis());
String str = future.get();
System.out.println("Future 已拿到数据,str = " + str + "; 当前时间为: " + System.currentTimeMillis());
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("进入 call 方法,开始休眠,休眠的时间为: " + System.currentTimeMillis());
Thread.sleep(10000);
return "Hello World";
}
}
执行结果:
在上面的代码中,future
是直接放到线程池中去执行的。从执行结果来看,主线程虽然休眠了 5s,但是从 call()
方法执行拿到任务的结果,这中间的时间差是 10s,说明 FutureTask.get() 方法会阻塞当前线程直到任务完成。
ExecutorService.shutdow()
方法,线程池就处于 SHUTDOWN
的状态,此时线程池不能够接收新的任务,它会等待所有任务执行完毕。如果调用 ExecutorService.shutdownNow()
方法,则线程处于 STOP
状态,此时线程池不能够接受新的任务,并且会去尝试终止正在执行的结果。
通过 FutureTask
也可以达到同样的效果:
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<String> call = new MyCallable();
FutureTask<String> task = new FutureTask<>(call);
executorService.submit(task);
executorService.shutdown();
Thread.sleep(5000);
System.out.println("主线程休眠 5 秒,当前时间: " + System.currentTimeMillis());
String str = task.get();
System.out.println("Future 已拿到数据,str = " + str + "; 当前时间为: " + System.currentTimeMillis());
}
}
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("进入 call 方法,开始休眠,休眠的时间为: " + System.currentTimeMillis());
Thread.sleep(10000);
return "Hello World";
}
}
执行结果:
以上的组合带来了以下的变化:如果有一种场景,方法 A 返回一个数据需要 10s,A 方法后面的代码运行需要 20s,但是在 20s 的执行结果中,只有后面 10s 依赖于方法 A 执行的结果。如果与以往采用同样的方式,势必会有 10s 的时间被浪费,如果采用前面两种组合,则效率会提高:
- 先把 A 方法的内容放到
Callable
实现类的call()
方法中; - 在主线程中通过线程池执行 A 任务;
- 执行后面方法中 10s 不依赖方法 A 运行结果的代码
- 获取方法 A 的运行结果,执行后面方法中 10s 依赖方法 A 运行结果的代码;
这样代码的效率就提高了,程序不必卡在 A 方法处。