一、分而治之
思想:
将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
策略:
对于一个规模为 n 的问题,若该问题可以容易地解决(比如说规模 n 较小)则直接解决,否则将其分解为 k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同(子问题相互之间有联系就会变为动态规划算法),递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
举例:归并排序(降序图示)
该算法是采用分治法的一个非常典型的应用。将已有序的子序列进行2-路归并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
二、动态规划
需要前面的所有前序状态才能完成推理过程。我们将这一模型称为高阶马尔科夫模型。对应的推理过程叫做“动态规划法”。
比如斐波那契数列 1 2 3 5 8 13 . . .
应用:爬楼梯可以一步一个台阶,也可以一步两个台阶,那么n级台阶有多少种爬法
问题分析:到达1级台阶是有一种,到达2级台阶有两种,到达3级台阶有3种,画图解释
图中 1⃣代表1级,2⃣代别2级,依次类推
可以看出能到达6⃣级的只有4⃣级和5⃣级,那么只要知道到达4⃣级和5⃣级的有多少种爬法,然后相加就行了。
就是f(6)=f(5)+f(4)
同理f(5)=f(4)+f(3)
那么可以推出,f(n)=f(n-1)+f(n-2)
这里不是在讲斐波那契数列,而是在讲它的分析过程。可以很明显的看出来,上一级的结果对下一级的计算是有影响的,这种就是动态规划。
三、Fork-Join
工作密取
即当前线程的 Task 已经全被执行完毕,则自动取到其他线程的 Task 池中取出 Task 继续执行。
ForkJoinPool 中维护着多个线程(一般为 CPU 核数)在不断地执行 Task,每个线程除了执行自己职务内的 Task 之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的 Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高 CPU 利用率。
Fork/Join使用的标准范式
四、Fork-Join API
ForkJoinPool 线程池,任务队列
创建一个池子,默认开启的最大线程数等于服务器的核心数。每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
每个工作线程在运行中产生新的Task任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。在既没有自己的任务,也没有可以窃取的任务时,进入休眠。
- invoke()执行任务,开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。和主线线程是同步的。
- execute()执行任务,开启一个新线程,主线程和pool里的子线程是异步的,注意如果主线程使用join()方法会阻塞主线程
- submit()执行任务,开启一个新线程,主线程和pool里的子线程是异步的,注意如果主线程使用join()方法会阻塞主线程
ForkJoinTask Task任务类
代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。
- RecusiveTask<T> 有返回值的任务,T就是返回值的类型
- RecusiveAction 没有返回值的任务。
我们需要继承上面两个抽象类,重写compute()方法,这个方法是进行任务再分裂或者进行结果计算。
join() 等待该任务的处理线程处理完毕,获得返回值。需要注意的是join是阻塞的。
五、Fork-Join实现4000长度的随机数组求和,每个task求和计算的时候模拟业务请求1s
把数组分割成一小段一小段的求和任务,并行执行,最后进行汇总。实现RecursiveTask,获取返回结果。
代码实现:
/**
* ForkJoin执行累加
*/
public class SumArray {
/**
* 继承RecursiveTask, 实现compute()方法
* 在compute()方法内进行判断是否再进行任务分割,或者获取结果
*/
private static class SumTask extends RecursiveTask<Integer> {
// 最小分割长度。length/最小长度
private final static int THRESHOLD = MakeArray.ARRAY_LENGTH / 5;
// 源数据
private int[] src;
// 起下标
private int fromIndex;
// 终下标
private int toIndex;
public SumTask(int[] src, int fromIndex, int toIndex) {
this.src = src;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
protected Integer compute() {
System.out.println("ThreadId: " + Thread.currentThread().getId() + " 是否再分割: " + !(toIndex - fromIndex < THRESHOLD) + " Task源数据 from:" + fromIndex + " to: " + toIndex);
// 任务是否需要再次分割
if (toIndex - fromIndex < THRESHOLD) {
int count = 0;
for (int i = fromIndex; i <= toIndex; i++) {
count = count + src[i];
}
try {
//模拟业务请求,休眠1000ms
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//返回结果
return count;
} else {
//再次分割中值
int mid = (fromIndex + toIndex) / 2;
//创建左边Task任务
SumTask left = new SumTask(src, fromIndex, mid);
//创建右边Task任务
SumTask right = new SumTask(src, mid + 1, toIndex);
// 执行任务
invokeAll(left, right);
return left.join() + right.join();
}
}
}
public static void main(String[] args) {
// 随机数组
int[] src = MakeArray.makeArray();
// 创建池forkjon池子,默认最大线程为服务器核心数
// ForkJoinPool pool = new ForkJoinPool();
// 可以指定最多使用多少个线程,这里未来效果,限制最大6个线程
ForkJoinPool pool = new ForkJoinPool(6);
// 创建task任务实例
SumTask innerFind = new SumTask(src, 0, src.length - 1);
// 开始时间
long start = System.currentTimeMillis();
pool.invoke(innerFind);
//pool.execute(innerFind); 主线程和pool里的子线程是异步的,注意如果下面使用join()方法会阻塞主线程
//pool.submit(innerFind); 主线程和pool里的子线程是异步的,注意如果下面使用join()方法会阻塞主线程
// 获取结果
Integer result = innerFind.join();
System.out.println("求和结果 " + result
+ " 耗时:" + (System.currentTimeMillis() - start) + "ms");
}
static class MakeArray {
//数组长度
public static final int ARRAY_LENGTH = 4000;
public static int[] makeArray() {
//new一个随机数发生器
Random r = new Random();
int[] result = new int[ARRAY_LENGTH];
for (int i = 0; i < ARRAY_LENGTH; i++) {
//用随机数填充数组
result[i] = r.nextInt(100);
}
return result;
}
}
}
可以看到结果,使用了6个线程,总共创建了15个Task任务,其中分割了7次,即计算了8次。
缺:源码解析-池子底层是怎么实现的?工作密取怎么实现的?task是怎么分配到线程的?