Java并发系列(10)——FutureTask 和 CompletionService

接上一篇《Java并发系列(9)——并发工具类

8 FutureTask 与 CompletionService

把 FutureTask 和 CompletionService 放在一起,并不是因为它们之间有什么特别的联系,虽然确实有一点联系。

主要是因为下一章要讲线程池,如果不把 FutureTask 和 CompletionService 搞清楚,线程池的部分代码看得会比较困惑。

8.1 FutureTask

FutureTask 的实现比较简单,但它是一个非常基础的东西。只要涉及到有返回值的异步调用,或直接或间接一般都会用到它。

用法很简单,这里给个示例:

package per.lvjc.concurrent.futuretask;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;

public class FutureTaskDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        RunnableFuture<String> future = new FutureTask<>(() -> {
            System.out.println("run by thread: " + Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(2);
            return "success";
        });
        new Thread(future).start();
        long start = System.currentTimeMillis();
        String result = future.get();
        long end = System.currentTimeMillis();
        System.out.println("get result:" + result + ", cost:" + (end - start) + "ms");
    }
}
8.1.1 类图

在这里插入图片描述

8.1.2 几个问题

FutureTask 实现了 Future 接口的 5 个方法,以及 Runnable 接口的 run 方法。

其实从 FutureTask 的用法很容易猜出,它内部持有一个 Runnable 或 Callable,调用它的 run 方法时会被代理到 Runnable 或 Callable 的 run 方法去。

但还有几个细节问题:

  • 怎样 cancel 一个任务?
    • 如果任务尚未执行,怎么办?
    • 如果任务正在执行,怎么办?
    • 如果任务已经执行完了,怎么办?
  • 怎样算是 isDone?
    • 如果任务尚未执行,算不算?
    • 如果任务正在执行,算不算?
    • 如果任务执行成功,算不算?
    • 如果任务执行抛出未捕获异常,算不算?
    • 如果任务尚未执行、正在执行、执行完成被 cancel 了,这几种情况分别算不算?
8.1.3 实现细节
8.1.3.1 属性

共 5 个:

  • state:int 类型,从 0 ~ 6 记录了 FutureTask 的 7 种状态;
  • callable:Callable 类型,待执行的任务,Runnable 会被包装成 Callable;
  • outcome:Object 类型,记录执行结果,即 Callable 的返回值,或者也可能是个 Throwable;
  • runner:Thread 类型,记录正在执行的线程;
  • waiters:WaitNode 类型,记录正在阻塞等待结果的线程,保存的是链表的第一个节点。
8.1.3.2 run 方法

主干逻辑:

    public void run() {
        //------1.判断状态------
        //1.1.状态不为 NEW,直接 return
        if (state != NEW ||
            //1.2.状态为 NEW,再尝试将 runner 由 null 改为当前线程
            //如果改成功了,往下走执行 Callable 的 run 方法
            //如果改失败了,说明被其它线程给改了,return,让改成功的线程去执行
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            //------2.执行 run 方法------
            Callable<V> c = callable;
            //2.1.再次判断状态,有可能被 cancel
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //2.2.执行 run 方法
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    //2.3.执行异常
                    setException(ex);
                }
                if (ran)
                    //2.4.执行成功
                    set(result);
            }
        } finally {
            //------3.收尾工作------
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            //3.1.把 runner 再改回 null
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            //3.2.处理可能被遗漏的 interrupt
            //因为其它线程在 cancel 的时候是先改状态,再执行 interrupt,
            //这就会存在一个线程调度问题,
            //状态改成了 INTERRUPTING,随后 cpu 时间片到了,线程被挂起,
            //以致于本该到来的 interrupt 被延后,直到任务已经执行完了,interrupt 还没来
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

run 方法异常退出分支,走 setException 方法:

    //处理 run 方法异常退出
    protected void setException(Throwable t) {
        //把状态从 NEW 改成 COMPLETING
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            //把 run 方法抛出的异常赋给 outcome
            outcome = t;
            //把状态改成 EXCEPTIONAL
            //到这里就不需要 cas 了,因为 COMPLETING 是一种安全的状态
            //其它线程如果看到状态是 COMPLETING 就不会动了
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            //最后的结束动作
            finishCompletion();
        }
        //如果更改状态失败,直接退出
    }

run 方法成功执行分支,走 set 方法:

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            //把 Callable 的返回值赋给 outcome
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

处理可能遗漏的 interrupt:

    private void handlePossibleCancellationInterrupt(int s) {
        // It is possible for our interrupter to stall before getting a
        // chance to interrupt us.  Let's spin-wait patiently.
        if (s == INTERRUPTING)
            while (state == INTERRUPTING)
                Thread.yield(); // wait out pending interrupt

        // assert state == INTERRUPTED;

        // We want to clear any interrupt we may have received from
        // cancel(true).  However, it is permissible to use interrupts
        // as an independent mechanism for a task to communicate with
        // its caller, and there is no way to clear only the
        // cancellation interrupt.
        //
        // Thread.interrupted();
    }

