FutureTask的应用
让我们先写一个demo
class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(2);
return 1;
}
}
public class test {
private int x;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(new Task());
System.out.println(future.get());
}
}
我们知道,实际上线程创建只有两种方式Runnable和Callable,而使用Callable,在线程池中,需要使用submit()才有返回值,所以我们进入submit()
不难看出,submit()做了一件事,它内部调用了一个叫newTaskFor的方法,这个方法会调用FutureTask的构造方法,所以我们可以得出结论
在线程池中,无论是出于异步的目的,还是单纯想要有返回值,都避不开FutureTask
也许有人会觉得,我可以直接execute来跳过submit(),然而在ThreadPoolExecutor类中我们可以看到,只有一个方法public void execute(Runnable command) ;
也不存在它的重载方法,也就是说不存在返回值。
所以唯一的方法就是使用FutureTask,这个类实现了RunnableFuture接口,RunnableFuture接口同时继承了Runnable和Future两个接口,可以满足execute(Runnable command)的调用
构造方法
增加任务,可以选择Callable或Runnable,需要注意的是,Runnable也可以有返回值,因为它调用了Executor当中的RunnableAdapter,可以将Runnable适配成Callable
FutureTask有着自己的结果存储变量outcome
,任务的返回值就是这个值,往往通过get()来获得它(如第一个demo所示)
主要参数
WaitNode 保存自己当前线程和下一个WaitNode的指针,这是个用来装相同任务线程的无锁并发栈,也就是说,如果未来有线程拥有同样任务,那么加入的方式是头插法,无锁并发是通过CAS来保证一致性的。
NEW 正在运行或将要运行
COMPLETING 完结前的操作
NORMAL 正常完结
EXCEPTIONAL 异常完结
CANCELLED 取消
INTERRUTING 正在进行中断操作
INTERRUTED 任务已经被标记中断
状态转变仅存在三个方法,set(),setException(),cancel()
在COMPLETING状态和INTERRUTING状态是懒写入的,因为值唯一并且在后来无法更改
基本操作
放入结果
作用:在成功完成计算的基础上run()内部会调用set()来设置值,将任务结果输入到指定outcome,除非已有相同的任务或已被取消。
状态变化:NEW->COMPLETING->NORMAL
逐个节点thread置空,唤醒等待线程,然后执行FutureTask的done(),然后将任务置空
这个方法在正常结束(NORMAL)和被取消(CANCELLED)时会被调用
但并没有具体实现,子类继承该方法用于在任务完成时回调或者记录总数(这是官方推荐的)。可以在方法的实现中查询状态来确定任务是否被取消了。
get
该方法如果要在NEW和COMPLETING调用需要满足
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// The code below is very delicate, to achieve these goals:
// - call nanoTime exactly once for each call to park
// - if nanos <= 0L, return promptly without allocation or nanoTime
// - if nanos == Long.MIN_VALUE, don't underflow
// - if nanos == Long.MAX_VALUE, and nanoTime is non-monotonic
// and we suffer a spurious wakeup, we will do no worse than
// to park-spin for a while
long startTime = 0L; // Special value 0L means not yet parked
WaitNode q = null;
boolean queued = false;
for (;;) {
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING)
// We may have already promised (via isDone) that we are done
// so never return empty-handed or throw InterruptedException
Thread.yield();
else if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
else if (q == null) {
if (timed && nanos <= 0L)
return s;
q = new WaitNode();
}
else if (!queued)
queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
else if (timed) {
final long parkNanos;
if (startTime == 0L) { // first time
startTime = System.nanoTime();
if (startTime == 0L)
startTime = 1L;
parkNanos = nanos;
} else {
long elapsed = System.nanoTime() - startTime;
if (elapsed >= nanos) {
removeWaiter(q);
return state;
}
parkNanos = nanos - elapsed;
}
// nanoTime may be slow; recheck before parking
if (state < COMPLETING)
LockSupport.parkNanos(this, parkNanos);
}
else
LockSupport.park(this);
}
}
如果是在COMPLETING状态,会直接释放CPU给其它线程
否则
第一次会进入else if (q == null) ,并新建waitNode节点
第二次会进入else if (!queued),通过CAS加入队列,weakCompareAndSet和compareAndSet的区别在于,前者是volatile非增强语义,后者是volatile增强语义
完成了初始化,后面将会一直进入 else if (timed),如果超时,就会删除节点并返回状态,而期间会有parkNanos记录剩余到达删除时间的纳秒数,这是为了后期挂起的线程还有执行时间而不是挂起
也就是说,<=completing一定会让出CPU,completing会处于就绪态而在此之前的状态会处于阻塞态
而走到这里如果依然处于NEW状态,就会进入WAITING,等待唤醒
如果没有许可,并且设置时间有效,那么就会挂起线程
唤醒条件:
1,被unpark
2,被中断,并且唤醒后不会报异常
3,时间到
最后的report()
用于返回结果,也可能抛出告知取消,为什么中断也会抛出取消呢,首先我们要明白一个前置条件,那就是只有get()和定时get()两个重载方法会调用report(),所以如果是一个新任务,那么它必定走到get()中的park()
而park()之后被唤醒情况有两种,第一种,set()内部调用finishCompletion()后把所有等待这个任务的线程释放,第二种,任务被取消了,运行线程会被中断,等待线程也会被内部调用的finishCompletion()给唤醒
第一种情况就是NORMAL,第二种情况就是CANCELLED
以下是第二种情况的具体流程
压轴-执行方法!
执行方法有两个-run()和runAndReset(),这两者的区别是
1 runAndReset()会反复执行,除非取消或遇到了异常,适用于程序内部循环调用。
2 runAndReset()不会保存结果,也就是outcome
不会被赋值
让我们分析一下这个方法
首先CAS保证了这个任务只被启动了一次,而handlePossibleCancellationInterrupt()用于协助取消任务,这一步是为了防止漏掉中断,如果有其他线程正在执行cancel()(cancel源码在get()分析最后面),他可能刚标记为INTERRUPTING还没来得及执行interrupt()然后run()就跑完了。
让我们看看另一个-run()
如果执行过且无异常就会将结果记入,记入函数set()在之前已经分析过了