【Java并发编程实战】 5.5.2 FutureTask

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方法。

awaitDonefutureTask实现阻塞的关键方法,我们重点关注一下它的实现原理。

/**
 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秒
已经运行了5true            //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秒
已经运行了4true             //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详解》 例子及源码分析来源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值