我们知道Runable()是无法直接返回值的。如果某个线程想获取另一个线程中变量的值,怎么办呢?
Java为我们提供了一种处理模式——Future模式:
线程A(生产者)处理一个很耗时的工作,将会生产出一个结果。线程B(消费者)可以随时拿线程A的结果进行下一步处理。同时线程B(消费者)可以获取线程A的任务运行状态,也可以取消线程A的任务运行。
打个比喻:你到千吉买月饼,千吉收钱后给你开了一张月饼券(Future)说稍后来取,因为千吉做一盒月饼(做月饼就是Callable)需要5个小时很耗时。现在你可以随时拿这张券去换月饼,如果你一直等着,那么5小时后你就能拿到月饼。你也可以出去玩游戏,过7,8个小时侯再来取月饼也行。另外,假如你在现场等了2个小时,发现月饼还没好,这时老妈叫回家吃饭,你也可以告诉千吉,这月饼我不要了。
Future模式是一个典型的生产–>消费的模式。一个线程负责生产结果,另一个线程消费结果。
而Callable和Future一个产生结果,一个拿结果。
Callable接口类似于Runnable,但是Runnable不会返回结果,Callable可以返回结果,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。
Callable的接口定义如下;
public interface Callable<V> {
V call() throws Exception;
}
V是返回值类型。
Callable和Runnable的区别如下:
- Callable定义的方法是call,而Runnable定义的方法是run。
- Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。
- Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。
一般,我们会用ExecutorService的submit方法执行Callable,并返回Future。
来看看买月饼的代码:
public class FutureDemo {
public static void main(String[] args) {
/* 定义生产者:用来做月饼的Callable */
final Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
/*模拟耗时操作,需要5秒*/
Thread.sleep(5000);
/*返回一盒做好的月饼编号*/
return new Random().nextInt(10000);
}
};
/*开启线程B--消费者:获取月饼*/
Runnable runnable=new Runnable() {
public void run() {
try {
ExecutorService tPool = Executors.newSingleThreadExecutor();
System.out.println("老板,给我开始做月饼...");
/*启动线程A--生产者:运行耗时操作,生产月饼
*同时返回一张月饼券CookTicket*/
final Future<Integer> CookTicket = tPool.submit(callable);
/*拿到月饼*/
System.out.println("5秒钟后用月饼券兑换到月饼,该盒月饼编号:"+CookTicket.get());
System.out.println("拿饼回家...");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
输出:
老板,给我开始做月饼...
5秒钟后用月饼券兑换到月饼,该盒月饼编号:652
拿饼回家...
以上线程B中的代码中可以放在主线程完成。
可以看到,线程B调用CookTicket.get()来获取线程A返回的数据,程序首先输出“老板,给我开始做月饼…”,等待5秒后获得线程A生产的结果。CookTicket.get()会阻塞当前线程B,等待线程A运行完毕后再输出结果。
假设我现在同时要3盒月饼时怎么办?千吉说我可以开3条生产线为你制作,再给你3张Future券。
这里我们可以用集合来实现,改写一下线程B运行的runable代码
/* 开启线程B--消费者:获取月饼 */
Runnable runnable = new Runnable() {
public void run() {
try {
System.out.println("老板,给我开始做月饼...");
/*用缓存线程池同时开多个线程工作*/
ExecutorService tPool = Executors.newCachedThreadPool();
/*定义月饼券集合*/
List<Future<Integer>> futures=new ArrayList<Future<Integer>>();
/*启动线程A--生产者:运行耗时操作,三条生产线开始生产月饼*/
for(int i=0;i<3;i++){
Future<Integer> tFuture=tPool.submit(callable);
futures.add(tFuture);
}
System.out.println("等待5秒钟....");
/* 拿月饼 --消费结果*/
for(Future<Integer> ft:futures){
System.out.println("用月饼券兑换到月饼,该月饼编号:"+ ft.get());
}
System.out.println("拿饼回家...");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
};
我们开启了三个线程来执行任务。在获取任务结果时,遍历list取出future,但这是按照将future添加到list的顺序来取出的。在多线程中,先启动的线程并不一定先完成,我们无从知晓哪个任务最先结束。所以取结果时,future.get()会阻塞以等待第一个线程执行完毕返回的future,但这时有可能第二、三线程早已执行完成,所以这样并不合理。
future有个方法isDone()用来判断future是否执行完成拿到结果的,并且它不会阻塞当前线程。因此我们思路是:循环扫描List对象,如果其中的future已完成,就马上执行。如此循环,直到完成所有List中的future的结果获取。我们继续修改“拿月饼”这一段:
/* 拿月饼 --消费结果*/
Boolean flag=true;
while(flag){
for(Future<Integer> ft:futures){
if(ft.isDone()){
System.out.println("用月饼券兑换到月饼,该月饼编号:"+ ft.get());
futures.remove(ft);
break;//break很关键,因为遍历时改动了list
}
}
if(futures.size()==0){
flag=false;
}
}
至此,哪个线程先完成,就能先输出结果。不过实现起来太繁琐了,好在JAVA为我们提供了一个更好的机制:CompletionService。
CompletionService的实现是维护一个保存Future对象的BlockingQueue(阻塞的FIFO 队列,见《阻塞队列BlockingQueue》)。只有当这个Future对象状态是完成的时候,才会加入到这个Queue中,先完成的先加入。由于是队列,用它的方法take()取数据时遵守先进先出原则。所以,先完成的必定也是先被取出。
代码可以变得更简洁:
public class FutureDemo {
public static void main(String[] args) {
/* 定义生产者:用来做月饼的Callable */
final Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
/* 模拟耗时操作,需要5秒 */
Thread.sleep(5000);
/* 返回一盒做好的月饼编号 */
return new Random().nextInt(10000);
}
};
/* 开启线程B--消费者:获取月饼 */
Runnable runnable = new Runnable() {
public void run() {
try {
System.out.println("老板,给我开始做月饼...");
/*用缓存线程池可同时开多个线程工作*/
ExecutorService tPool = Executors.newCachedThreadPool();
/*定义领多盒的月饼券*/
CompletionService<Integer> CookCompletion = new ExecutorCompletionService<Integer>(tPool);
/*启动线程A--生产者:运行耗时操作,三条生产线开始生产月饼*/
for(int i=0;i<3;i++){
CookCompletion.submit(callable);
}
System.out.println("等待5秒钟....");
/* 拿到月饼 */
for(int i=0;i<3;i++){
System.out.println("用月饼券兑换到月饼,该月饼编号:"+ CookCompletion.take().get());
}
System.out.println("拿饼回家...");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
};
new Thread(runnable).start();
}
}
输出:
老板,给我开始做月饼...
等待5秒钟....
用月饼券兑换到月饼,该月饼编号:8708
用月饼券兑换到月饼,该月饼编号:8057
用月饼券兑换到月饼,该月饼编号:4188
拿饼回家...
总结:
Future接口中有如下方法:
- boolean cancel(boolean mayInterruptIfRunning)取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
- boolean isCancelled() 任务是否已经取消,任务正常完成前将其取消,则返回 true
- boolean isDone() 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
- V get()等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
- V get(long timeout, TimeUnit unit) 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException