小白的web优化之路 四、使用Callable与Future来实现异步请求

  在上一篇 小白的web优化之路 三、使用MQ来实现事务异步处理 中,我们使用MQ来将任务异步话,使接口速度得到的极大的提高,这一节中,我们就要考虑一个需要同步返回多次请求的问题。

  在完成了产品经理的那个发送短信通知的‘古怪’需求之后,小白觉得自己终于可以继续开发CURD的任务了,心里不免有点小激动。突然,小白收到了产品经理的一个消息,小白心里一震,打开消息,屏幕上写着“小白我这有一个获取信息的需求,你过来一下吧”。

  小白心里一想,不就是获取信息么,谁不会呢?哪知道去了产品经理的需求后,小白才意思到自己还是太年轻,这个获取信息的接口需要调用公司其它组的多个接口,然后将数据聚合起来,而且数据要求实时,并根据用户喜好返回不同的数据,这就要求很难去做一个缓存。但是如果一个一个去调用接口的话,那岂不是这个接口的总时长要爆炸?前端肯定是不会同意的?那怎么办呢?

  这时候小白的主管过来了,看到这种情况,跟小白说,我们可以使用异步请求接口,这样假使有5个接口,我们也可以以请求一个接口的速度来获取到数据。小白开始嘀咕,如果这样的话,那我是启动五个线程,去执行对应的Runnable,那我岂不是在主线程中循环的判断这个Runnable有没有执行完,并获取到相应的结果,听起来好复杂啊。主管笑了笑,你可以使用Java中的Callable与Future。

  在Java中,其实已经有了对异步处理任务并返回相应结果的类,即为Callable与Future。其中Callable为我们执行任务的接口,我们实现了Callable接口之后,将其封装到Future中,既可以使用Future来获取异步数据值,停止异步任务,捕捉异步操作中的异常等。

  我们以接口中异步获取5个网站数据为例,下面是Callable接口:

package com.happyheng.callable;

import com.happyheng.utils.HttpUtils;
import org.springframework.util.StringUtils;

import java.util.concurrent.Callable;

/**
 * 模拟访问的数据
 * Created by happyheng on 2018/6/23.
 */
public class RequestCallable implements Callable{

    private String requestUrl;

    public RequestCallable(String requestUrl) {

        this.requestUrl = requestUrl;
    }

    public String getRequestUrl() {
        return requestUrl;
    }

    public void setRequestUrl(String requestUrl) {
        this.requestUrl = requestUrl;
    }

    @Override
    public String call() throws Exception {

        if (StringUtils.isEmpty(requestUrl)) {
            return null;
        }

        long beginTime = System.currentTimeMillis();

        // 开始http请求
        String result = HttpUtils.get(requestUrl);

        System.out.println("callable url: " + requestUrl + " 用时 --------"
                + (System.currentTimeMillis() - beginTime) + " ms");

        return result;
    }
}

  可以看到,Callable代码很简单,传入一个url,然后调用HttpUtils来获取数据。

  下面是异步获取数据的Service,里面使用了Future来封装Callable,并使用线程池异步执行,最后获取到最终数据:

package com.happyheng.service;

import com.happyheng.callable.RequestCallable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 *
 * Created by happyheng on 2018/6/23.
 */
@Service
public class CallableService {

    private static final int FUTURE_TIMEOUT = 5000;

    // 使用先进先出的队列
    public final static BlockingQueue queue = new LinkedBlockingQueue<>();
    // 执行callable的线程池
    public static ExecutorService executorService = new ThreadPoolExecutor(5,
            15 ,
            10,
            TimeUnit.SECONDS,
            queue);


    public List getCallableData(List requestUrlList) {

        if (CollectionUtils.isEmpty(requestUrlList)) {
            return new ArrayList<>();
        }

        List> futureTaskList = new ArrayList<>();
        for (String url : requestUrlList) {
            RequestCallable callable  = new RequestCallable(url);
            FutureTask futureTask = new FutureTask<>(callable);
            futureTaskList.add(futureTask);

            // 线程池开始获取数据
            executorService.submit(futureTask);
        }

        // 获取数据
        List resultDataList = new ArrayList<>();
        for (FutureTask futureTask : futureTaskList) {
            try {
                // 获取数据,注意有超时时间,如果超出,即获取不到数据
                String resultData = futureTask.get(FUTURE_TIMEOUT, TimeUnit.MILLISECONDS);
                resultDataList.add(resultData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                System.out.println("超时");
                e.printStackTrace();
            }finally {
                // 最后一定要调用cancel方法,里面的参数 mayInterruptIfRunning 是是否在运行的时候也关闭,如果设置为true,那么在
                // 运行的时候也能关闭,之后的代码不会再执行。
                // 如果正在运行,暂停成功,会返回true,如果运行完了,那么不管 mayInterruptIfRunning 是什么值,都会返回false。
                futureTask.cancel(true);
            }
        }

        return resultDataList;
    }


}

  最后Controller进行调用:

package com.happyheng.controller;

import com.alibaba.fastjson.JSON;
import com.happyheng.service.CallableService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * Created by happyheng on 2018/6/18.
 */
@RestController
@RequestMapping("/callable")
public class CallableController {

    @Autowired
    private CallableService callableService;

    @RequestMapping("/test")
    public String test(HttpServletRequest request) {

        long beginTime = System.currentTimeMillis();

        // 访问网站
        List requestUrlList = new ArrayList<>();
        requestUrlList.add("https://www.baidu.com");
        requestUrlList.add("http://news.baidu.com");
        requestUrlList.add("http://www.qq.com");
        requestUrlList.add("http://www.sina.com.cn");
        requestUrlList.add("http://www.163.com");

        // 异步获取数据
        List resultData = callableService.getCallableData(requestUrlList);

        System.out.println("总用时 ------- " + (System.currentTimeMillis() - beginTime) + " ms");

        return JSON.toJSONString(resultData);
    }

}

最后的结果为:

小白的web优化之路 四、使用Callable与Future来实现异步请求_第1张图片

可以看到,我们的总时长差不多是接口中请求最高的时长,是符合预期的。


示例代码已放到github上,地址为 项目源码地址 ,欢迎大家star。


你可能感兴趣的:(java多线程,Java,Web优化之路)