说起并发,大家都会想到多线程,当然多线程的合理使用能够提高cpu的利用率和提高请求的响应效率,使用不当会带来线程安全问题,导致数据一致性问题以及jvm内存溢出。接着我们就分析一下并发编程和使用guava的ListenableFuture实现高效编程。
背景需求
就以退款列表为例,前端分页查询退款信息,每页展示20条数据,但是页面需要展示的每一行退款信息不只是简单的退款相关字段,有物流信息、订单信息以及买家信息,也就是说每一条退款数据都是以退款数据为基线,查询到的多维相关信息的整合内容。
方案分析
对于上述需求,我们一般会有两种实现方式,分别是单线程同步查询组合并返回和多线程异步查询组合并返回,实现思路如下:
单线程
多线程
我们可以这样假设,如果每一次外部服务调用rt是200ms,那么如果是单线程访问的话需要循环同步调用20次,也就是外部依赖服务调用需要耗时2s;多线程的话我们可以并发开20个线程异步调用外部服务,理论上只需要200ms,这就是并发编程的魅力所在。
技术实现
单线程
业务代码实现:
@Override
public List selectAll() {
List list = this.userDao.queryAll();
for(User u : list) {
//模拟外部服务调用
this.mockOutService(u);
}
return list;
}
private void mockOutService(User u) {
try {
u.getName();
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
测试代码:
public static void main(String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("spring-root.xml");
context.start();
UserService userService = context.getBean(UserService.class);
long begin = System.currentTimeMillis();
List list = userService.selectAll();
for(User u : list) {
}
}
运行测试代码:
同步查询耗时1300ms。
多线程
业务代码:
@Override
public List selectAll2() {
List list = this.userDao.queryAll();
List> futureList = new ArrayList(list.size());
for(User u : list) {//多线程并发查询
Future future = executorService.submit(new Callable() {
@Override
public User call() throws Exception {
return mockOutService(u);
}
});
futureList.add(future);
}
for(Future future : futureList) {//获取多线程查询结果并组装
try {
User u = future.get();
u.getName();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
return list;
}
测试代码:
public static void main(String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext("spring-root.xml");
context.start();
UserService userService = context.getBean(UserService.class);
long begin = System.currentTimeMillis();
List list = userService.selectAll2();
for(User u : list) {
}
}
运行测试代码:
可以看到查询效率有非常明显的提升,但是没有按照我们预期的提升四倍,是因为第一次运行过程中中间涉及了线程池创建线程等其他问题,此处不做纠结。
优秀的ListenableFuture
在上述代码中我们分析了单线程和多线程对查询的性能对比,可以明显发现多线程的优势所在。不知道有没有人注意到,虽然使用jdk自带的线程池实现了多线程操作,但是获取多线程处理结果的代码是同步的:
如果调用future.get()的时候,线程还没有处理完,是需要同步等待的,也就是说需要主线程不停的轮询多线程的处理结果,这样的话代码比较复杂并且效率也不高。
ListenableFuture是guava中提供的对多线程的比较优秀的支持,ListenableFuture顾名思义就是可以监听的Future,它是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。使用ListenableFuture Guava帮我们检测Future是否完成了,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。ListenableFuture是一个接口,它从jdk的Future接口继承。
接下来我们基于ListenableFuture来实现上述案例的并发查询:
@Override
public List selectAll3() {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List list = this.userDao.queryAll();
for (User u : list) {//多线程并发查询
ListenableFuture future = listeningExecutorService.submit(new Callable() {
@Override
public User call() throws Exception {
return mockOutService(u);
}
});
Futures.addCallback(future, new FutureCallback() {
@Override
public void onSuccess(User user) {//调用成功后要做的事情,比如把订单信息填进去
user.getName();
}
@Override
public void onFailure(Throwable t) {
}
});
}
return list;
}
运行测试代码:
同样我们可以拿到结果,并且还有一定的性能提升。我们用一张图来对比jdk线程池并发编程与guavaListenableFuture并发编程:
在日常开发中希望更多的使用guava的ListenableFuture替代jdk自带线程池的Future,能够带来更好的性能提升。
领取专属 10元无门槛券
私享最新 技术干货