如果状态是 INTERRUPTING,说明调用 cancel 方法的线程还没有结束,在这里要等它跑完。

这里是一个 while 循环,直到状态被改掉为止,通过 yield 试图把 cpu 让给其它线程,也是希望 cancel 线程能早点拿到 cpu 资源早点跑完。

最后,不管是 setException 还是 set 方法在赋值给 outcome 之后都会调用:

    private void finishCompletion() {
        // assert state > COMPLETING;
        //------1.唤醒因为 get 方法在阻塞的所有线程------
        //1.1.外面一层循环是给 cas 失败自旋用的
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                //1.2.里面一层循环,是遍历链表
                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;
            }
        }
        //------2.调用 hook 方法,默认是空实现------
        done();

        callable = null;        // to reduce footprint
    }

run 方法总结:

  • 主要做三件事:
    • 执行任务;
    • 保存任务执行结果;
    • 唤醒阻塞的线程来拿结果;
  • callable 属性:主要就是执行 callable 对象的 run 方法,执行完置为 null,因此只会执行一次;
  • runner 属性:控制 Callable#run 方法的逻辑不会被多个线程并发执行;
  • outcome 属性:
    • Callable#run 方法抛出异常,outcome 属性保存这个 Throwable;
    • Callable#run 方法正常退出,outcome 属性保存其返回值;
  • waiters 属性:Callable#run 方法执行完毕,不论成功还是异常,outcome 属性赋值后,遍历链表唤醒所有阻塞的线程;
  • state 属性:
    • 直到 outcome 赋值之前,一直都是 NEW;
    • 成功执行,状态变更:NEW -> COMPLETING -> NORMAL;
    • 异常执行,状态变更:NEW -> COMPLETING -> EXCEPTIONAL。
8.1.3.3 get 方法

主干逻辑:

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //小于等于 COMPLETING 意思是还没执行完成
        if (s <= COMPLETING)
            //阻塞等待结果
            s = awaitDone(false, 0L);
        //执行完成,获取结果
        return report(s);
    }

阻塞逻辑:

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        //死循环自旋
        for (;;) {
            //1.如果从 get 阻塞中被打断
            if (Thread.interrupted()) {
                //把当前节点从链表删除
                removeWaiter(q);
                //抛出打断异常,退出阻塞
                throw new InterruptedException();
            }

            int s = state;
            //2.状态值大于 COMPLETING,执行已经有结果了,退出阻塞
            if (s > COMPLETING) {
                if (q != null)
                    //这里仅仅把 thread 置空,而没有删除当前节点,
                    //上面 removeWaiter 方法里面遍历链表时遇到 thread == null 的节点会将其删除
                    q.thread = null;
                return s;
            }
            //3.状态已经是 COMPLETING,让出 cpu 资源,进入下一次循环
            //这里不用创建节点加入链表阻塞,
            //因为 COMPLETING 状态表示任务已经执行完,只是还没有保存结果,
            //这之间只有几条指令的时间,直接 cpu 空跑即可,
            //再创建节点、阻塞、被唤醒,就浪费时间了
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //4.走到这里,说明状态还是 NEW,创建节点,进入下一次循环
            else if (q == null)
                q = new WaitNode();
            //5.节点已经创建过了,将其加入链表,进入下一次循环
            else if (!queued)
                //q.next = waiters 而不是 waiters.next = q,
                //显然是把新节点插在了头部
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //6.节点已经入队,如果有超时时间设置,检查是否超时
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    //超时,不管有没有结果,删除当前节点,退出阻塞
                    removeWaiter(q);
                    return state;
                }
                //没有超时,阻塞当前线程
                LockSupport.parkNanos(this, nanos);
            }
            //7.节点已经入队,没有超时设置,阻塞等待唤醒
            else
                LockSupport.park(this);
        }
    }

退出阻塞之后,获取执行结果的逻辑:

    private V report(int s) throws ExecutionException {
        Object x = outcome;
        //1.正常退出,返回执行结果
        if (s == NORMAL)
            return (V)x;
        //2.任务被取消,抛异常
        if (s >= CANCELLED)
            throw new CancellationException();
        //3.没有正常结束,也没有被取消,那就是任务执行本身异常,包装异常抛出
        throw new ExecutionException((Throwable)x);
    }

get 方法总结:

  • 用链表保存了所有在 get 方法阻塞的线程;
  • 链表采用头插法,后阻塞的线程排在前面,不算超时自己醒来的的线程,会先被唤醒;
  • get 会得到三种结果:
    • 任务正常完成:得到任务返回值;
    • 任务异常退出:抛出 ExecutionException,里面包装了任务抛出的异常;
    • 任务被取消:抛出 CancellationException。
