FutureTask编程思想

982 阅读4分钟

前言

在实际工作场景中会很有多这样需求:大屏分类统计、首页展示不同的信息、xxx页面聚合展示

而这些需求往往存在不同的表或者别的数据结构中,如果将聚合信息的需求分成不同的接口进行查询难免效率会低一些

毕竟需要调用多个后台接口采集不同的数据进行分类展示,那么目前应对手段也不少

比如客户端缓存、服务端内存缓存、redis缓存、数据库缓存、前端页面静态化等等这些手段

但是除了缓存以外能不能在一个接口里面将所有数据查询出来再一起返回到前端页面?这么做以后又该如何保证这个接口的性能?

可以尝试着采用多线程手段提高接口性能,将一个大任务分成多个小任务然后分发到每个线程进行执行

为了获取到每个线程执行后结果,我们可以采用Callable接口

Callable

下面将模拟首页大屏图表统计信息接口统计内容为:水费、电费、租金的方式

/**
 * <p>
 * 图表统计信息
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 17:42
 */
public class CallableDemo {

    public static void main(String[] args) throws InterruptedException {
        long beginTime = System.currentTimeMillis();

        gatherStatistics();

        long endTime = System.currentTimeMillis();
        System.out.println("首页大屏统计接口耗时:" + (endTime - beginTime) + "毫秒");
    }

    /**
     * 首页大屏统计
     *
     * @return 统计结果
     */
    public static Map<String, List<BigDecimal>> gatherStatistics() throws InterruptedException {
        Map<String, List<BigDecimal>> map = new HashMap<>(6);
        map.put("统计水费", statisticsWaterCost());
        map.put("统计电费", statisticsElectricityCost());
        map.put("统计租金", statisticsResideCost());
        return map;
    }

    /**
     * 统计水费
     *
     * @return 水费统计列表
     */
    public static List<BigDecimal> statisticsWaterCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }

    /**
     * 统计电费
     *
     * @return 电费统计列表
     */
    public static List<BigDecimal> statisticsElectricityCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 100; i <= 200; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }

    /**
     * 统计租金
     *
     * @return 租金统计列表
     */
    public static List<BigDecimal> statisticsResideCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 200; i <= 300; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }
}

总体耗时为6003毫秒,接下来通过多线程的方式进行改造一波

/**
 * <p>
 * 图表统计信息
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 17:42
 */
public class CallableDemo {

    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long beginTime = System.currentTimeMillis();

        gatherStatistics();

        long endTime = System.currentTimeMillis();
        System.out.println("首页大屏统计接口耗时:" + (endTime - beginTime) + "毫秒");
    }

    /**
     * 首页大屏统计
     *
     * @return 统计结果
     */
    public static Map<String, List<BigDecimal>> gatherStatistics() throws InterruptedException, ExecutionException {
        Future<List<BigDecimal>> statisticsWaterCostFuture = executorService.submit(() -> statisticsWaterCost());
        Future<List<BigDecimal>> statisticsElectricityCostFuture = executorService.submit(() -> statisticsElectricityCost());
        Future<List<BigDecimal>> statisticsResideCostFuture = executorService.submit(() -> statisticsResideCost());

        Map<String, List<BigDecimal>> map = new HashMap<>(6);
        map.put("统计水费", statisticsWaterCostFuture.get());
        map.put("统计电费", statisticsElectricityCostFuture.get());
        map.put("统计租金", statisticsResideCostFuture.get());
        return map;
    }

    /**
     * 统计水费
     *
     * @return 水费统计列表
     */
    public static List<BigDecimal> statisticsWaterCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }

    /**
     * 统计电费
     *
     * @return 电费统计列表
     */
    public static List<BigDecimal> statisticsElectricityCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 100; i <= 200; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }

    /**
     * 统计租金
     *
     * @return 租金统计列表
     */
    public static List<BigDecimal> statisticsResideCost() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        List<BigDecimal> vos = new ArrayList<>();
        for (int i = 200; i <= 300; i++) {
            vos.add(BigDecimal.valueOf(i));
        }
        return vos;
    }
}

当将首页大屏统计接口改造成多线程时,执行时间为3002毫秒.提升了3000毫秒

从中发现了只是一个小小的改造,无非加了三样东西:线程池、Callable、Future

FutureTask

从上面的代码中可以看到Future可以获取到Callable的返回值。我们直接看线程池的submit方法

使用

这么一来我们也就可以自己来实例化FutureTask将Callable任务传进去,然后交由线程执行即可获取返回值

/**
 * <p>
 * 图表统计信息
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 17:42
 */
public class CallableDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(() -> "123456");
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

嗯!照样可以拿到Callable的返回值,那它底层的实现原理又是怎么样的呢?

手写

从表象中可以直观的发现get方法一直阻塞到执行完run方法为止,那接下来我们就来实现一把

/**
 * <p>
 * 手写FutureTask
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 18:17
 */
public class MyFutureTask<T> implements Runnable {

    private Callable<T> callable;

    private volatile T result;

    private volatile boolean isExecuteRun = false;

    private BlockingDeque<Thread> waiters = new LinkedBlockingDeque<>(1000);


    public MyFutureTask(Callable<T> callable) {
        this.callable = callable;
    }

    @Override
    public void run() {
        try {
            result = callable.call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            isExecuteRun = true;
        }
        Thread thread;
        while ((thread = waiters.poll()) != null) {
            LockSupport.unpark(thread);
        }
    }


    public T get() throws InterruptedException, ExecutionException {
        while (!isExecuteRun) {
            Thread thread = Thread.currentThread();
            waiters.offer(thread);
            LockSupport.park(thread);
        }
        return result;
    }
}
/**
 * <p>
 * 图表统计信息
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 17:42
 */
public class CallableDemo {


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyFutureTask<String> futureTask = new MyFutureTask<>(() -> "123456");
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

从结果中发现能够实现FutureTask的思想,是不是发现好简单!

请求合并

对于Future接口还有一个实现类比较常用CompletableFuture,在实际工作中可以用它来针对某一查询接口进行优化

/**
 * <p>
 * 请求合并
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/25 0025 17:42
 */
public class CallableDemo {

    private static BlockingDeque<GetByIdRequest> waiters = new LinkedBlockingDeque<>(1000);

    private static List<User> users = new ArrayList<>();

    static {
        for (int i = 1; i <= 5; i++) {
            String id = String.valueOf(i);
            users.add(new User(id, "张三" + id));
        }


        // 定时任务,每秒处理队列中的请求
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleWithFixedDelay(() -> {

            // 收集请求列表
            List<GetByIdRequest> getByIdRequests = new ArrayList<>();
            int size = waiters.size();
            for (int i = 0; i < size; i++) {
                getByIdRequests.add(waiters.poll());
            }

            // 根据请求id分组请求,目的是同样的一个id只查询一次数据库.并且将查询出来的结果集通知正在等待的线程
            Map<String, List<GetByIdRequest>> collect = getByIdRequests.stream().collect(Collectors.groupingBy(GetByIdRequest::getId));
            collect.forEach((id, list) -> {
                try {
                    User user = getById(id);
                    list.forEach(v -> v.getCompletableFuture().complete(user));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
        }, 1, 1, TimeUnit.SECONDS);
    }

    /**
     * 根据id查询用户的请求类
     */
    static class GetByIdRequest {
        private String id;
        private CompletableFuture<User> completableFuture;

        public GetByIdRequest(String id) {
            this.id = id;
            this.completableFuture = new CompletableFuture<>();
        }

        public String getId() {
            return id;
        }

        public CompletableFuture<User> getCompletableFuture() {
            return completableFuture;
        }
    }

    /**
     * 用户实体类
     */
    static class User {
        private String id;
        private String name;

        public User(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    User user = BlockGetById("1");
                    System.out.println(Thread.currentThread().getName() + "执行结束,获取到的用户信息" + user + ",执行结束时间为:" + System.currentTimeMillis());
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }

    public static User BlockGetById(String id) throws ExecutionException, InterruptedException {
        GetByIdRequest getByIdRequest = new GetByIdRequest(id);
        waiters.offer(getByIdRequest);
        // 阻塞等待结果
        return getByIdRequest.completableFuture.get();
    }

    /**
     * 根据主键id查询名字
     *
     * @param id 主键id
     * @return 名字
     */
    public static User getById(String id) throws InterruptedException {
        // 模拟查询数据库操作,睡眠一秒
        TimeUnit.SECONDS.sleep(1);
        Map<String, List<User>> userIdMap = users.stream().collect(Collectors.groupingBy(User::getId));
        List<User> users = userIdMap.get(id);
        return users != null && users.size() > 0 ? users.get(0) : null;
    }
}

为了降低查询数据库的频率,可以将多个请求合并起来并将查询结果通知到对应的多个请求

这也算是应对并发的一种手段,弊端是查询不再是直接查询,而是需要等待通知

从单个请求来说会降低响应速度但是从整体来说它提高了查询效率

总结

当同一个请求处理很多业务的时候,而这些任务又互不相关!可以通过FutureTask可以优化程序的性能!

通过自定义手写的MyFutureTask相信大家也对FutureTask底层实现原理有了一定的了解,无非是阻塞/唤醒的运用而已

同时也分享了CompletableFuture实现请求合并降低数据库压力的小技巧