Future与Callable原理

简介: 本文主要介绍Future与Callable原理,即如何在线程外获取线程执行结果以及其原理。

     本文主要介绍Future与Callable原理,即如何在线程外获取线程执行结果以及其原理。

 示例

1  示例一

以下示例代码通过线程池执行一个Callable,然后通过Future来获取返回结果。

public static void main(String[] args) throws Exception {
    Callable<Integer> callable = () -> {
        Thread.sleep(1000);
        Random random = new Random();
        return random.nextInt(100);
    };
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Future<Integer> future = executorService.submit(callable);
    System.out.println(DateUtil.getCurrentTime() + " ready to do task");
    Integer result = future.get();
   System.out.println(DateUtil.getCurrentTime() + " get task result! result=" + result);
}

此方法的执行结果如下,可以看出,主线程等待task执行了,1s以后线程执行完毕,返回结果后,主线程获取到结果并输出。

14:45:57:090 ready to do task

14:45:58:123 get task result! result=46

 

2  示例二

以下示例中,我们自己创建并执行线程:

    public static void main(String[] args) throws Exception {
        Callable<Integer> callable = () -> {
            Thread.sleep(1000);
            Random random = new Random();
            return random.nextInt(100);
        };

        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread thread = new Thread(task);
       System.out.println(DateUtil.getCurrentTime() + " ready to do task");
        thread.start();
        Integer result = task.get();
       System.out.println(DateUtil.getCurrentTime() + " get task result! result=" + result);
    }

输出结果如下所示

15:51:47:615 ready to do task

15:52:13:885 get task result! result=31

 

 原理分析

上面两个示例之所以能获取到线程的执行结果,而且其原理都是一样的。都源于Callable、Future、Runnable、FutureTask这几个类的支持,接下来我们将分析一下这是如何实现的。

1  原理概述

一个线程(例如threadA)获取另外一个线程(例如:threadB)的执行结果,这个功能基于两点实现:将对Runnable#run的执行转换成对Callable#call方法的调用,并存储返回结果;通过等待队列来管理等待结果的线程(类似于AQS)。

对于我们常用的FutureTask对象而言,可以理解成FutrueTask在Runnable、Callable中间做了一个转换器,将线程的Runnable#run方法的执行转换到对Callable#call方法的调用,因为run方法是线程中执行任务的方法,call本身有返回结果,所以FutureTask在运行run方法时只需要执行call方法,然后将执行的结果保存到一个地方,这样以后其他线程就能通过Future来获取其他线程的执行结果了。

简单而言,如下图所示:

3bfae24c9a1d813a8ebca4669d7c54abc33e02b7

创建Callable对象,并在其call()方法中添加需要执行的任务。

通过Callable创建FutureTask(taskB)。

通过task创建线程(threadB),然后执行此线程start()方法。

操作系统调度并执行threadB的run()方法,因为FutureTask实现了Runnable接口,所以此时执行的是FutureTask(taskB)中的run()方法。

threadA通过taskB.get()方法获取threadB的执行结果,如果threadB未执行完毕,那么threadA将被挂起。

FutureTask的run()方法主要逻辑是:执行Callable的call()方法;然后将call方法的返回结果set到FutrueTask的outcome字段中;然后唤醒等待此线程运行结果的线程(即threadA)。

 

2  Callable

Callable接口非常简单,只是声明了一个call方法。

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;
}

 

 

3  Future

Future可以代表一个异步计算的结果,通过get方法可获取此结果:如果计算尚未完成,那么当前线程将被挂起;如果计算已经完成,当前线程将会被唤醒并得到此结果。

以下是Jdk提供的一段使用Futrue的示例代码,这段代码和上面的「示例一」很像,所以就不过多介绍。

interface ArchiveSearcher {
    String search(String target);
}

ExecutorService executor = Executors.newSingleThreadExecutor();
ArchiveSearcher searcher = (target) -> {
    return "query=" + target + "  content=hello world";
};

void showSearch(final String target) throws InterruptedException {
    Future<String> future = executor.submit(() -> {
        return searcher.search(target);
    });

    try {
        displayText(future.get()); // use future
    } catch (ExecutionException ex) {
        cleanup();
        return;
    }
}

 

 

4  FutureTask

1)   继承结构

FutureTask是Future子类中最常用的一个,其继承结果如下图所示:

5291f45c7937f3f371cfa86b2e81153726b0d36d

从继承关系中可以看到,FutureTask实现了Runnable,所以能通过FutureTask创建一个线程,运行一些指定的任务;FutrueTask实现了Future,所以能实现返回异步计算的结果。

 

2)   对Runnable#run方法的封装

FutureTask实现了Runnable接口并实现了run方法,这是其能够实现返回线程结果最重要的原因。因为在run方法中会调用Callable#call方法,并将结果保存在下来,这样后面的线程只要能够访问FutureTask,就可以获取保存的结果。接下来我们将详细讨论有关的几个方法。