8.1.3.4 cancel 方法

主干逻辑:

    //取消任务,成功取消返回 true,否则返回 false
    public boolean cancel(boolean mayInterruptIfRunning) {
        //------1.判断状态------
        //1.1.如果状态不为 NEW(由 run 方法可知意为执行已经出结果了),直接 return false
        if (!(state == NEW &&
              //1.2.如果状态为 NEW,尝试将状态改为 INTERRUPTING 或 CANCELLED,
              //如果更改状态失败,意味着状态已经不为 NEW 了,返回 false
              //如果更改状态成功,意味着成功取消任务,继续往下走,最终肯定会返回 true
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            //------2.打断线程------
            //2.1.根据入参,如果需要将 run 方法线程打断,就执行打断操作
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    //NEW 状态的第一种情况:任务已经执行,将其打断
                    if (t != null)
                        t.interrupt();
                    //NEW 状态的第二种情况:任务尚未执行,没有线程可打断
                } finally { // final state
                    //任务是被打断取消的,以 INTERRUPTED 状态结束
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
            //2.2.如果不需要打断 run 方法线程,什么都不做
        } finally {
            //------3.收尾工作:唤醒阻塞线程------
            finishCompletion();
        }
        return true;
    }

cancel 方法总结:

  • cancel 会失败的情况:run 线程任务执行已经出了结果,不论是正常执行结束或者抛出异常;
  • cancel 会成功的情况,又分两种不同的处理逻辑:
    • 需要打断:如果任务已经执行,将其打断,最终以 INTERRUPTED 状态结束;
    • 不需要打断:如果任务已经执行,放任其不管,最终以 CANCELLED 状态结束;
    • 对于尚未执行的任务,以上两种情况,仅最终状态不同,没有其它区别;
  • 对于唤醒阻塞线程:
    • cancel 成功:需要唤醒;
    • cancel 失败:不需要唤醒;
  • cancel 方法的影响:
    • 如果返回 false,即 cancel 失败:cancel 方法什么都没干,没有任何影响;
    • 如果返回 true,即 cancel 成功:cancel(true) 会打断 run 线程,cancel(false) 不会,然后它们都会唤醒阻塞线程,被唤醒的线程 get 执行结果会抛出一个 CancellationException。
8.1.3.5 七种状态汇总

FutureTask 的 7 种状态:

  • NEW(0):尚未执行,或正在执行;
  • COMPLETING(1):执行完成,还没有保存结果;
  • NORMAL(2):正常完成,已保存了任务执行的返回值;
  • EXCEPTIONAL(3):异常退出,已保存了任务抛出的异常;
  • CANCELLED(4):任务被取消,如果取消的时候,任务还没执行,不会再执行;如果正在执行,让其继续跑下去;
  • INTERRUPTING(5):任务正在被取消,如果任务还没执行,不会再执行;如果正在执行,紧接着将其打断(任务是否响应这个打断是另外一回事);
  • INTERRUPTED(6):任务已被取消,如果取消的时候,任务还没执行,不会再执行;如果正在执行,那么此时任务已经被打断过了,任务有没有理会这个打断,FutureTask 无法得知。

状态变更的四种情况:

  • 正常执行:NEW -> COMPLETING -> NORMAL;
  • 执行异常:NEW -> COMPLETING -> EXCEPTIONAL;
  • cancel(true) 成功:NEW -> INTERRUPTING -> INTERRUPTED;
  • cancel(false) 成功:NEW -> CANCELLED;
  • cancel(true/false) 失败:对任务执行没有任何影响,即前两种情况。

状态分类:

  • 从是否安全的维度:
    • 非安全状态:NEW,所有线程都可以把 NEW 状态改为其它状态,要考虑并发安全问题;
    • 安全状态:其它所有状态,一个线程看到状态不为 NEW,要么等待状态再次变更,要么直接返回,不能做任何事,所以不存在并发安全问题;
  • 从阶段的维度:
    • 初始态:NEW;
    • 中间态:COMPLETING,INTERRUPTING;
    • 最终态:NORMAL,EXCEPTIONAL,INTERRUPTED,CANCELLED。
8.1.3.6 isCancelled 方法
    public boolean isCancelled() {
        //包括:CANCELLED,INTERRUPTING,INTERRUPTED
        //第一个是 cancel(false) 导致的
        //后两个是 cancel(true) 导致的
        return state >= CANCELLED;
    }
8.1.3.7 isDone 方法
    public boolean isDone() {
        //包括除 NEW 以外的 6 种状态
        //因为 NEW 代表尚未执行和正在执行
        //所以只要不是 NEW,都算执行完了
        //所以不要误认为 isDone 就是任务正常执行,还包括异常退出和任务取消
        return state != NEW;
    }

8.2 CompletionService

后面要讲的 ExecutorService 的实现会用到 CompletionService,但 CompletionService 实际上也会用到 Executor,所以涉及到 Executor 的部分留到后面再讲。

8.2.1 接口方法

也不复杂,共 5 个:

  • submit(Callable):Future,非阻塞方法,提交一个 Callable 任务,返回 Future,用于获取结果;
  • submit(Runnable, V):Future,非阻塞方法,提交一个 Runnable 任务,返回 Future,主要用于感知任务是否执行完成,因为任务返回的值是已知的,就是自己在参数里传入的;
  • take():Future,阻塞方法,等待直到接下来第一个任务执行完成,取走其 Future;
  • poll():Future,非阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的立即返回 null;
  • poll(long,TimeUnit):Future,阻塞方法,取走目前第一个执行完成的任务的 Future,如果没有已经执行完成的,可以等待指定的时间。

它的核心功能主要在后面三个方法,即通过 submit 提交一批任务,然后 take 或者 poll 拿到最先完成的任务。

这样可以避免,前面执行的任务耗时很长,导致后面执行的任务已经完成了也拿不到结果。

8.2.2 demo
package per.lvjc.concurrent.futuretask;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class CompletionServiceDemo {

    private static Executor executor = Executors.newFixedThreadPool(10);
    private static ExecutorCompletionService<Integer> ecs = new ExecutorCompletionService<>(executor);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //new 出 10 个 Callable
        List<Callable<Integer>> tasks = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            tasks.add(() -> {
                TimeUnit.SECONDS.sleep(10 - finalI);
                return finalI;
            });
        }

        //不用 CompletionService,统计耗时
        getAndProcessResultByFutureGet(tasks);
        //用 CompletionService,统计耗时
        //getAndProcessResultByCompletionService(tasks);
    }

    private static void getAndProcessResultByFutureGet(List<Callable<Integer>> tasks) throws ExecutionException, InterruptedException {
        //提交所有任务
        List<FutureTask<Integer>> futureTasks = new ArrayList<>();
        for (Callable<Integer> task : tasks) {
            FutureTask<Integer> futureTask = new FutureTask<>(task);
            futureTasks.add(futureTask);
            executor.execute(futureTask);
        }
        
        //统计等待所有任务执行完成,并处理完所有结果的耗时
        long start = System.currentTimeMillis();
        for (FutureTask<Integer> futureTask : futureTasks) {
            int result = futureTask.get();
            processResult(result);
        }
        long end = System.currentTimeMillis();
        System.out.println("all result processed, cost " + (end - start) + " ms");
    }

    private static void getAndProcessResultByCompletionService(List<Callable<Integer>> tasks) throws InterruptedException, ExecutionException {
        //提交所有任务
        for (Callable<Integer> task : tasks) {
            ecs.submit(task);
        }
        
        //统计等待所有任务执行完成,并处理完所有结果的耗时
        long start = System.currentTimeMillis();
        int taskSize = tasks.size();
        int count = 0;
        while (true) {
            Future<Integer> future = ecs.take();
            processResult(future.get());
            count++;
            if (count == 10) {
                break;
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("all result processed by ecs, cost " + (end - start) + " ms");
    }

    private static void processResult(int result) throws InterruptedException {
        //处理每个任务的结果耗时 1 秒
        TimeUnit.SECONDS.sleep(1);
        System.out.println("processed result:" + result);
    }
}

不用 CompletionService,执行结果如下,耗时 20 秒:

processed result:0
processed result:1
processed result:2
processed result:3
processed result:4
processed result:5
processed result:6
processed result:7
processed result:8
processed result:9
all result processed, cost 20029 ms

用 CompletionService,执行结果如下,耗时 11 秒:

processed result:9
processed result:8
processed result:7
processed result:6
processed result:5
processed result:4
processed result:3
processed result:2
processed result:1
processed result:0
all result processed by ecs, cost 11035 ms

出现这样的结果是因为,这 10 个任务,前面的耗时较长,后面的耗时较短。

虽然都是提交给线程池执行,但是不用 CompletionService 的情况,傻傻地等第一个耗时最长的任务,后面的任务已经执行完成了也不先拿出来去处理,导致总耗时:等待任务结果 10 秒 + 处理结果 10 秒 = 20 秒。

而使用了 CompletionService,耗时最短的任务结果出来了,先拿去处理,这样总耗时:等待任务结果 10 秒 + 处理结果 1 秒 = 11 秒。

8.2.3 实现

实现其实很简单,看到 take,poll 这些方法,显然是一个 BlockingQueue。

在 ExecutorCompletionService 类里面:

    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

主要就这个 QueueingFuture,重写了 FutureTask 的 done 方法,任务执行完成后,把执行结果放到队列里面。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值