小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
在上一篇文章中,我们介绍了请求合并的代码优化方案,它能够解决大量请求造成的数据库压力过大的情况,下面我们再来看看另一种情况下的解决方案。
其实在学数据结构和算法的时候,大家应该都接触过分而治之的思想,其实说白了就是递归调用本函数的一个过程,在这个过程中,不断把任务变小,简化计算的流程。这种思想,在进行系统架构的时候同样适用。如果一个请求要访问大量的数据,那么我们就可以将这个任务拆分分别执行,最终再将执行结果返回给客户端。
这里就要引入JDK 1.7后提供的一个多线程执行框架Fork/Join
,它能够把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果。
ForkJoin
框架为我们提供了RecursiveAction
和RecursiveTask
来创建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获取。