Future、FutureTask获取线程结果、执行超时最详细解析

本文目的:了解

1. 如何获取线程返回结果
2. 如何判定线程执行超时
3. 线程超时如何取消执行
4. 如何监视线程执行


在这里插入图片描述

一、java创建线程的三种方式

①继承Thread类创建线程类
②通过Runable接口创建线程类
③通过Callable接口创建线程

下面通过例子看看这三种创建方式

1、继承Thread类(run 方法的返回类型是void)

public class MyThread extends Thread {
	@Override
	public void run() {
		 .....
	}
}

2、实现runnable接口(run 方法的返回类型是void)

public class RunnableTest implements Runnable {
	@Override
	public void run() {
		.....
	}
}

3、实现Callable接口(call 方法的返回类型是 具体类型)

public class CallableTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        ......
        return 9527;
    }
}

区别:

1、继承和实现接口创建,这个不是本文讨论重点,细节不谈
2、Callable 有返回线程执行结果(这个结果可以自定义类型和内容),重点,往下

在这里插入图片描述

二、如何获取线程执行结果

1、实现

/**
 * 创建线程的方式:Callable
 * @Author Tan
 */
public class CallableTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 执行。。。");
        TimeUnit.SECONDS.sleep(3L);
        return 9527;
    }

    public static void main(String[] args) {
        /*
         * @Date: 2020-06-09 17:11
         * Step 1: 创建
         */
        CallableTest runnableTest = new CallableTest() ;
        FutureTask<Integer> futureTask = new FutureTask<>(runnableTest);

        /*
         * @Date: 2020-06-09 17:12
         * Step 2: 执行
         */
        new Thread(futureTask,"CallableTest").start();

        /*
         * @Date: 2020-06-09 17:12
         * Step 3: 获取执行结果
         */
        try {
            Integer integer = futureTask.get();
            System.out.println("执行结果:" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行日志
在这里插入图片描述

2、分析

这里引进FutureTask 来获取执行结果,先用FutureTask将线程实例封装,然后用它创建线程并start,最后在 futureTask 中获取线程执行返回的结果
在这里插入图片描述
值得注意的是:通过get方法获取执行结果,该方法会阻塞直到任务返回结果,有点类似 Thread类的 join
在这里插入图片描述

三、探索 Future 的奥秘

上面获取线程结果的例子中, FutureTask是让我们取得线程执行结果的载体,看看这个类如何实现

1、从类说起

在这里插入图片描述
>Runnable:接口,有run方法,实现者重写run即可。很多与线程相关的类都实现该接口重写run执行线程,如最熟悉的Thread
>Future:接口,提供对执行结果的处理方法

A Future represents the result of an asynchronous computation.
Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.
The result can only be retrieved using method get when the computation has completed, blocking if necessary until it is ready.
Cancellation is performed by the cancel method. Additional methods are provided to determine if the task completed normally or was cancelled.
Once a computation has completed, the computation cannot be cancelled.
If you would like to use a Future for the sake of cancellability but not provide a usable result, you can
declare types of the form Future<?> and return null as a result of the underlying task.
– 来自源码 Future 类注释说明

Future 表示异步计算的结果。提供方法来检查计算是否完成,等待其完成,并获取计算结果。
当计算完成时只能用 get 方法获取计算结果,get方法必须阻塞等待计算完成。
取消可以通过执行cancel方法实现。提供了其他方法(isCancelled,isDone)来确定任务是正常完成还是被取消。
一旦计算完成,就不能取消计算。
如果为了可取消性而想使用Future,但不需要提供可用的执行结果,则可以使用声明形式Future<?>来获取任务返回null的结果。
>RunnableFuture:接口,继承上面的Future,和Runnable,意味着结合了这两个的方法
>FutureTask:实现类,实现了RunnableFuture。

A FutureTask can be used to wrap a Callable or Runnable object.
Because FutureTask implements Runnable, a FutureTask can be submitted
to an Executor for execution.
– 来自 源码 FutureTask类注释说明

FutureTask可用于包装Callable或Runnable对象。因为FutureTask实现了Runnable,所以可以将 FutureTask提交给Executor执行。

一句话说明用法,简洁直接,往回看之前的例子就好理解了。

在我们的例子中,也正是因为FutureTask实现了Runnable,所以才可以直接交给Thread去执行。下面从源码的角度看看我们怎么实现。

2、源码解析

(1).线程封装

首先 CallableTest implements Callable 表明 CallableTest 是个 Callable

然后 FutureTask futureTask = new FutureTask<>(runnableTest); 就是 FutureTask包装Callable对象,这里包装了我们自定义的实例 CallableTest
这里看下FutureTask的构造函数

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

实际上就是传递callable,然后把状态变成 NEW(表示新建状态)

接着是 new Thread(futureTask,“CallableTest”).start();,前面说了,FutureTask 实现了 Runnable,所以可以用来创建Thread实例,看Thread的构造函数就知道了

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

到这里就是走线程执行的路线了,Thread.start之后,start0() 将线程的执行交给底层调度,得到调度权之后就执行Runnable.run,也就是我们定义的线程执行内容了

public synchronized void start() {
	......
	start0();
 	......
}

(2).FutureTask 的run方法

在这里插入图片描述

(3).FutureTask 的get方法

在这里插入图片描述
至此,整个封装,运行,结果获取的过程就完成了

等等,还有个重要的知识点!! get方法必须阻塞等待计算完成?

往回翻,发现在get方法里面有个语句 s = awaitDone(false, 0L);

(4).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);
    }
}

