Java并发编程(1)- Callable、Future和FutureTask

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。




撸过JavaSE(即Java基础技术栈)的小伙伴都知道,实现多线程有两种方式,一种是继承Thread,extends Thread 然后实现其中的run()方法;另外一种是实现Runnable接口,即implements Runnable,然后实现其中的run()方法;仔细观察这两种方式,会发现这两者都不能返回线程异步执行完的结果,但在实际项目开发中却偶尔需要获取其中的返回结果,咋办嘞?于是乎CallableFuture就排上用场了,本文我们将对其做一番详尽的介绍!

  还是先介绍下多线程的传统实现方式吧,如下代码所示:

public class ThreadUtil {
public static void main(String[] args) throws Exception{
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("---子线程正在执行---"+Thread.currentThread().getName());
Map<String,Object> dataMap=Maps.newHashMap();
dataMap.put("id",10010);
dataMap.put("name","steadyjack");
dataMap.put("nickName","多隆");
System.out.println("---子线程执行后得到的结果:"+dataMap);
}
});
try {
thread.start();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("---主线程正在执行---"+Thread.currentThread().getName());
}
}

  在上述代码中,我们首先通过一个实现Runnable接口的匿名实现类创建了一个线程对象实例,即thread,并编写实现了其中run()方法的代码逻辑(而这就是该线程要执行的任务),主要是构造一个私有变量Map<String,Object>,并将相关的数据塞入进去。

  点击运行该代码后,显而易见可以预测其运行结果:   


  从上述编写的代码以及运行结果来看,会发现如果我们想获取得到dataMap的内容是很困难的,因为run()方法的返回值为void;当然啦,也不是完全没有办法,在上面的条件下,如果想要获取到dataMap并做进一步的操作的话,则可以将dataMap定义为全局的共享变量,或者使用线程通信的方式来达到效果,如下所示为通过共享全局变量的方式:   

public class ThreadUtil {
private static final Map<String,Object> dataMap=Maps.newHashMap();

………
}

  之后就可以在该类的其他地方使用了!

  但这种方式有个很明显的弊端,那就是多线程共享、并发访问可能会出现安全性问题,即如果开启10个线程,每个线程需要对dataMap里头的key,即id 1,在高并发的情况下其最终的运行效果很可能不一定是 10020 (因为初始值为10010,每个线程加1次,10个线程下来就是加10次,理想情况下为10020),如下图所示:


  但有时候我们在项目里头既要用到异步(为了解耦)、也想要获取异步执行的结果,可以说是“鱼和熊掌皆想兼得”:


  于是乎这个重任就落到了CallableFuture身上了,这是JDK1.5版本开始就已经提供了,可以通过它们实现在任务异步执行完毕之后得到任务的执行结果。

  看到这里,可能有些小伙伴会发问:为什么通过Callable就可以获取到线程异步执行的结果呢?这一切还得回归到源码身上,如下所示为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;
}

  会发现它跟Runnable一样都是接口,但区别在于创建Callable时可以传入一个泛型V,而这个泛型类型V会发现真是call()方法执行后返回的结果(call()方法的作用类似于run()方法,反正都是指一个线程要执行的任务),OK,到此谜底就解开了!

  那么怎么使用Callable呢?在Java里面可以通过调用ExecutorService类里面的相关API来使用Callable,如下图所示:


  仔细观察上图,会发现如果想要获取线程执行Callable类型任务后的结果时,需要通过Future进行获取,那么Future为何物呢?

  Future,也是一个接口,可以对具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果,必要时可以通过get()方法获取执行结果,该方法会阻塞直到任务返回结果,如下图所示:


  从上图中可以得知,在Future接口中声明了5个方法,下面依次解释每个方法的作用:

1cancel():该方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false;方法里的参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务;如果任务已经完成,则无论mayInterruptIfRunningtrue还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunningtrue还是false,肯定返回true

2isCancelled():该方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true

3isDone():该方法表示任务是否已经完成,若任务已经完成,则返回true

