Callable、Runnable、Future 和 FutureTask

CallableFuture 是 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,代表异步计算的结果。

FutureTaskRunnableFuture 接口的实现类,RunnableFuture 接口实现了 RunnableFuture 接口。因此 FutureTask 是可以交给 Thread 或者 Executor 执行的。

以下是 FutureTask 的继承关系:

FutureTask 的继承关系

根据 FutureTask.run() 方法被执行的时机,FutureTask 可以处于下面 3 种状态:

  • 未启动:FutureTask.run() 方法还没有被执行之前,就处于未启动状态;
  • 已启动:FutureTask.run() 方法在被执行过程中,FutureTask 处于已启动状态;
  • 已完成:FutureTask.run() 方法执行完成后正常结束,或者执行 FutureTask.cacel(boolean),被取消,或者执行 FutureTask.run() 方法时抛出异常而结束,就处于已完成状态;

以下是 FutureTask 的状态变化图:

扫描二维码关注公众号,回复: 16054261 查看本文章

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 的 get 和 cancel 执行示意图

可以把 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() 方法会阻塞当前线程,知道任务执行完成返回结果为止。

对于 CallableFuture 的使用:

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 的时间被浪费,如果采用前面两种组合,则效率会提高:

  1. 先把 A 方法的内容放到 Callable 实现类的 call() 方法中;
  2. 在主线程中通过线程池执行 A 任务;
  3. 执行后面方法中 10s 不依赖方法 A 运行结果的代码
  4. 获取方法 A 的运行结果,执行后面方法中 10s 依赖方法 A 运行结果的代码;

这样代码的效率就提高了,程序不必卡在 A 方法处。

参考

搞懂 Runnable、Future、Callable、FutureTask 之间的关系,这篇就够了

猜你喜欢

转载自blog.csdn.net/xingyu19911016/article/details/130193410