硬核FutureTask解析

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()在之前已经分析过了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值