无限循环阻塞,简直粗暴!!
在这里插入图片描述

四、进阶之线程超时

FutureTask里面还提供了个方法用于检测线程执行时间

/**
 * @throws CancellationException {@inheritDoc}
 * timeout:多长时间获取结果(从线程执行开始算起)
 * 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)
        // 超时会抛出 TimeoutException 异常,我们通过捕获这个异常就可以知道已经超时
        throw new TimeoutException();
    return report(s);
}

1、实现

实现目标:传入时间参数,在指定时长内没有执行完就取消
用法很简单,就是在之前的基础上小小改动

/**
 * 创建线程的方式:Callable
 * @Author Tan
 */
public class CallableTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 执行。。。");
        TimeUnit.SECONDS.sleep(3L);
        return 9527;
    }

    public static void main(String[] args) {
        /*
         * @Date: 2020-06-09 17:11
         * Step 1: 创建
         */
        CallableTest runnableTest = new CallableTest() ;
        FutureTask<Integer> futureTask = new FutureTask<>(runnableTest);

        /*
         * @Date: 2020-06-09 17:12
         * Step 2: 执行
         */
        new Thread(futureTask,"CallableTest").start();

        /*
         * @Date: 2020-06-09 17:12
         * Step 3: 获取执行结果
         */
        try {
//            Integer integer = futureTask.get(); // 之前的
			//(1)换成这个,表示3秒后获取线程执行结果(如果获取不到,会抛TimeoutException异常)
            Integer integer = futureTask.get(3, TimeUnit.SECONDS);
            System.out.println("执行结果:" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            System.out.println("执行超时"); 	//(2)捕获TimeoutException到异常,表明已超时
            futureTask.cancel(true);		//(3)取消线程执行
            e.printStackTrace();
        }
    }
}

不过这样有个不足之处,没有办法获取超时线程的信息。如果处理超时交给另一个线程处理就更加难以获取,为此,我们重写futureTask。

2、改进

(1).新建MyFutureTask

public class MyFutureTask<V> extends FutureTask<V> {
    private CallableTest runnableTest;
    
    public MyFutureTask(Callable callable) {
        super(callable);
        runnableTest = (CallableTest) callable;
    }
    public CallableTest getRunner() {
        return runnableTest;
    }
}

(2).改进CallableTest

/**
 * 创建线程的方式:Callable
 *
 * @Author Tan
 * @see FutureTask
 */
public class CallableTest implements Callable<Integer> {

	// (1)定义一些需要的信息
    private String runnerName;

    @Override
    public Integer call() throws Exception {
        runnerName = Thread.currentThread().getName();  // (2)赋值
        System.out.println(runnerName + " 执行。。。");
        TimeUnit.SECONDS.sleep(10L);
        return 9527;
    }

    public String getRunnerName() {
        return runnerName;
    }

    public void setRunnerName(String runnerName) {
        this.runnerName = runnerName;
    }