a)    run()方法

此方法主要逻辑是:

进行状态校验,如果线程已经启动那么将直接返回。

执行Callable的call()方法。

通过set方法,将执行结果更新到FutrueTask的outcome字段中,并唤醒等待此结果的线程。

码如下所示:

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                    null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
           handlePossibleCancellationInterrupt(s);
    }
}

 

b)   set()方法

set()方法的主要逻辑是:

将线程的状态更新为完成状态

将call方法的返回结果更新到FutureTask的outcome字段中;

唤醒等待此线程运行结果的线程。

源码如下所示:

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

 

3)   通过等待队列管理等待结果的线程

a)   管理挂起线程

假设现在有三个线程都在等待执行结果,并且调用get()以求获得结果的顺序是thread1、thread2、thread3(这里假设线程按照这个顺序被挂起),那么FutureTask中waiters会指向一个链表,如下所示:

28420f61a3aa201b72d8327a0fe96178e62c3d87

每一个线程通过get()方法获取结果时,因为任务还没有执行完,所以他们需要进入等待状态,即被挂起。在被挂起之前,每个线程都会创建一个WaitNode节点,并挂在waiters属性上。当线程执行完毕,就通过这个链表找到挂起的线程,接着就可以唤醒这些被挂起的线程了,最后返回线程的执行结果

 

b)    get()方法

线程可以通过FutureTask#get方法来获取其他线程的执行结果,这里返回的值,其实就是上面run方法中保存的那个值。

从源码可以看到,只要任务没有运行完,状态就不会是COMPLETING,线程就会通过awaitDone被挂起。

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

 

c)    awaitDone()方法

此方法就是用来将需要等待的线程挂起,挂起的逻辑可以参考上面「管理挂起线程」部分以及以下源码。注意,如果线程被中断或者等待时间超过时限了,那么等待队列将会被清理,而等待的线程将会被唤醒。

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                q.next = waiters, q);
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

 

d)    finishCompletion()方法

在set()方法中我们看到,首先将结果保存好,然后通过finishCompletion方法清理因为等待挂起的线程:清理链表,唤醒线程。

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                   LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();

    callable = null;        // to reduce footprint
}

 

 

相关文章
|
1月前
|
Java
Java并发编程:理解并使用Future和Callable接口
【2月更文挑战第25天】 在Java中,多线程编程是一个重要的概念,它允许我们同时执行多个任务。然而,有时候我们需要等待一个或多个线程完成,然后才能继续执行其他任务。这就需要使用到Future和Callable接口。本文将深入探讨这两个接口的用法,以及它们如何帮助我们更好地管理多线程。
|
5月前
|
Java
ExecutorService、Callable、Future实现有返回结果的多线程原理解析
ExecutorService、Callable、Future实现有返回结果的多线程原理解析
31 0
|
5月前
|
存储 Java
并发编程系列教程(09) - Callable与Future模式
并发编程系列教程(09) - Callable与Future模式
25 0
|
8月前
|
Java
【并发技术11】Callable与Future的应用
【并发技术11】Callable与Future的应用
|
Java Android开发
Android中Callable、Future、FutureTask的概念以及几种线程池的使用
在开始介绍线程池之前,先来介绍下`Callable`和`Future`的概念,众所周知,`Android`中实现多线程的方式有两种,实现`Runnable`接口或者继承一个`Thread`,但是这两种方式都有一个缺点:在任务执行完成之后没有返回结果,所以在`Java 1.5`之后,出现了`Callable`和`Future`,通过他们构建的线程,可以在线程执行完成之后得到返回结果。
214 0
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
5 多线程锁 5.1 锁的八个问题演示 package com.xingchen.sync; import java.util.concurrent.TimeUnit; class Phone { public static synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public synchronized void
84 0
|
Java
Callable、Future、FutureTask在多线程中的应用场景
Callable、Future、FutureTask在多线程中的应用场景
204 0
|
Java
Future和Callable学习
通常使用线程池+Runnable的时候,会发现Runnable不能返回值,也就执行的结果情况,同时对于出现异常,我们获取异常信息,进行相应的处理。如果需要返回结果,同时需要进一步加工的时候,就可以考虑使用Future+Callable了。同时接口Future的默认实现是FutureTask,因此对于其实现get()方法,会有一个问题,就是如果前面的任务一旦执行的时间耗时较长的时候,就会出现一直阻塞的状态,此时就会出现排队等待的状态,大大影响其性能。适用场景:当一个线程需要等待另一个线程把某个任务执行完成后它才能继续执行,此时可以使用FutureTask。因为FutureTask基于AQS实现,
73 0
Future和Callable学习
|
Java 调度 容器
Java中的Runnable、Callable、Future、FutureTask的区别与示例
Java中的Runnable、Callable、Future、FutureTask的区别与示例
168 0