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
}
此时 FutureTask
的 state
为 NEW
。FutureTask
有这么几种状态:
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;
这里看一下 NEW
和 COMPLETING
,从名字都可以看出 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);
}
}
本质上执行的是 FutureTask
的 Callable
的 call
方法,获得执行结果,执行过程中会可能发生异常也可能顺利执行,执行成功则会执行 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(); }
}
从 set
和 setException
方法可以看出最终都会调用 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
}
主要就是用来唤醒等待执行结果的线程。
欢迎关注公众号