先提交后获取:Future和FutureTesk的异步执行

在上一篇日志中,并行执行下的多线程协作完成查找操作里,我让每一个查找线程实现了Callable接口,从主线程中也看到,使用了Future<Integer>泛型接口来接收线程的返回结果。想了一下,觉得有必要补上一篇日志,总结下自己对Future模式的学习。

使用Runnable还是Callable?

通常我们创建线程都是继承或实现Runnable接口,使用Callable接口和Runnable接口相似,不同之处在于,Runnable接口没有返回值,因为看Runnable接口中,只有一个抽象方法public void run()可以看出,run()方法没有参数传入,返回类型是void,即线程执行完后,不能返回结果。实现Runnable接口的线程如果想要知道执行结果,可能需要通过修改共享变量或者线程间通信的方式告知。而Callable接口是允许有返回值的,先来看看它的源码:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以看到,Callable接口的传入参数类型V,接口里面只有一个方法call(),不过它不是一个抽象方法,而是带返回类型V的方法。也就是说,如果用Callable接口来封装任务,创建线程,则可以获得任务的返回值。

Callable接口使用场景

你可能会想,什么样的场景下需要用到这带返回类型线程?假设一个线程计算任务需要耗费很长时间,它的计算结果我们并不急着需要,在等待该线程计算完成的过程中,我们想充分利用着等待的时间,让CPU去响应其他的线程,等我们需要这个计算结果时,再去拿这个计算任务的返回值。

举个生活中的例子,假如我们想吃云吞做早餐,云吞要放到沸水中煮,在打开电磁炉煮热水,等待水沸腾的过程中,我们可以去把云吞从冰箱里拿出来解冻,此时电磁炉中的水是否沸腾我们不关心,等我们把所有云吞都解冻好了,真正需要放到沸水中煮时,再去打开电磁炉盖,关心此时的水是否已沸腾。

使用Callable接口的线程大概就是这么一个思想,在线程提交执行后,会立刻有返回类型(假设是V),虽然此时线程可能没执行完,之后我们可以通过调用Future.get()方法来获得这个返回值(如果你是把任务返回结果放到Future泛型接口中,你也可以把它放到实现Future接口的类FutureTask类中,同样调用FutureTask.get()方法来获得返回值,下面我会详细说)。

Future接口和FutureTesk类

那么,实现Callable接口的线程,执行结果返回给谁呢?谁来接收实现Callable接口的线程返回值?有两种方式,使用Future泛型接口或使用Future接口的类FutureTesk类。

1、如果使用的是Future<V>泛型接口接收线程的返回值:这个接口里封装了一些方法用来获取,判断或终止实现Callable接口的任务,来看看Future接口的源码:

public interface Future<V> {  
	boolean cancel(boolean mayInterruptIfRunning);  
	boolean isCancelled();  
	boolean isDone();  
	V get() throws InterruptedException, ExecutionException;  
	V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;  
}
cancel()方法用来取消任务的运行,如果任务还没开始执行,则返回fasle,如果任务已经启动执行,则停止后返回true,boolean型参数mayInterruptIfRunning就是用来设置是否中断执行中的线程。

isCancelled()方法用来判断该任务是否已经被取消运行;

isDone()方法用来判断任务是否已执行完毕;
get()方法获取任务的返回对象,但是如果没有可用的返回对象,该方法会阻塞直到任务完成返回对象;

最下面一个get()方法带有参数long timeout,可以设置超时时间,等待timeout时间后获取返回结果,假设线程已经完成,但等待timeout时间还没结束,一样会返回结果;

使用Future<V>泛型接口接收线程的返回值,实现Callable接口的线程通常都是交由线程池来执行并返回,因为在ExecutorService接口中的submit()方法有以下几个重载形式:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

可以看到,实现Callable接口的线程通过线程池的submit()方法交给线程池执行,会返回一个Future<V>泛型接口,然后你就可以通过这个泛型接口调用get()方法获得线程执行的返回值。来看个具体的例子:

package com.justin.futurecallable;

import java.util.concurrent.Callable;

public class DataCreatorDemo implements Callable<String> {
	private String dataString;
	//构造方法初始化
	public DataCreatorDemo(String dataString) {
		this.dataString = dataString;
	}
	
