FutureTask 中 get(timeout) 的超时是怎么玩的?

FutureTask 中 get(timeout) 的超时是怎么玩的?

昨天晚上在一个交流群里一位群友提出了一个问题,他想实现一种客户端功能,可以让客户端调用其他接口的时候,如果超时,就返回 null。这个问题好处理,直接使用 Future 即可,即这个方法:

public interface Future<V> {
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

但是这位群友又提出了一个有意思的问题:如果使用线程池的话,达到最大核心线程数了,需要进入队列等待,超时时间的起始时间应该是在执行的时候算的吧?
首先关于线程池的执行流程,这个没毛病,问题的关键是 Future#get 是从啥时候开始算超时的呢,比如现在这个任务 01:00:00 被丢到了线程池中执行,但是 01:00:05 才开始执行这个任务,那算获取任务执行结果是否超时的时候总得有个开始时间吧,那么开始时间是从 01:00:00(即丢到线程池的时候)开始算还是从 01:00:05(任务具体开始执行的时候)开始算呢?这个问题其实不难想,但是还是有点偏,突然被问到的时候还是有点愣的感觉。
接下来先看一个例子:

package dongguabai.demo.juc.future.demo;

import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author Dongguabai
 * @Description
 * @Date 创建于 2021-01-07 00:35
 */
public class FutureTaskDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService service = Executors.newFixedThreadPool(1);
        final Future<Object> submit = service.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                System.out.println(new Date().toLocaleString() + "--开始执行");
                Thread.sleep(5000);
                System.out.println(new Date().toLocaleString() + "--执行完成");
                return "OK";
            }
        });
        Thread.sleep(4000);
        try {
            final Object o = submit.get(2, TimeUnit.SECONDS);
            System.out.println();
        } catch (TimeoutException e) {
            System.out.println(new Date().toLocaleString() + "--超时了");
        }
        service.shutdown();
    }
}

运行结果:

2021-1-7 0:39:21--开始执行
2021-1-7 0:39:26--执行完成

可以发现超时时间既不是从任务被丢到线程池的时候开始算也不是从任务具体开始执行的时候开始算,而是啥时候 get 就从啥时候开始算,换句话说,这里的超时是对调用线程来说的,与执行任务的线程没啥关系(这其实是一个很容易被忽视的一点,就是调用方觉得失败了,但是这个任务是有可能执行成功的)。因为如果从任务丢到线程池的时候开始算,中途 main 线程已经休眠了 4s 了,早该超时了,如果是从任务具体开始执行的时候开始算,这个任务要执行至少 5s,如果要超时,也早就超时了。
接下来就简单分析一下 java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit) 这个方法:

    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

一开始会判断 state 状态是否小于等于 COMPLETING。回过头看一下 java.util.concurrent.AbstractExecutorService#submit(java.util.concurrent.Callable<T>) 方法:

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

可以发现传入的 Callable 被封装成了 FutureTask

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

此时 FutureTaskstateNEWFutureTask 有这么几种状态:

    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

这里看一下 NEWCOMPLETING,从名字都可以看出 NEW 是一个初始状态,此时任务还没有开始执行。接着看下 java.util.concurrent.FutureTask#run 方法,这个也是具体执行任务的方法:

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

本质上执行的是 FutureTaskCallablecall 方法,获得执行结果,执行过程中会可能发生异常也可能顺利执行,执行成功则会执行 set(result) 方法保存结果,出现异常会执行 setException(ex) 保存异常:

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

可以发现 COMPLETING 状态就是一个中间状态,处于任务执行完成或出现异常,但是还未将结果保存起来的过程中,正常执行完成且结果保存后状态为 NORMAL ,执行失败异常保存后状态为 EXCEPTIONAL
再回到 java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit) 方法,判断 s 是否小于等于 COMPLETING 其实就是在判断任务是否执行完成或者出现异常了。
再看 java.util.concurrent.FutureTask#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()) {
                //第一次 q == null
                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 == null,new 一个 WaitNode,创建一个头节点
                q = new WaitNode();
            else if (!queued)
                //第二次就是把 q 设置为 waiters,即 waiters = q = new WaitNode()
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {
                
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    //如果时间到了,直接返回 state,还没有执行完成或者抛出异常 state 则仍然为 COMPLETING
                    removeWaiter(q);
                    return state;
                }
                //时间还没到,就再阻塞一会
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

其实到这里群友提的问题就已经有答案了,超时时间是从 get 的时候开始算的,啥时候 get 啥时候开始算。
最后看一下 WaitNode,主要是用来存储因为 get 被挂起的线程:

    static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

setsetException 方法可以看出最终都会调用 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
    }

主要就是用来唤醒等待执行结果的线程。

欢迎关注公众号
​​​
在这里插入图片描述

### Java `FutureTask` 类介绍 `FutureTask` 是 Java 中的一个具体类,实现了 `RunnableFuture` 接口。此接口结合了 `Runnable` 和 `Future` 的功能,允许对象作为任务提交给执行器并能够返回结果或抛出异常[^2]。 #### 主要特性 - **可取消的异步计算**:支持通过调用 `cancel()` 方法来终止尚未完成的任务。 - **状态管理**:内部维护着任务的状态变化(如启动、运行中、已完成),这些状态对于外部不可见但影响对外行为。 - **灵活性高**:既可以直接实例化后交给线程池执行,也能独立于任何框架之外自行创建线程执行。 ### 使用方法 为了更好地理解如何使用 `FutureTask` ,下面给出一段简单的代码示例: ```java import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Example { public static void main(String[] args) throws InterruptedException, ExecutionException { Callable<Integer> task = () -> { Thread.sleep(1000); // Simulate a long-running operation. return 42; // The result of the computation. }; FutureTask<Integer> futureTask = new FutureTask<>(task); // Create and start thread to run the FutureTask Thread workerThread = new Thread(futureTask); workerThread.start(); System.out.println("Doing some other work..."); Integer result = futureTask.get(); // Wait for completion or timeout/exception System.out.println("Result from async task: " + result); } } ``` 这段程序展示了怎样定义一个带有返回值的任务 (`Callable`) 并将其封装到 `FutureTask` 对象里;之后再把这个 `FutureTask` 提供给一个新的工作线程去执行。主线程继续做其他事情直到调用了 `get()` 来等待子线程结束并取回运算的结果。 如果尝试在一个已经完成或者被取消的任务上调用 `get()` 方法,则会立即得到相应的结果或是遇到由之前发生的错误所引发的异常。另外需要注意的是,在某些情况下可能会触发超时异常(`TimeoutException`) 或者中断异常(`InterruptedException`)[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值