文章目录
1. Future接口
在Java中,如果需要设定代码执行的最长时间,即超时,可以用Java线程池ExecutorService类配合Future接口来实现。 Future接口是Java标准API的一部分,在java.util.concurrent
包中。Future接口是Java线程Future模式的实现,可以来进行异步计算。
Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future 接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。
Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。
Future接口是一个泛型接口,严格的格式应该是Future,其中V代表了Future执行的任务返回值的类型。 Future接口的方法介绍如下:
boolean cancel(boolean mayInterruptInRunning) //取消一个任务,并返回取消结果。参数表示是否中断线程。
boolean isCancelled() //判断任务是否被取消
Boolean isDone() //判断当前任务是否执行完毕,包括正常执行完毕、执行异常或者任务取消。
V get() //获取任务执行结果,任务结束之前会阻塞。
V get(long timeout, TimeUnit unit) //在指定时间内尝试获取执行结果。若超时则抛出超时异常
2. 什么是FutureTask
2.1 继承了Future接口
FutureTask提供了对Future的基本实现,可以调用方法去开始和取消(调用cancel()
)一个计算,可以查询计算是否完成(调用isDone()
)并且获取计算结果。
只有当计算完成时才能获取到计算结果,一旦计算完成,计算将不能被重启或者被取消,除非调用runAndReset方法。
2.1 继承了Runnable接口
FutureTask还实现了Runnable接口,因此FutureTask交由Executor执行,或直接构建一个Thread实例,直接调用start()。
3. FutureTask状态转换
根据FutureTask的run方法执行的时机,FutureTask可以处于以下三种执行状态:
- 1、未启动:
在FutureTask.run()还没执行之前,FutureTask处于未启动状态。当创建一个FutureTask对象,并且run()方法未执行之前,FutureTask处于未启动状态。 - 2、已启动:
FutureTask对象的run方法启动并执行的过程中,FutureTask处于已启动状态。 - 3、已完成:
正常执行结束,或者执行被取消(调用FutureTask对象cancel()方法),或者FutureTask对象run的方法内执行抛出异常而导致中断而结束
3.1 状态的实现原理
FutureTask内部定义了state字段,并定义了7种状态:
private volatile int state;
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; //任务线程已中断
FutureTask中使用state表示任务状态,state值变更的由CAS操作保证原子性。
FutureTask对象初始化时,在构造器中把state设置为NEW
,之后状态的变更依据具体执行情况来定。
例如任务执行正常结束前,state会被设置成COMPLETING
,代表任务即将完成,接下来很快就会被设置为NORMAL
或者EXCEPTIONAL
,这取决于调用Runnable中的call()方法是否抛出了异常。有异常则后者,反之前者。
任务提交后、任务结束前取消任务,那么有可能变为CANCELLED
或者INTERRUPTED
。在调用cancel()方法时,如果传入false表示不中断线程,state会被置为CANCELLED,反之state先被变为INTERRUPTING
,后变为INTERRUPTED
。
总结下,FutureTask的状态流转过程,可以出现以下四种情况:
1. 任务正常执行并返回。 NEW -> COMPLETING -> NORMAL
2. 执行中出现异常。NEW -> COMPLETING -> EXCEPTIONAL
3. 任务执行过程中被取消,并且不响应中断。NEW -> CANCELLED
4.任务执行过程中被取消,并且响应中断。 NEW -> INTERRUPTING -> INTERRUPTED
4. 例子
没有采用原文例子,那个有点复杂,我们来个最简单的:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws Exception {
// 创建Callable接口实例
Callable<Integer> call = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("正在计算结果...");
Thread.sleep(3000);
return 1;
}
};
// 利用Callable接口实例创建一个FutureTask实例
FutureTask<Integer> task = new FutureTask<>(call);
// FutureTask实例构建线程
Thread thread = new Thread(task);
thread.start(); //启动子线程
// do something
System.out.println(" 干点别的...");
Integer result = task.get(); //阻塞等待子线程结束
System.out.println("拿到的结果为:" + result);
}
}
执行结果:
干点别的...
正在计算结果...
拿到的结果为:1 //打印子线程的值
这个例子展示主线程等子线程的返回值。
5. 原理分析
5.1 public void run()
由上图得知,FutureTask间接实现了Runnable接口,当构建的线程被启动时,会调用run(),下面我们从该方法入手:
public void run() {
// 校验任务状态 校验当前任务状态是否为NEW以及runner是否已赋值。这一步是防止任务被取消。
if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null,Thread.currentThread()))
return;
try {
Callable<V> c = callable; // double check 任务状态state
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//执行业务逻辑,也就是c.call()方法被执行
result = c.call();
ran = true;
} catch (Throwable ex) {
//如果业务逻辑异常,则调用setException方法将异常对象赋给outcome,并且更新state值
result = null;
ran = false;
setException(ex);
}
if (ran)
//如果业务正常,则调用set方法将执行结果赋给outcome,并且更新state值
set(result);
}
} finally {
// 重置runner
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
我们继续往下看,set(V v) 具体是怎么做的:
protected void set(V v) {
// state状态 NEW->COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
// COMPLETING -> NORMAL 到达稳定状态
UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
// 一些结束工作
finishCompletion();
}
}
态变更的原子性由unsafe对象提供的CAS操作保证。FutureTask的outcome变量存储执行结果或者异常对象,会由主线程返回。
5.2 get()
任务由线程池提供的线程执行,那么这时候主线程则会阻塞
,直到任务线程唤醒它们。我们通过get(long timeout, TimeUnit unit)方法看看是怎么做的:
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); //返回值
}
get的源码很简洁,首先校验参数,然后根据state状态判断是否超时,如果超时则异常,不超时则调用report(s)去获取最终结果。
当 s<= COMPLETING
时,表明任务仍然在执行且没有被取消。如果它为true,那么走到awaitDone
方法。
awaitDone
是futureTask
实现阻塞的关键方法,我们重点关注一下它的实现原理。
/**
1. 等待任务执行完毕,如果任务取消或者超时则停止
2. @param timed 为true表示设置超时时间
3. @param nanos 超时时间
4. @return 任务完成时的状态
5. @throws InterruptedException
*/
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)
// 可能任务线程被阻塞了,主线程让出CPU
Thread.yield();
else if (q == null)
// 等待线程节点为空,则初始化新节点并关联当前线程
q = new WaitNode();
else if (!queued)
// 等待线程入队列,成功则queued=true
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
// timed=false时会走到这里,挂起当前线程
LockSupport.park(this);
}
}
当没有结果时,当前线程会陷入阻塞,等待被唤醒:
注释里也很清楚的写明了每一步的作用,我们以设置超时时间为例,总结一下过程:
1、计算deadline,也就是到某个时间点后如果还没有返回结果,那么就超时了。
2、进入自旋,也就是死循环。
3、首先判断是否响应线程中断。对于线程中断的响应往往会放在线程进入阻塞之前,这里也印证了这一点。
4、判断state值,如果>COMPLETING表明任务已经取消或者已经执行完毕,就可以直接返回了。
5、如果任务还在执行,则为当前线程初始化一个等待节点WaitNode,入等待队列。这里和AQS的等待队列类似,只不过6、Node只关联线程,而没有状态。AQS里面的等待节点是有状态的。
7、计算nanos,判断是否已经超时。如果已经超时,则移除所有等待节点,直接返回state。超时的话,state的值仍然还是COMPLETING。
8、如果还未超时,就通过LockSupprot类提供的方法在指定时间内挂起当前线程,等待任务线程唤醒或者超时唤醒。
当线程被挂起之后,如果任务线程执行完毕,就会唤醒等待线程,这一步就是在finishCompletion里面做的,前面已经提到这个方法。我们再看看这个方法具体做了哪些事吧~
/**
* 移除并唤醒所有等待线程,执行done,置空callable
* nulls out callable.
*/
private void finishCompletion() {
//遍历等待节点
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;
// unlink to help gc
q.next = null;
q = next;
}
break;
}
}
//模板方法,可以被覆盖
done();
//清空callable
callable = null;
}
由代码和注释可以看出来,这个方法的作用主要在于唤醒等待线程
。由前文可知,当任务正常结束或者异常时,都会调用finishCompletion
去唤醒等待线程。这个时候,等待线程就可以醒来,开开心心的获得结果啦。
5.3 public boolean cancel(boolean mayInterruptIfRunning)
在此之前,我们先提个疑问,对于普通的任意线程,我们可以停掉他们吗?
参考《可以手动停止一个线程吗》
结论:取消操作cancel()不一定会起作用,完全取决于线程的状态和线程内部实现:
-
如果子线程被正常调度,并且没有在结束的临界点(也就是说子线程状态此时为
New
),返回值总是true
,根据cancel(mayInterruptIfRunning)
入参,决定是否去中断子线程
1.如果mayInterruptIfRunning==true
,尝试去中断此时,就是常规的去打断一个线程,调用线程的,其结果取决于是否有中断
- 如果子线程内部有sleep等语法,并处于相关状态下,可能会打断
- 如果子线程内部没有sleep等语法,此时子线程继续运行
2.如果
mayInterruptIfRunning==false
,不去中断,此时子线程继续运行 -
如果此时子线程尚未启动,可以中止线程,返回值为false
原理是直接修改线程状态为CANCELLED或INTERRUPTING,这样在调度器试图启动子线程时,发现已经不再需要启动了 -
子线程已经处于终止态,直接返回false
线程已经结束了,也就没啥事要干了
源码:
public boolean cancel(boolean mayInterruptIfRunning) {
/*
* 在状态还为NEW(尚未启动)的时候,根据参数中的是否允许中断,
* 将状态流转到INTERRUPTING或者CANCELLED。
*/
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try {
//如果允许中断,则尝试中断
if (mayInterruptIfRunning) {
try {
// 中断runner线程,机制还是线程的interrupt()方法,该方法依赖线程是否阻塞。
Thread t = runner;
if (t != null)
t.interrupt();
} finally {
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}
中断runner线程,机制还是线程的interrupt()方法,该方法依赖线程是否阻塞 ,原因是正在运行的线程是无法中断的,只有阻塞时才可以。原理参见 《线程中断详解》
5.3.1 验证子线程正常调度,子线程不含sleep语句
子线程正常调度,子线程不包含sleep语句,不会终止线程。
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
public class FutureDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
// 预创建线程
executorService.prestartCoreThread();
Future future = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws InterruptedException {
System.out.println("start to run future task"); // 如果打印, 表明线程真正被调度了
int timer = 0;
Long start = System.currentTimeMillis(); // 开始时间
while (true) {
Long current = System.currentTimeMillis();
if ((current - start) > 100000) {
System.out.println("当前任务执行已经超过100s");
return 1; //模拟耗时操作,超过100s自行结束
}
Long end = System.currentTimeMillis();
long between = (end - start) / 1000;// 除以1000是为了转换成秒
if (between >= 1) {
start = end;
timer++;
System.out.println("已经运行了" + timer + "秒"); // 如果每秒打印一次,表明线程一直在运行
}
}
}
});
Thread.sleep(5000); // 保证子线程先被调度运行
System.out.println(future.cancel(false)); //尝试停止线程
// System.out.println(future.cancel(true)); //与上面互换,不影响结果
}
}
执行结果:
start to run future task //显示子线程被调度了
已经运行了1秒
已经运行了2秒
已经运行了3秒
已经运行了4秒
已经运行了5秒
true //cancle返回值为true
已经运行了6秒 //子线程仍然在打印,说明没被停掉
已经运行了7秒
...
分析:我们在主线程休眠了5s,确保子线程被调度,休眠结束后,我们调用cancel(false)
试图去停止子线程,从结果来看,虽然返回值是true,但是实际上子线程仍然继续在运行;我们换做cancel(true)
时,结果也一样。
5.3.2 验证子线程正常调度,子线程包含sleep语句
子线程正常调度,子线程包含sleep语句,根据mayInterruptIfRunning,当其值为true,可以终止线程;其值为false,不会终止。
我们改造下5.3.1中的例子,在子线程内增加sleep语句:
Thread.sleep(10); //新增代码
Long end = System.currentTimeMillis(); //新增代码参考位置,在该行上部
执行入参mayInterruptIfRunning为false:
System.out.println(future.cancel(false));
执行结果:
start to run future task //显示子线程被调度了
已经运行了1秒
已经运行了2秒
已经运行了3秒
已经运行了4秒
true //cancle返回值为true
已经运行了5秒 //子线程仍然在打印,说明没被停掉
已经运行了6秒
执行入参mayInterruptIfRunning为true:
System.out.println(future.cancel(true));
执行结果:
start to run future task //显示子线程被调度了
已经运行了`1`秒
已经运行了`2`秒
已经运行了`3`秒
已经运行了`4`秒
已经运行了`5`秒
true //cancle返回值为true
//`不同点在这个地方,没有后续打印,线程被停掉了`
5.3.3 验证子线程尚未调度
在子线程尚未调度时,可以提前终止线程。
修改5.3.1中的例子,去掉主线程的sleep代码:
// Thread.sleep(5000); // 保证子线程先被调度运行
一般情况下,子线程都会先启动,执行若干次,偶尔可以复现子线程未启动:
true //仅打印true,之前`没有子线程日志,表明子线程未启动`
参考:java Future 接口介绍 本篇Future 接口介绍部分源自本文
《并发编程之FutureTask详解》 例子及源码分析来源