带返回值的线程
传统的创建线程的两种方法
- 实现Runnable接口
- 继承Thread方法然后重写run方法
这两种方法创建的线程都没有带有返回值.有时候我们需要线程带返回值过来.则可以通过实现Callable接口,配合FutureTask或者executors来实现.看简单的一个计算阶乘10!的方法
public class test{
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask=new FutureTask(new Factorial(10));
Thread t=new Thread(futureTask);
t.start();
//调用get方法获取线程计算的返回值,调用get会阻塞
System.out.println(futureTask.get());
}
static class Factorial implements Callable<Integer>{
private int n;
public Factorial(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
if(n==0){
return 1;
}
int num=n;
for (int i = 1; i < n; i++) {
num*=i;
}
return num;
}
}
}
复制代码
可以观察到它是new一个FutureTask对象,FutureTask对象需要一个实现Callable接口的对象,然后再交由一个线程来执行. 而我们编写的计算逻辑在call方法中.
进入FutureTask观察其源码可以发现它实现了RunnableFuture接口,而RunnableFuture接口又继承自Runnable接口和Future接口,所以它其实也就需要实现run()方法. 查看它的run方法便可以得知,它在run方法中其实也就调用了Callable接口的call方法,并获取返回值,设置到 outcome属性中,然后通过调用get方法来返回该值.
比较常用的是通过线程池来提交任务.
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
//线程池传递一个实现Callable接口的实现类
Future<Integer> submit = executorService.submit(new Factorial(10));
System.out.println(submit.get());
executorService.shutdown(); //关闭线程池
}
复制代码
查看源码可观察到,它内部也是将Callable封装到FutureTask中,然后调用线程池中的线程来执行FutureTask.
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
复制代码
CompletionService的使用
假设现在有一个网页,我们需要下载图片并显示出来给用户看,最高效率的做法就是每下载完一张图片就显示一张,其它未下载完成的占一个空间等待渲染.用FutureTask如何实现,我们用一个HashMap来模拟图片,key代表图片,value代表下载的时间(秒).
final Map<String,Integer> map=new LinkedHashMap<>();//确保插入有序
map.put("1.jpg",5);
map.put("2.jpg",3);
map.put("3.jpg",7);
map.put("4.png",1);
复制代码
我们假设为每张图片创建一个下载任务,我们用sleep来模拟下载时间,那么代码应该这么写.
LinkedBlockingQueue<Future<String>> s= new LinkedBlockingQueue<>();//用于保存Future
ExecutorService service = Executors.newFixedThreadPool(4);//创建一个线程池
for (String s : map.keySet()) {  //遍历每张图片,并提交任务
Future<String> future= service.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(map.get(s)*1000); //模拟下载中
return s; //下载完成返回这张图片
}
});
s.add(future);
}
//从阻塞队列中取的Future获取结果
for (int i = 0; i < 4; i++) {
Future<String> poll = null;
try {
poll = s.take();
System.out.println(poll.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
output:
1.jpg
2.jpg
3.jpg
4.png
复制代码
获取到future然后保存起来,在外面进行get()获取结果,但是很明显这个不符合我们想要的,我们想要的是先下载完成的立马可以显示出来,很明显4.png下载才需要1秒应该第一显示,上面的思路逻辑明显不符合要求.
我们需要的是当这个任务完成时立马被设置到阻塞队列中.
传统线程启动
查看FutureTask源码,可以知道它有一个done()方法,默认方法体为空,当完成设置结果后,它会调用done()方法.所以我们的任务就是重写这个done方法.然而非常的麻烦,假设不用线程池来还好,那么只需要新建一个FutureTask的子类然后重写done方法
class QueenFutureTask<T> extends FutureTask<T>{
public QueenFutureTask(Callable<T> callable) {
super(callable);
}
public QueenFutureTask(Runnable runnable, T result) {
super(runnable, result);
}
@Override
protected void done() {
try {
block.add((String) this.get()); //把下载完的图片加入到阻塞队列中
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
复制代码
然后用传统线程来启动即可
public static void main(String[] args) throws ExecutionException, InterruptedException {
final Map<String,Integer> map=new LinkedHashMap<>();
map.put("1.jpg",5);
map.put("2.jpg",3);
map.put("3.jpg",7);
map.put("4.png",1);
for (String s : map.keySet()) {
new Thread(new QueenFutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(map.get(s) * 1000);
return s;
}
}) {
}).start();
}
for (int i = 0; i < 4; i++) {
String take = block.take();
System.out.println(take);
}
}
output:
4.png
2.jpg
1.jpg
3.jpg
复制代码
用线程池也行,不过很麻烦,因为你查看源代码就可以得知,默认实现是通过新建一个FutureTask来执行,而我们又无法直接重写FutureTask,只能使用的QueenFutureTask,所以你还得重写线程池的newTaskFor方法(代码就不贴了,太长).所幸jdk提供了一个CompletionService类(终于进入正题),将Executor和BlockingQueue功能融合
CompletionService的使用
它将提交的任务实现完成后放入阻塞队列中,使用非常简单,自己看吧
public static void main(String[] args) throws InterruptedException, ExecutionException {
final Map<String,Integer> map=new LinkedHashMap<>();
map.put("1.jpg",5);
map.put("2.jpg",3);
map.put("3.jpg",7);
map.put("4.png",1);
ExecutorService service = Executors.newFixedThreadPool(4);
//需要传一个线程池
CompletionService<String> completionService=new ExecutorCompletionService<>(service);
for (String s : map.keySet()) {
completionService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(map.get(s)*1000);
return s;
}
});
}
for (int i = 0; i < map.size(); i++) {
Future<String> take = completionService.take(); 
String s = take.get();
System.out.println(s);
}
service.shutdown();
}
output:
4.png
2.jpg
1.jpg
3.jpg
复制代码