ListenableFuture并发编程

2020-11-19 15:09:16 浏览数 (1)

说起并发,大家都会想到多线程,当然多线程的合理使用能够提高cpu的利用率和提高请求的响应效率,使用不当会带来线程安全问题,导致数据一致性问题以及jvm内存溢出。接着我们就分析一下并发编程和使用guava的ListenableFuture实现高效编程。

背景需求

就以退款列表为例,前端分页查询退款信息,每页展示20条数据,但是页面需要展示的每一行退款信息不只是简单的退款相关字段,有物流信息、订单信息以及买家信息,也就是说每一条退款数据都是以退款数据为基线,查询到的多维相关信息的整合内容。

方案分析

对于上述需求,我们一般会有两种实现方式,分别是单线程同步查询组合并返回和多线程异步查询组合并返回,实现思路如下:

单线程

多线程

我们可以这样假设,如果每一次外部服务调用rt是200ms,那么如果是单线程访问的话需要循环同步调用20次,也就是外部依赖服务调用需要耗时2s;多线程的话我们可以并发开20个线程异步调用外部服务,理论上只需要200ms,这就是并发编程的魅力所在。

技术实现

单线程

业务代码实现:

@Override

public List<User> selectAll() {

List<User> 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<User> list = userService.selectAll();

System.out.println("耗时:" (System.currentTimeMillis() - begin));

for(User u : list) {

System.out.println(u);

}

}

运行测试代码:

同步查询耗时1300ms。

多线程

业务代码:

@Override

public List<User> selectAll2() {

List<User> list = this.userDao.queryAll();

List<Future<User>> futureList = new ArrayList<>(list.size());

for(User u : list) {//多线程并发查询

Future<User> future = executorService.submit(new Callable<User>() {

@Override

public User call() throws Exception {

return mockOutService(u);

}

});

futureList.add(future);

}

for(Future<User> 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<User> list = userService.selectAll2();

System.out.println("耗时:" (System.currentTimeMillis() - begin));

for(User u : list) {

System.out.println(u);

}

}

运行测试代码:

可以看到查询效率有非常明显的提升,但是没有按照我们预期的提升四倍,是因为第一次运行过程中中间涉及了线程池创建线程等其他问题,此处不做纠结。

优秀的ListenableFuture

在上述代码中我们分析了单线程和多线程对查询的性能对比,可以明显发现多线程的优势所在。不知道有没有人注意到,虽然使用jdk自带的线程池实现了多线程操作,但是获取多线程处理结果的代码是同步的:

如果调用future.get()的时候,线程还没有处理完,是需要同步等待的,也就是说需要主线程不停的轮询多线程的处理结果,这样的话代码比较复杂并且效率也不高。

ListenableFuture是guava中提供的对多线程的比较优秀的支持,ListenableFuture顾名思义就是可以监听的Future,它是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。使用ListenableFuture Guava帮我们检测Future是否完成了,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。ListenableFuture是一个接口,它从jdk的Future接口继承。

接下来我们基于ListenableFuture来实现上述案例的并发查询:

@Override

public List<User> selectAll3() {

ExecutorService executorService = Executors.newFixedThreadPool(20);

List<User> list = this.userDao.queryAll();

for (User u : list) {//多线程并发查询

ListenableFuture<User> future = listeningExecutorService.submit(new Callable<User>() {

@Override

public User call() throws Exception {

return mockOutService(u);

}

});

Futures.addCallback(future, new FutureCallback<User>() {

@Override

public void onSuccess(User user) {//调用成功后要做的事情,比如把订单信息填进去

user.getName();

}

@Override

public void onFailure(Throwable t) {

System.out.println("occur error:" t.toString());

}

});

}

return list;

}

运行测试代码:

同样我们可以拿到结果,并且还有一定的性能提升。我们用一张图来对比jdk线程池并发编程与guavaListenableFuture并发编程:

在日常开发中希望更多的使用guava的ListenableFuture替代jdk自带线程池的Future,能够带来更好的性能提升。

0 人点赞