Java代码优化实战 - 分而治之

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


上一篇文章中,我们介绍了请求合并的代码优化方案,它能够解决大量请求造成的数据库压力过大的情况,下面我们再来看看另一种情况下的解决方案。

其实在学数据结构和算法的时候,大家应该都接触过分而治之的思想,其实说白了就是递归调用本函数的一个过程,在这个过程中,不断把任务变小,简化计算的流程。这种思想,在进行系统架构的时候同样适用。如果一个请求要访问大量的数据,那么我们就可以将这个任务拆分分别执行,最终再将执行结果返回给客户端。

这里就要引入JDK 1.7后提供的一个多线程执行框架Fork/Join,它能够把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果。

ForkJoin框架为我们提供了RecursiveActionRecursiveTask来创建ForkJoin的任务,简单来说:

  • Recursiveaction: 用于创建没有返回值的任务
  • RecursiveTask :用于创建有返回值的任务

举个例子,还是用上一小节中我们的数据,现在数据库中存储了id从0到999的一千件商品,我们要对其总值进行求和(别问为什么不直接用sum()函数,举个例子而已)。

    @ResponseBody
    @RequestMapping("/single")
    public int single() {
        long startTime = System.currentTimeMillis();
        int sum = 0;
        for (int i = 0; i < 1000; i++) {
            sum += itemService.queryByCode(i + "").getPrice();
        }
        System.out.println(sum);
        long endTime = System.currentTimeMillis();
        System.out.println("程序运行时间:" + (endTime - startTime) + "ms");
        return sum;
    }
复制代码

看一下程序运行时间,5235毫秒:

使用ForkJoin对任务进行划分:

public class ForkJoinTask  extends RecursiveTask<Integer> {
    private int arr[];
    private int start;
    private int end;

    private static final int MAX = 50;

    public ForkJoinTask(int[] arr, int start, int end){
        this.arr=arr;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum=0;
        if((end - start) < MAX) {
            //直接做业务工作
            for (int i = start; i < end; i++) {
                sum += arr[i];
            }
            return sum;
        }   else{
            //继续拆分
            int middle = (start + end) / 2;
            ForkJoinTask left=new ForkJoinTask(arr, start, middle);
            ForkJoinTask right=new ForkJoinTask(arr, middle, end);
            left.fork();
            right.fork();
            return left.join() + right.join();
        }
    }
}
复制代码

再运行测试:

    @ResponseBody
    @RequestMapping("/fork")
    public int forkJoin() {
        long startTime = System.currentTimeMillis();
        int arr[] = new int[1000];
        for (int i = 0; i < 1000; i++) {
            arr[i]=i;
        }

        ForkJoinPool pool=new ForkJoinPool();
        ForkJoinTask task=new ForkJoinTask(arr,0,arr.length);
        Integer sum =  pool.invoke(task);
        System.out.println(sum);

        long endTime = System.currentTimeMillis();
        System.out.println("程序运行时间:" + (endTime - startTime) + "ms");
        return sum;
    }
复制代码

再看一下程序运行时间,只有6毫秒:

是不是觉得快了很多,直接将运行速度提升了非常多!其实ForkJoin运行速度快的原因还有一个黑科技,那就是当一个线程在完成自己的任务队列的处理任务后,会帮助其他线程完成任务,完成后再放回其他队列,这也被称为工作窃取。

如上图所示,线程1在完成自己的任务后,发现线程2还有任务没有完成,这时它会去取到线程2没有完成的任务,做完后再把结果放回线程2。

除此之外,我们还可以通过增加线程数量进一步加快运行速度,线程数量的选择可以根据具体业务环境进行配置优化。

ForkJoinPool pool=new ForkJoinPool(Runtime.getRuntime().availableProcessors()*4);
复制代码

总结:

与上一篇文章一起,分别从请求合并和分而治之两种角度介绍了系统的优化,可以看出,在平常的工作中,代码优化这一条路还有很长要走。文中的代码大家可以从我的github获取。

github.com/trunks2008/…

猜你喜欢

转载自juejin.im/post/7018091060718993439