满怀忧思,不如先干再说!做干净纯粹的技术分享,赞想点就点,注相关就关,有话评论区直接走起来!
在《Java并发编程》合集第一篇讲解线程创建时,说到创建线程有四种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 通过线程池【这种方式应该是线程的管理策略,有的地方并不将此种方式计入其中】
到这里全面性的讲解一下Callable接口,之前文章中使用Runnable接口创建线程,但是Runnable接口的run() 存在一个缺陷问题,就是不能将执行完的结果返回。
如果你对Callable很懵,不会用,想不到应用场景,只会背面试题,这篇文章你是来对了哦!
Java为了实现这个功能,在jdk1.5中提出了Callable接口。Callable任务可以有返回值,但是无法直接从Callable任务里获取返回值;需要使用Future来获取Callable任务的返回值。
所以Callable任务和Future模式,通常结合起来使用。学习Callable接口就必须也要学习Future接口。通过本文你可以掌握:
- Callable接口与Future关系
- Callable接口通过Thread和线程池配合Future实现线程创建和运行
- Callable接口与Runnable接口区别,以及应用场景,什么时候用Runnable什么时候用Callable
- FutureTask的方法作用以及cancel()方法详解
- 通过【社区】/【朋友圈】案例,介绍Callable接口提升系统响应速度实践方案
认识Callable
Callable 接口位于java.util.concurrent包下。此包简称JUC
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception; //计算结果,如果无法计算则抛出异常。
}
发现Callable接口是一个泛型接口,并且使用@FunctionalInterface注解修饰,说明是一个函数式接口,其中包含一个call()抽象方法,拥有与泛型接口类型一致的返回值,并且可以抛出异常。
Callable配合FutureTask创建线程需求:通过Callable接口实现线程创建,线程中实现语句输出,并返回整型结果
分析:
- 创建类实现Callable接口,返回值为整型,设置Callable接口泛型类型为Integer
- 实现call方法,实现线程逻辑并返回一个整型数字
- 在测试类中创建Callable接口实现类对象
- 构造一个FutureTask对象,将Callable接口实现类类型当做构造参数传入
- 创建Thread对象,传入FutureTask对象,启动线程
- 通过FutureTask对象的get方法获取线程运行的返回值
import java.util.concurrent.Callable;
// 1、实现Callable接口,指定泛型类型为Integer
public class CallableDemo implements Callable<Integer> {
// 2、重写call方法,返回值类型与泛型类型一致
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() "线程运行......");
// 返回值,也就是线程的运算结果
return 28;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableMain {
public static void main(String[] args) {
// 1、创建实现类对象
CallableDemo callableDemo = new CallableDemo();
// 2、创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(callableDemo);
// 3、将futureTask当做Thread类参数传入
Thread t1 = new Thread(futureTask,"t1");
// 4、启动线程
t1.start();
// 5、通过futureTask的get方法获取返回值,有异常抛出
try {
Integer result = futureTask.get();
System.out.println("线程执行结果:" result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
这里之所以要转成 FutureTask 放进 Thread中去,是因为Callable本身与Thread没有关系,通过FutureTask才能和Thread产生联系。
发现实现起来并不复杂,如果想要真正搞明白Callable就需要深入研究Future接口
Future接口Future接口同样位于java.util.concurrent包下。下方为Future接口的源码,附加简单注释
public interface Future<V> {
// 尝试取消此任务的执行。
boolean cancel(boolean mayInterruptIfRunning);
// 如果此任务在正常完成之前被取消,则返回true
boolean isCancelled();
// 如果此任务完成,则返回true 。 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true
boolean isDone();
// 获得任务计算结果
V get() throws InterruptedException, ExecutionException;
// 可等待多少时间去获得任务计算结果
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。
实现Future模式通俗点来描述就是:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableMain {
public static void main(String[] args) {
// 1、创建实现类对象
CallableDemo callableDemo = new CallableDemo();
// 2、创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(callableDemo);
// 3、将futureTask当做Thread类参数传入
Thread t1 = new Thread(futureTask,"t1");
// 4、启动线程
t1.start();
// 5、通过futureTask的get方法获取返回值,有异常抛出
try {
Integer result = futureTask.get();
System.out.println("线程执行结果:" result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
// 6、判断是否完成任务
boolean isDone = futureTask.isDone();
System.out.println(isDone);
}
}
运行结果:
Future 用于存储从另一个线程获得的结果。如果只是简单创建线程,直接使用Runnable就可以,想要获得任务返回值,就用Future。
FutureTask位于java.util.concurrent包下,可取消的异步计算。 此类提供Future的基本实现,具有启动和取消计算、查询以查看计算是否完成以及检索计算结果的方法。 计算完成后才能检索结果; 如果计算尚未完成, get方法将阻塞。 一旦计算完成,就不能重新开始或取消计算【除非使用runAndReset调用计算】。该类继承结构图:
FutureTask源码
- FutureTask类实现了RunnableFuture接口
- RunnableFuture接口继承Runnable和Future接口
- 所以FutureTask可以被当做参数,传入到Thread类构造方法中,因为Thread类构造参数接收的是Runnable类型对象,根据多态可以传入
FutureTask构造可以接收Callable和Runnable。以此通过FutureTask为中介,将Callable和Thread关联起来
FutureTask应用场景及注意事项你在什么时候使用过Callable呢?评论区告诉我们吧
应用场景:
- 在主线程执行比较耗时的操作时,但同时又不能去阻塞主线程时,就可以将这样的任务交给FutureTask对象在后台完成,然后等主线程需要的时候,就可以直接get()来获得返回数据或者通过isDone()来获得任务的状态。
- 一般FutureTask多应用于耗时的计算,这样主线程就可以把一个耗时的任务交给FutureTask,然后等到完成自己的任务后,再去获取计算结果
注意:
- 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。
- 一旦计算完成,就不能再重新开始或取消计算。
- get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
- 因为只会计算一次,因此通常get方法放到最后。
通过以下案例,演示FutureTask的方法作用:
- FutureTask接收Callable接口,Callable接口是函数式接口,可以使用JDK8中的lambda表达式创建
- 在任务中,循环10次,每次循环睡眠200毫秒,来控制任务需要2秒才执行结束,需要一段时间
- 然后创建并启动线程,调用isDone()
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class CallableMain {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
// 1、通过 lambda 创建FutureTask
FutureTask<String> futureTask = new FutureTask<>(() -> {
String str = "";
for (int i = 0; i < 10; i ) {
str = String.valueOf(i);
Thread.sleep(200);
}
return str;
});
// 2、创建线程对象
Thread t1 = new Thread(futureTask,"t1");
t1.start();
// isDone():查看任务是否完成,完成返回true,否则是false
System.out.println(futureTask.isDone());
// get():获取任务结果,如果任务没有执行完则阻塞
System.out.println("阻塞式获取结果:" futureTask.get());
// 取消任务,参数为true取消,false不取消
futureTask.cancel(true);
}
}
运行结果:发现程序阻塞,2秒后打印运算结果
如果使用get(long timeout, TimeUnit unit)超时等待方法,设置一个超时时间,如果还没有获取到结果,则抛出异常
比如,我们的任务需要执行2秒,这里设置等待1秒,没有拿到结果,抛出TimeoutException超时异常
cancel()中的false参数
此方法传入true会中断线程停止任务,传入false则会让线程正常执行至完成,难以理解传入false的作用,既然不会中断线程,那么这个cancel方法不就没有意义了吗?
简单来说,传入false参数只能取消还没有开始的任务,若任务已经开始了,就任由其运行下去。当创建了Future实例,任务可能有以下三种状态:
- 等待状态:还未执行run()方法。此时调用cancel()方法不管传入true还是false都会标记为取消,任务依然保存在任务队列中,但当轮到此任务运行时会直接跳过。
- 运行中:已经在执行run()方法。此时传入true会中断正在执行的任务,传入false则不会中断。
- 完成状态:已经执行完run()方法,或者被取消了,亦或者方法中发生异常而导致中断结束。此时cancel()不会起任何作用,因为任务已经完成了。
Future.cancel(true)适用于:
- 长时间处于运行的任务,并且能够处理interruption
Future.cancel(false)适用于:
- 未能处理interruption的任务
- 不清楚任务是否支持取消
- 需要等待已经开始的任务执行完成
线程池ExecutorService类中的submit方法支持提交并运行线程,提供了Callable和Runnable的支持。
线程池文章并没有发布,下一篇开始线程池部分,这简单使用一下,不会线程池的同学,可以先查阅一下其他资料
实现方式
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableAndThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、创建线程池
ExecutorService threadExecutor = Executors.newSingleThreadExecutor();
// 2、提交线程任务
Future<Integer> future = threadExecutor.submit(() -> {
System.out.println("线程池执行Callable线程");
return 1024;
});
// 3、获取线程执行结果
Integer result = future.get();
System.out.println(result);
// 4、关闭线程池,否则程序不停止
threadExecutor.shutdown();
}
}
运行结果:
Runnable和Callable的区别
- Callable实现call()方法,Runnable实现run()方法
- Callable的任务执行后可返回任务运算结果,而Runnable的任务结果无法返回
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
案例:
系统中有社区功能,可以查看其他用户发布的说说,类似于朋友圈,还需获取每一个说说的点赞和评论信息,比如一页10条来说,需要访问21次数据库【10条说说1次查到,每个说说的点赞信息和评论信息各需要10次,共21次】,访问一次数据库按100ms计算,21次,累计时间为2.1s。这个响应时间,怕是无法令人满意的。可以通过异步化改造接口。
查出帖子列表后,迭代帖子列表,在循环里起10个线程,并发去获取每条帖子的点赞列表,同时另起10个线程,并发去获取每条帖子的评论列表。这样改造之后,接口的响应时间大大缩短,在200ms。这个时候就要用Callabel结合Future来实现。
分析:
- 这里模拟一下数据库查询,先创建三个实体类分别为说说,评论,点赞;
- 创建三个方法分别获取说说,根据说说id获取评论和点赞;
- 通过线程池使用Callable接口启动线程,线程中不做数据操作,将获取到的评论和点赞数据返回出来
- 在末尾统一获取数据,这样线程运行时就不会因为获取数据而等待阻塞,只需要启动线程,获取数据即可
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class Shuoshuo implements Serializable {
private Long id;
private String context;
private List<Comment> comments;
private List<ShuoshuoLike> shuoshuoLikes;
public Shuoshuo(Long id, String context) {
this.id = id;
this.context = context;
}
}
import lombok.Data;
import java.io.Serializable;
@Data
public class Comment implements Serializable {
private Long id;
private Long shuoshuoId;
private String commentContent;
public Comment(Long id, Long shuoshuoId, String commentContent) {
this.id = id;
this.shuoshuoId = shuoshuoId;
this.commentContent = commentContent;
}
}
import lombok.Data;
import java.io.Serializable;
@Data
public class ShuoshuoLike implements Serializable {
private Long id;
private Long shuoshuoId;
private Long userId;
public ShuoshuoLike(Long id, Long shuoshuoId, Long userId) {
this.id = id;
this.shuoshuoId = shuoshuoId;
this.userId = userId;
}
}
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;
import java.util.stream.Collectors;
public class ShuoshuoMain {
// 1、获取说说
private static List<Shuoshuo> getShuoshuoList() {
List<Shuoshuo> shuoshuos = new ArrayList<>();
shuoshuos.add(new Shuoshuo(1L,"说说1"));
shuoshuos.add(new Shuoshuo(2L,"说说2"));
shuoshuos.add(new Shuoshuo(3L,"说说3"));
shuoshuos.add(new Shuoshuo(4L,"说说4"));
return shuoshuos;
}
// 2、根据说说Id 获取评论
private static List<Comment> getComment(Long shuoshuoId) {
List<Comment> comments = new ArrayList<>();
comments.add(new Comment(1L,1L,"1号说说很不错1"));
comments.add(new Comment(2L,1L,"1号说说很不错2"));
comments.add(new Comment(3L,1L,"1号说说很不错3"));
comments.add(new Comment(4L,1L,"1号说说很不错4"));
comments.add(new Comment(5L,1L,"1号说说很不错5"));
comments.add(new Comment(6L,2L,"2号说说很不错1"));
comments.add(new Comment(7L,2L,"2号说说很不错2"));
comments.add(new Comment(8L,2L,"2号说说很不错3"));
comments.add(new Comment(9L,2L,"2号说说很不错4"));
comments.add(new Comment(10L,3L,"3号说说很不错1"));
comments.add(new Comment(11L,3L,"3号说说很不错2"));
comments.add(new Comment(12L,3L,"3号说说很不错3"));
comments.add(new Comment(13L,4L,"4号说说很不错1"));
comments.add(new Comment(14L,4L,"4号说说很不错2"));
comments.add(new Comment(15L,4L,"4号说说很不错3"));
// 使用stream过滤出指定id的评论
List<Comment> commentList = comments.stream().filter(item -> item.getShuoshuoId() == shuoshuoId).collect(Collectors.toList());
return commentList;
}
// 3、根据说说Id 获取点赞
private static List<ShuoshuoLike> getLike(Long shuoshuoId) {
List<ShuoshuoLike> shuoshuoLikes = new ArrayList<>();
shuoshuoLikes.add(new ShuoshuoLike(1L,1L,101L));
shuoshuoLikes.add(new ShuoshuoLike(2L,1L,102L));
shuoshuoLikes.add(new ShuoshuoLike(3L,1L,103L));
shuoshuoLikes.add(new ShuoshuoLike(4L,1L,104L));
shuoshuoLikes.add(new ShuoshuoLike(5L,2L,105L));
shuoshuoLikes.add(new ShuoshuoLike(6L,2L,106L));
shuoshuoLikes.add(new ShuoshuoLike(7L,2L,107L));
shuoshuoLikes.add(new ShuoshuoLike(8L,2L,108L));
shuoshuoLikes.add(new ShuoshuoLike(9L,3L,101L));
shuoshuoLikes.add(new ShuoshuoLike(10L,3L,103L));
shuoshuoLikes.add(new ShuoshuoLike(11L,3L,104L));
shuoshuoLikes.add(new ShuoshuoLike(12L,3L,105L));
shuoshuoLikes.add(new ShuoshuoLike(13L,4L,105L));
shuoshuoLikes.add(new ShuoshuoLike(14L,4L,103L));
shuoshuoLikes.add(new ShuoshuoLike(15L,4L,104L));
shuoshuoLikes.add(new ShuoshuoLike(16L,4L,101L));
// 使用stream过滤出指定id的点赞信息
List<ShuoshuoLike> shuoshuoLikeList = shuoshuoLikes.stream().filter(item -> item.getShuoshuoId() == shuoshuoId).collect(Collectors.toList());
return shuoshuoLikeList;
}
// 4、主方法
public static void main(String[] args) {
// 1、获取帖子
List<Shuoshuo> shuoshuoList = getShuoshuoList();
// 2、创建线程池对象,分别获取评论和点赞信息
ExecutorService commentPool = Executors.newSingleThreadExecutor();
ExecutorService likePool = Executors.newSingleThreadExecutor();
// 3、任务列表,将启动的线程存储进任务列表中,在最后统一获取数据
List<Future> commentFutureList = new ArrayList<>();
List<Future> likeFutureList = new ArrayList<>();
try {
// 4、循环说说,获取评论和点赞信息,这里是耗时操作,此处开启对应的线程,同时获取数据,可以提升性能
for (Shuoshuo shuoshuo : shuoshuoList) {
// 5、查看评论列表,此处只启动并执行线程任务,由于get会阻塞,此处不通过get方法获取执行结果,
Future<List<Comment>> commentFuture = commentPool.submit(() -> {
List<Comment> comment = getComment(shuoshuo.getId());
return comment;
});
// 将future存储进任务列表中
commentFutureList.add(commentFuture);
// 6、查看点赞列表
Future<List<ShuoshuoLike>> likeFuture = likePool.submit(() -> {
List<ShuoshuoLike> likeList = getLike(shuoshuo.getId());
return likeList;
});
likeFutureList.add(likeFuture);
}
// 7、循环说说列表,从任务列表中获取执行结果
for (int i = 0; i < shuoshuoList.size(); i ) {
// List为有序集合,不需担心数据错位,或者可以使用Map集合根据说说id来存储对应的评论和点赞信息
Shuoshuo shuoshuo = shuoshuoList.get(i);
shuoshuo.setComments((List<Comment>) commentFutureList.get(i).get());
shuoshuo.setShuoshuoLikes((List<ShuoshuoLike>) likeFutureList.get(i).get());
}
}catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 8、关闭线程池
commentPool.shutdown();
likePool.shutdown();
}
// 9、遍历说说列表
shuoshuoList.forEach(System.out::println);
}
}
运行结果:
总结
- 掌握Callable接口和Future接口的关系
- Callable配合Thread类和线程池启动线程任务,通过Future获取运行状态和结果
- 掌握Callable应用场景,主要应用在主任务中有耗时操作,可以通过开辟子线程完成次操作,之后再获取子线程的执行结果,主线程根据此结果继续执行
- 如果主任务中不需要子线程的结果,比如子线程负责向数据库写数据,就不需要使用Callable,使用Runnable即可,因为Callable比Runnable稍微复杂点
临近年关,公司业务也在结尾,加班加点继续码字,接下来会更新线程池相关技术点,觉得不错点点赞,关注,支持一下哦!
,