    public static void main(String[] args) {
        /*
         * @Date: 2020-06-09 17:11
         * Step 1: 创建
         */
        CallableTest runnableTest = new CallableTest();
        MyFutureTask<Integer> futureTask = new MyFutureTask<>(runnableTest);

        /*
         * @Date: 2020-06-09 17:12
         * Step 2: 执行
         */
        new Thread(futureTask, "CallableTest").start();

        /*
         * @Date: 2020-06-09 17:12
         * Step 3: 获取执行结果
         */
        try {
            Integer integer = futureTask.get(3, TimeUnit.SECONDS);
            System.out.println("执行结果:" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            System.out.println("执行超时:" + futureTask.getRunner().getRunnerName());// (2)获取futureTask的自定义信息
            futureTask.cancel(true);
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

五、高级应用之线程池

因为FutureTask实现了Runnable,所以可以将 FutureTask提交给Executor执行。

1、官方的例子

官方在类Future注释上给出了使用例子

class App {
	ExecutorService executor = ...
	ArchiveSearcher searcher = ...
	void showSearch(final String target)throws InterruptedException {
		Future<String> future = executor.submit(new Callable<String>() {
			public String call() {
			  return searcher.search(target);
			}});
		displayOtherThings(); // do other things while searching
		try {
			displayText(future.get()); // use future
		} catch (ExecutionException ex) {
			cleanup(); 
			return; 
		}
	}
}}

// FutureTask 类是 Future 的一个实现,间接实现Runnable,因此可以由 Executor 执行。
// 上面的 submit 结构可以替换为:
FutureTask<String> future = new FutureTask<String>(new Callable<String>() {
	public String call() {
	  return searcher.search(target);
}});
executor.execute(future);

上面提供两种方式:

submit之后返回 Future 对象
先定义FutureTask实例,再拿去 execute

其实也好理解,就是submit有返回值,所以直接获取,而executor没有,但是能先定义,再执行。很多人说execute不能处理异常,这是错的,上面第二种就给出了答案,两种方式都可以通过future获取执行异常.

2、场景说明与应用

实际生产过程中我们都是用线程池去做些并发的任务,我们都知道,一旦线程交给线程池去管理,很难从外部去把控每个线程的运行。

为此,我们需要站在线程的上帝角度去管控线程,不妨把负责这个工作的线程叫作监视线程,一般情况,监视线程放在一个线程池里面。

比如,线程池A是执行业务的线程,线程池B是监视线程池A的监视线程池

这样可以解决一些场景:
1、某线程执行结果不符合要求,需要再次计算
2、某线程执行时间过长,需要终止

下面就 超时取消这个场景 写个简单的例子

/**
 * 检查超时线程(在线程池B执行的线程)
 * 超时取消
 * @Author Tan
 */
public class CheckThread extends Thread {

    private Future future;

	/**
	 * 被监视线程结果传进来
	 */
    public CheckThread(Future future) {
        this.future = future;
    }

    public void run() {
        try {
            // 没有超时就直接返回,否则抛 TimeoutException。注意,这里会根据设定时间阻塞
            String result = (String) future.get(5, TimeUnit.SECONDS);
            System.out.println("--执行结果:" + result);
        } catch (InterruptedException e) {
            System.out.print("InterruptedException");
        } catch (ExecutionException e) {
            System.out.print("ExecutionException");
        } catch (TimeoutException e) {
            System.out.println(" ---执行超时");
            future.cancel(true);
        }
    }
}

public class TimeoutExecutor {

	// 执行线程池A
    private static ExecutorService executorServiceA = Executors.newFixedThreadPool(5);

	// 监视线程池B
    private static ExecutorService timeoutExecutorB = Executors.newFixedThreadPool(5);

    private static Integer tim = 20;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i < tim; i++) {
            int finalI = i;
            Future<String> result = executorServiceA.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    try {
                        if (finalI == 5) {
                            Thread.sleep(8000);
                        }
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return Thread.currentThread().getName() + "输出:" + finalI;
                }
            });
            // 将 A 执行结果 交给B 检查(异步检查)
            timeoutExecutorB.execute(new CheckThread(result));
        }
    }
}

在这里插入图片描述
看的出来,第五个线程因为超时被取消了(要求5秒处结果,该线程休眠8秒),取消之后就中断睡眠了

关于 获取线程执行结果。以及Future的剖析 到此结束。

🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌—🐌

👨‍🌾本文结束,希望有帮到阁下。更多问题可以到我博客来搜索

👉【点击搜索

👉【进入博客

如果觉得不错,欢迎关注点赞🙏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cy谭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值