4get():该方法用来获取线程的执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;

5get(long timeout, TimeUnit unit):用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

  综上所述,Future提供了三种功能:判断任务是否完成;可以中断任务;可以获取任务的执行结 果;

  实践是检验整理的唯一标准,我们还是需要编写一定的代码进行验证,如下代码所示我们先定义一个线程实现类:   

public class ProductThread implements Callable<Map<String,Object>>{
private Map<String,Object> dataMap;

@Override
public Map<String, Object> call() throws Exception {
System.out.println("---子线程在执行任务---");
Thread.sleep(3000);

dataMap=Maps.newHashMap();
dataMap.put("id",10010);
dataMap.put("name","steadyjack");
dataMap.put("nickName","多隆");
return dataMap;
}
}

  然后通过ExecutorService调用相应的API执行该任务,如下代码所示:   

public class ThreadUtil {
private static final Map<String,Object> dataMap=Maps.newHashMap();

public static void main(String[] args) throws Exception{
ArrayBlockingQueue queue=new ArrayBlockingQueue(2);
ExecutorService executorService=new ThreadPoolExecutor(2,4,1, TimeUnit.MINUTES,queue);
Future<Map<String,Object>> future=executorService.submit(new ProductThread());
executorService.shutdown();
System.out.println("---主线程在执行任务---");

Map<String,Object> map=future.get();
System.out.println("子线程执行结果:"+map);
}
}

  点击运行代码后即可得到最终的运行效果,如下图所示:   


  从该运行结果中可以得知,当调用executorService.submit()方法时,主线程main会开启一个异步的子线程去执行ProductThread中的任务,然后继续往后面的代码走,即继续执行executorService.shutdown()System.out.println("---主线程在执行任务---");等代码;最后是通过future.get()获取线程最终异步执行返回的结果。

  至此我们已经通过Callable + Future组合实现两个目的:

1)开启异步的线程执行相应的任务(解耦;提高系统吞吐量,提高资源利用率)

2)可以获取到异步执行后的结果

  有了这两大利器,其实在实际项目开发中就已经够用了,但JDK的开源者很用心,还提供了另外一大利器FutureTask,那么为啥要有这玩意呢?很简单,因为Future是一个接口,所以无法直接通过new来创建对象,因此就有了FutureTask;我们先来看下它的定义吧:   

public class FutureTask<V> implements RunnableFuture<V> {}

  它实现了RunnableFuture接口,而RunnableFuture接口的定义如下所示:   

public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}

  从中可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,想想都觉得确实挺牛逼:


  话不多说,直接上代码吧:   

ArrayBlockingQueue queue=new ArrayBlockingQueue(2);
ExecutorService executorService=new ThreadPoolExecutor(2,4,1, TimeUnit.MINUTES,queue);
FutureTask<Map<String,Object>> futureTask=new FutureTask<Map<String, Object>>(new ProductThread());
executorService.execute(futureTask);
Map<String,Object> resMap=futureTask.get();
executorService.shutdown();
System.out.println("---主线程正在执行任务---"+Thread.currentThread().getName());

System.out.println("--子线程执行任务后得到的结果:"+resMap);

  运行结果如下图所示:


  从上述该源代码中可以看出:
1futureTask可以作为Runnable被执行:executorService.execute(futureTask);

2)也可以当做Future获取线程异步执行后的结果:futureTask.get();

总结:

1)代码下载:关注“程序员实战基地”微信公众号(扫描下图微信公众号即可),回复“100”,即可获取代码下载链接;至此,我们已经介绍完了CallableFuture以及FutureTask相关基础特性,下一篇我们将重点再详细地介绍下FutureTask更层次的东西,欢迎关注debug的技术公众号一起学习干货技术吧!


我是debug,一个相信技术改变生活、技术成就梦想 的攻城狮;如果本文对你有帮助,请关注公众号,并动动手指点赞、收藏以及转发,你的三连可是debug分享的动力哦