	@Override
	public String call() throws Exception {
		System.out.println("进入DataCreator线程" + Thread.currentThread().getName() + "....");
		long startTime = System.currentTimeMillis();
		StringBuffer BLACKPINK = new StringBuffer("BLACKPINK:");
		BLACKPINK.append(dataString);
		try {
			//线程睡眠模拟数据创建过程较长
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("数据创建完成,耗时: " + (System.currentTimeMillis()-startTime) + "ms");
		
		return BLACKPINK.toString(); //最后返回String类型结果出去
	}
}

我们有一个创建数据的线程,模拟一个时间运行较久的任务。

package com.justin.futurecallable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureCallableMain {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//创建Future<String>泛型接口队列,你也可以直接用一个泛型接口来接收
		List<Future<String>> resultQueue = new ArrayList<Future<String>>();
		Future<String> result;
		
		//创建线程池
		ExecutorService threadpool = Executors.newCachedThreadPool();
		//创建任务
		DataCreatorDemo dataCreator1 = new DataCreatorDemo(" IN YOUR AREA.");
		DataCreatorDemo dataCreator2 = new DataCreatorDemo(" IS THE REVOLUTION.");
		//任务提交到线程池
		
		//泛型接口队列接收线程返回值
		resultQueue.add(threadpool.submit(dataCreator1));
		//用泛型接口来接收
		result = threadpool.submit(dataCreator2);
		//此时主线程可以做其他事情,为了模拟显示,让主线程睡眠2秒后输出语句
		try {
			Thread.sleep(1000);
			System.out.println("MainThread do sth else....");
			Thread.sleep(1000);
			System.out.println("MainThread comeback.");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		//最后拿出两个线程中的数据
		System.out.println(resultQueue.get(0).get());
		System.out.println(result.get());
		threadpool.shutdown();
	}

}

在Main函数中,首先第14-15行,创建Future<String>的泛型接口或泛型接口队列(如果你接收不止一个线程的返回值的话),用于接收实现Callable接口的线程返回。第25-27行,把线程提交到线程池执行后,主线程就可以去做别的事情了,等到真正需要线程执行结果时,(第39-40行)才去队列或泛型接口中调用get()方法来拿。

但是Future是一个接口,也就是说,它不能实例化对象,不过Future接口有一个唯一实现类FutureTesk,使用这个类一样可以接收实现Callable接口线程的返回值。

2、使用FutureTesk类实例对象来接收线程的返回值:FutureTesk的源码比较长,大家可以看这里:

https://www.cnblogs.com/daxin/p/3802392.html

其中FutureTesk类的两个构造方法:

/**
  * 构造参数 传入callable对象,并且将当前状态state设置为null。
  */
 public FutureTask(Callable<V> callable) {
 	if (callable == null) {
		throw new NullPointerException();
	}
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
 }

 /**
  * 传入runnable对象和result
  * 通过Executors包装一下,还是返回一个callable的实例
  * 在执行完runnable的任务后直接返回result
  */
 public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
 }

还有其中的run()方法(源码看上面给出的链接),无论是把FutureTesk实例对象交给Thread执行还是线程池执行,都是调用这个run()方法。我们还要知道的是,FutureTesk类实现了RunnableFuture接口,而RunnableFuture接口继承自Runnable和Future<T>接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

所以FutureTesk类对象可以接收Future<T>的返回值,也可以实现Runnable被线程执行。来看具体实现:

package com.justin.futurecallable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

public class FutureTeskDemo {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//创建线程池
		ExecutorService threadpool = Executors.newCachedThreadPool();
		//创建任务
		DataCreatorDemo dataCreator1 = new DataCreatorDemo(" IN YOUR AREA.");
		FutureTask<String> futuretesk = new FutureTask<String>(dataCreator1);
		threadpool.submit(futuretesk); //FutureTesk任务提交线程池执行
		
		//此时主线程可以做其他事情,为了模拟显示,让主线程睡眠2秒后输出语句
		try {
			Thread.sleep(1000);
			System.out.println("MainThread do sth else....");
			Thread.sleep(1000);
			System.out.println("MainThread comeback.");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//最后拿出两个线程中的数据
		System.out.println(futuretesk.get());
		threadpool.shutdown();
	}

}

创建数据线程还是第一个例子中那个。唯一不同就是第15行实例化了一个FutureTesk<String>泛型类型对象来接收线程的返回值,同样,在你需要线程返回结果时,调用该对象的get()方法即可。

总的来说,Future和FutureTesk都能接收实现Callable接口线程的返回值,并在需要时通过get()方法来获取结果。主线程只需要把实现Callable接口的线程提交到线程池去执行,就可以去做其他的事情,这种方式适合那些需要执行大量计算的线程,但又不急着需要计算结果的场景,就像我上一篇日志中,每一个线程分别对不同分段的数组做查找操作,它们之间是并行的,每一个线程提交到线程池后,都会立刻返回,由Future<Integer>泛型队列接收,之后你可以随时从这个队列中获取每一个线程的查找结果,查找成功的线程会返回元素下标,查找失败的线程会返回“-1”,详细代码可以看我上一篇日志。

本篇日志的完整实现代码已上传GitHub:

https://github.com/justinzengtm/Java-Multithreading/tree/master/%E5%B9%B6%E8%A1%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/FutureMode

https://gitee.com/justinzeng/multithreading/tree/master/%E5%B9%B6%E8%A1%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/Future%E6%A8%A1%E5%BC%8F

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/91384131