• Java 并发:Future FutureTask


    Future

    当向一个ExecutorService提交任务后可以获得一个Future对象,在该对象上可以调用getcancel等命令来获取任务运行值或者是取消任务。下面是一个简单的计数任务:

    public class NormalFuture {
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
    
            Future<Integer> count = executorService.submit(new Counting(2));
    
            System.out.println("total:" + count.get());
        }
    }
    
    
    class Counting implements Callable<Integer> {
        private final long period;
    
        public Counting(long period) {
            this.period = TimeUnit.SECONDS.toNanos(period);
        }
    
        public Integer call() throws Exception {
            long deadline = System.nanoTime() + period;
            int count = 0;
    
            for (;;) {
    
                if (deadline <= System.nanoTime()) {
                    break;
                }
    
                System.out.println(++count);
    
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return count;
        }
    }
    

    Future 的用途

    future主要用于与之关联的任务的状态、结果并可取消该任务。

    异步任务

    future用在需要异步执行的任务上,即不需要立即获取结果或者说无法立即获得结果,但在这段时间内主线程还可以做一些其他的工作,等到这些工作做完确实到了没有任务结果不能进行下去的地步时,可以用get调用来获取任务结果。

    同步任务

    一般来说任何异步执行的框架总是可以转换成同步执行的,只要一直等结果就行了。当向ExecutorService提交任务后,就可以立马调用Future对象的get方法,就跟直接在当前线程内调用方法然后等待一样。如果没有Future机制那么任务提交到线程池后我们就无法知晓任务的状态。当然如果没有JDK自带的Future机制,我们可以在提交的Runnable或者Callable对象内实现相应的一些线程通知同步等方法(如消息队列,信号量)。不过既然JDK已经提供了如Future, ExecutorCompletionService这些机制,再这么做就有些重复造轮的感觉了。

    任务取消

    任务并不是直接将线程池内运行该任务的线程停止(Thread类里相关的方法早已弃用)。而是取消其他线程在该任务对应的Future.get上的等待,也可以选择对运行任务的线程设置interrupt,如果任务线程在可中断的操作上那么线程池内执行该任务的线程会马上退出。但如果该任务是一个计算密集型的任务(没有包含任何可中断的方法调用)那么该线程还会继续执行,不过所有在与该线程关联的Future对象上等待的线程已经被激活并收到任务已取消的异常。

    mayInterruptIfRunning = false

    取消future对应的任务时可以使用mayInterruptIfRunning = false,那么这个取消过程只会使等待在上面的线程收到CancellationException,实际运行任务的线程还是会继续进行。如下面的例子所示:

    public class NormalFuture {
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
    
            Future<Integer> count = executorService.submit(new Counting(5));
    
            TimeUnit.MILLISECONDS.sleep(500);
            count.cancel(false); // console counting output will continue with argument mayInterruptIfRunning = false
    
            System.out.println("total:" + count.get());
    
        }
    }
    

    Counting任务中的输出在cancel执行后依然会继续,但主线程在count变量上get的时候确实收到了异常:

    1
    2
    3
    4
    5
    Exception in thread "main" java.util.concurrent.CancellationException
    	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
    	at java.util.concurrent.FutureTask.get(FutureTask.java:188)
    	at futures.NormalFuture.main(NormalFuture.java:18)
    6
    7
    8
    9
    10
    ...
    

    mayInterruptIfRunning = true

    当我们在取消任务时使用了mayInterruptIfRunning = true,那么将会向执行任务的线程进行一个interrupt操作,如果里面的任务能够响应这个请求(可中断的方法调用如sleep),线程一般来说会退出,任务会结束。下面把取消任务的参数改为true

           count.cancel(true);
    

    再运行程序,可以看到Counting任务在主线程执行cancel调用后马上就停止了(没有继续输出数字)

    1
    2
    3
    4
    5
    Exception in thread "main" java.util.concurrent.CancellationException
    	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
    	at java.util.concurrent.FutureTask.get(FutureTask.java:188)
    	at futures.NormalFuture.main(NormalFuture.java:18)
    

    不可中断任务

    什么是不可中断任务,就是其中没有方法对线程的interrupt状态进行检测并在interrupt置位时能够主动抛出异常或者退出。下面几个就是不可中断任务:

        for (int i=0; i<10000000; i++) {
            System.out.println(i);
       }
    

    上面的纯粹的在进行计算和输出(println是不可中断调用)

    再比如

        for (int i=0; i<1000000; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Throwable ignore) {
            } 
        }
    

    虽然sleep调用是可中断方法,但是外层直接捕获了异常并忽略,这样不会使线程退出,也就变得不可中断了。

    class Counting implements Callable<Integer> {
        private final long period;
    
        public Counting(long period) {
            this.period = TimeUnit.SECONDS.toNanos(period);
        }
    
        public Integer call() throws Exception {
            long deadline = System.nanoTime() + period;
            int count = 0;
    
            for (;;) {
    
                if (deadline <= System.nanoTime()) {
                    break;
                }
    
                System.out.println(++count);
    
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("known interrupted, but continue.");
                }
            }
            return count;
        }
    }
    

    上面就把Counting任务改为了一个不可中断的任务,此时即使是用Future.cancel(true)去取消,任务还是会照样执行,运行输出如下:

    1
    2
    3
    4
    5
    Exception in thread "main" java.util.concurrent.CancellationException
    	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
    	at java.util.concurrent.FutureTask.get(FutureTask.java:188)
    	at futures.NormalFuture.main(NormalFuture.java:18)
    known interrupted, but continue.
    6
    7
    8
    9
    10
    

    FutureTask

    Future只是一个接口,具体使用到得实现类为FutureTask它不但实现了Future接口也实现了Runnable接口,这两者的关系非常紧密。简单的想象一下,比如在执行包裹的runnable对象的run方法后修改Future对象中的状态,通知等待在Future.get上的线程。

    状态

    FutureTask有几个状态

        /**
         * The run state of this task, initially NEW.  The run state
         * transitions to a terminal state only in methods set,
         * setException, and cancel.  During completion, state may take on
         * transient values of COMPLETING (while outcome is being set) or
         * INTERRUPTING (only while interrupting the runner to satisfy a
         * cancel(true)). Transitions from these intermediate to final
         * states use cheaper ordered/lazy writes because values are unique
         * and cannot be further modified.
         *
         * Possible state transitions:
         * NEW -> COMPLETING -> NORMAL
         * NEW -> COMPLETING -> EXCEPTIONAL
         * NEW -> CANCELLED
         * NEW -> INTERRUPTING -> INTERRUPTED
         */
        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;
    
    • NEW表示刚提交任务到任务计算结束,要设置结果值前的这段时间的状态。也就是说任务运行时其状态也是NEW
      所以可以看到对Future接口中isDone的实现如下:
     public boolean isDone() {
            return state != NEW;
        }
    

    即凡是不处与NEW的状态都认为任务已经执行完成(当然可能是取消了,或者是抛异常了),这里的完成只是说任务(提交的Runnable或者Callable中的任务函数)运行已经停止了。

    • COMPLETING 表示正在设置任务返回值或者异常值(catch语句中捕获的)

    状态转换路径

    NEW -> COMPLETING -> NORMAL

    正常情况下的转换路径

        protected void set(V v) {
            if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
                outcome = v;
                UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
                finishCompletion();
            }
        }
    
    NEW -> COMPLETING -> EXCEPTIONAL

    任务抛出异常情况下的转换路径

        protected void setException(Throwable t) {
            if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
                outcome = t;
                UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
                finishCompletion();
            }
        }
    
    NEW -> CANCELLED

    任务取消时的转换路径之一,这里对应Future.cancel(bool)的参数是false时的情况

    NEW -> INTERRUPTING -> INTERRUPTED

    任务取消时的转换路径之二,这里对应Future.cancel(bool)的参数是true时的情况,即会尝试着向线程池中的执行线程发出interrupt请求,这里不管最终目标线程有没有忽略这个interrupt请求,Future中的状态都会变为INTERRUPTED

    执行函数

    可以看到在传入的Callable周围包裹了进行状态检测和转换的逻辑,也可以看到对任务异常的处理。

        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 {
    ....
            }
        }
    

    Future.get等待队列

    当有多个其他线程同时调用Future.get并等待,在任务完成后需要将他们一一唤醒。要实现这个功能可以使用传统的wait&notifyAll或者基于AQS如信号量等同步机制,不过这里使用CAS操作实现了一个无锁队列,确切的说是一个栈。根据栈的特点,它是FILO的,所以最早调用Future.get的线程反而会最晚被唤醒。唤醒与睡眠使用了LockSupport的park系列函数。下面是唤醒的一个实现:

        /**
         * Removes and signals all waiting threads, invokes done(), and
         * nulls out callable.
         */
        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
        }
    

    Future.get等待过程

    上述的get等待队列唤醒过程是get上的最后一步。这个完整过程如下,即

    • 当前get等待的线程是否被interrupt请求,如果是,则放弃等待并抛出InterruptedException,可见get方法是一个可中断方法
    • 如果任务已经完成执行则返回任务状态停止循环检查
    • 当前线程对应的等待节点是否已经创建,没有的话进行创建
    • 当前线程对应的等待节点是否已经入队,如果没有则加入Future的等待队列(无锁stack)中
    • 如果以上都不满足则投入睡眠等待唤醒,对于带超时参数的get版本要判断当前是否已经超时
        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) // cannot time out yet
                    Thread.yield();
                else if (q == null)
                    q = new WaitNode();
                else if (!queued)
                    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
                    LockSupport.park(this);
            }
        }
    

    最后get函数调用report函数进行结果返回或者抛出异常

        private V report(int s) throws ExecutionException {
            Object x = outcome;
            if (s == NORMAL)
                return (V)x;
            if (s >= CANCELLED)
                throw new CancellationException();
            throw new ExecutionException((Throwable)x);
        }
    
        public V get() throws InterruptedException, ExecutionException {
            int s = state;
            if (s <= COMPLETING)
                s = awaitDone(false, 0L);
            return report(s);
        }
    

    钩子函数

    在FutureTask中有个空函数protected void done(),子类可以覆盖它以便当任务完成时可以被调用,ExecutorCompletionService类就用到了这个。它有个QueueingFuture继承了FutureTask,QueueingFuture的done函数就是把当前QueueingFuture包裹的内部FutureTask加入到完成队列中,这个有点绕。见代码:

        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得到的Future,但在这个Future对象上并不能得到任务结果(super(task, null)),只能等待任务完成。要获取得到任务结果必须从completionQueue队列中获取future对象并在其上调用get函数才行。

  • 相关阅读:
    我国大部分地区今晚将上演红色月全食美景[2007828]
    n阶汉诺塔问题(Hanoi)
    NBear中使用WhereClip表示in查询的问题
    web性能测试分析理论篇
    Net类库一览 转
    使用ASP.Net Forms模式实现WebService身份验证
    2005数据源的相关数据绑定
    单位分级,涵闸和人员绑定到树
    ajax,js文件中中文显示成乱码的问题
    WinFom应用程序数据操作技巧
  • 原文地址:https://www.cnblogs.com/lailailai/p/4648599.html
Copyright © 2020-2023  润新知