前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系。譬如人口普查工作,大家都知道我国总人口为14亿左右,可是14亿的数目是怎么数出来呢?倘若只有一个人去统计,从小数到老都数不完。好比一个线程老牛破车干不了多少事情,既然如此,不妨多起一些线程呗。于是人口普查工作就由中央分解到各个省份,各省又分派到下面的市县,再由市县分派到更下面的街道或乡镇,每个街道和乡镇统计完本辖区内的人口数量后,分别上报给对应的市县,市县再上报给省里,最后由各省上报中央,这才统计完成全国的人口总数。在人口普查的案例中,这些线程不但存在上下级关系,而且下级线程的任务由上级线程分派而来,同时下级线程的处理结果又要交给上级线程汇总。根据任务流的走向,可将整个处理过程划分成下列三个阶段:
1、第一阶段从主线程开始,从上往下逐级分解任务,此时线程总数逐渐变多,每个分线程都先后收到上级线程分派的任务;
2、第二阶段由最下面的基层线程进行具体的任务操作,此时线程总数是不变的;
3、第三阶段从基层线程开始,从下往上逐级汇总任务结果,此时线程总数逐渐变少,最后主线程会收到汇总完成的最终结果;
以上的第一阶段,概括地说叫做“分而治之”;至于第三阶段,可概括称之为“汇聚归一”。为了实现这种分而治之的业务需求,Java7新增了Fork/Join框架用以对症下药。该框架的Fork操作会按照树状结构不断分出下级线程,其对应的是分而治之的过程;而Join操作则把叶子线程的运算结果逐级合并,其对应的是汇聚归一的过程。在这分分合合的过程当中,悄然浮现出Fork/Join框架专用的线程池工具ForkJoinPool,而它正是从ExecutorService派生出来的一个子类。鉴于分治策略的特殊性质,Fork/Join框架并不使用常见的Runnable任务,而改为使用专门的递归任务RecursiveTask,该任务的fork方法实现了分而治之的Fork操作,join方法实现了汇聚归一的Join操作。
举个简单应用的例子,对于一段连续的数列求和,比如对0到99之间的所有整数求和,通常的做法是写个循环语句依次累加。常规的写法显然只有一个主线程在执行加法运算,无法体现多核CPU的性能优势,故而可以尝试将求和操作分而治之,先把整段数列划分为若干个子数列,再对各个子数列分别求和,最后汇总所有子数列的求和结果。采取RecursiveTask实现这种分派求和任务的话,可参见下面的代码例子,注意递归任务的入口由run方法改成了compute方法:

//定义一个求和的递归任务
public class SumTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = 1L;
private static final int THRESHOLD = 20; // 不可再切割的元素个数门槛
private int src[]; // 待求和的整型数组
private int start; // 待求和的下标起始值
private int end; // 待求和的下标终止值 public SumTask(int[] src, int start, int end) {
this.src = src;
this.start = start;
this.end = end;
} // 对指定区间的数组元素求和
private Integer subTotal() {
Integer sum = 0;
for (int i = start; i < end; i++) { // 求数组在指定区间的元素之和
sum += src[i];
}
// 打印求和日志,包括当前线程的名称、起始数值、终止数值、区间之和
String desc = String.format("%s ∑(%d~%d)=%d", Thread.currentThread().getName(), start, end, sum);
System.out.println(desc);
return sum;
} @Override
protected Integer compute() {
if ((end - start) <= THRESHOLD) { // 不可再切割了
return subTotal(); // 对指定区间的数组元素求和
} else { // 区间过大,还能继续切割
int middle = (start + end) / 2; // 计算区间中线的位置
// 创建左边分区的求和任务
SumTask left = new SumTask(src, start, middle);
left.fork(); // 把左边求和任务添加到处理队列中
// 创建右边分区的求和任务
SumTask right = new SumTask(src, middle, end);
right.fork(); // 把右边求和任务添加到处理队列中
// 左边子任务的求和结果加上右边子任务的求和结果,等于当前任务的求和结果
int sum = left.join() + right.join();
// 打印求和日志,包括当前线程的名称、起始数值、终止数值、区间之和
String desc = String.format("%s ∑(%d~%d)=%d", Thread.currentThread().getName(), start, end, sum);
System.out.println(desc);
return sum; // 返回本次任务的求和结果
}
}
}

然后外部往上面的求和任务输入待求和的整型数组,并调用任务对象的invoke获取执行结果,即可命令内置的线程池启动求和任务。调用代码示例如下:

	// 测试任务自带的线程池框架
private static void testInternalTask() {
// 下面初始化从0到99的整型数组
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = i + 1;
}
// 创建一个求和的递归任务
SumTask task = new SumTask(arr, 0, arr.length);
try {
// 执行同步任务,并返回执行结果。任务的invoke方法使用了内部的ForkJoinPool
Integer result = task.invoke();
System.out.println("最终计算结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}

运行以上的调用代码,输出下列的线程池日志:

ForkJoinPool.commonPool-worker-3: ∑(0~12)=78
ForkJoinPool.commonPool-worker-0: ∑(75~87)=978
ForkJoinPool.commonPool-worker-2: ∑(50~62)=678
ForkJoinPool.commonPool-worker-0: ∑(87~100)=1222
ForkJoinPool.commonPool-worker-3: ∑(12~25)=247
ForkJoinPool.commonPool-worker-3: ∑(0~25)=325
ForkJoinPool.commonPool-worker-0: ∑(75~100)=2200
ForkJoinPool.commonPool-worker-2: ∑(62~75)=897
ForkJoinPool.commonPool-worker-2: ∑(50~75)=1575
ForkJoinPool.commonPool-worker-1: ∑(37~50)=572
ForkJoinPool.commonPool-worker-3: ∑(25~37)=378
ForkJoinPool.commonPool-worker-3: ∑(25~50)=950
ForkJoinPool.commonPool-worker-1: ∑(0~50)=1275
ForkJoinPool.commonPool-worker-2: ∑(50~100)=3775
main: ∑(0~100)=5050
最终计算结果: 5050

从日志可见,Fork/Join框架的默认线程池一共启动了四个线程(正好是设备的CPU个数),同时最后一步的统计工作由主线程来完成。

注意到前述的调用代码并未写明Fork/Join框架的线程池工具ForkJoinPool,这是因为递归任务拥有默认的内置线程池,即使外部不指定线程池对象,递归任务也会使用内置线程池进行线程调度。不过默认的线程池无法设置个性化的参数,所以还是建议在代码中显式指定ForkJoinPool线程池,并调用线程池对象的execute/invoke/submit三个方法之一启动递归任务。有关这三个方法的具体用途说明如下:
execute:异步执行指定任务,且无返回值。
invoke:同步执行指定任务,并等待返回值,返回值就是最终的运算结果。
submit:异步执行指定任务,且返回结果任务对象。之后可择机调用结果任务的get方法获取最终的运算结果。
下面是在外部调用时显式指定线程池的求和代码例子:

	// 测试任务以外的线程池框架
private static void testPoolTask() {
// 下面初始化从0到99的整型数组
int[] arr = new int[100];
for (int i = 0; i < 100; i++) {
arr[i] = i + 1;
}
// 创建一个求和的递归任务
SumTask task = new SumTask(arr, 0, arr.length);
// 创建一个用于分而治之的线程池,并发数量为6
ForkJoinPool pool = new ForkJoinPool(6);
// 命令线程池执行求和任务,并返回存放执行结果的任务对象
ForkJoinTask<Integer> taskResult = pool.submit(task);
try {
Integer result = taskResult.get(); // 等待执行完成,并获取求和的结果数值
System.out.println("最终计算结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
pool.shutdown(); // 关闭线程池
}

运行修改后的调用代码,输出下列的线程池日志:

ForkJoinPool-1-worker-1: ∑(0~12)=78
ForkJoinPool-1-worker-3: ∑(62~75)=897
ForkJoinPool-1-worker-5: ∑(12~25)=247
ForkJoinPool-1-worker-5: ∑(87~100)=1222
ForkJoinPool-1-worker-5: ∑(25~37)=378
ForkJoinPool-1-worker-5: ∑(37~50)=572
ForkJoinPool-1-worker-5: ∑(25~50)=950
ForkJoinPool-1-worker-1: ∑(0~25)=325
ForkJoinPool-1-worker-4: ∑(50~62)=678
ForkJoinPool-1-worker-4: ∑(50~75)=1575
ForkJoinPool-1-worker-6: ∑(75~87)=978
ForkJoinPool-1-worker-6: ∑(75~100)=2200
ForkJoinPool-1-worker-2: ∑(0~50)=1275
ForkJoinPool-1-worker-3: ∑(50~100)=3775
ForkJoinPool-1-worker-1: ∑(0~100)=5050
最终计算结果: 5050

由日志可见,此时的线程池运行情况与刚才相比有两点不同:其一开启的线程数量变多了,这缘于新的线程池对象设置了并发数量为6;其二最后一步的统计工作仍在线程池内部执行,因而减轻了主线程的负担。结论当然是外部显式指定ForkJoinPool的方式更优。

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(一百零六)Fork+Join框架实现分而治之的更多相关文章

  1. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

  2. Java开发笔记(十六)非此即彼的条件分支

    前面花了大量篇幅介绍布尔类型及相应的关系运算和逻辑运算,那可不仅仅是为了求真值或假值,更是为了通过布尔值控制流程的走向.在现实生活中,常常需要在岔路口抉择走去何方,往南还是往北,向东还是向西?在Jav ...

  3. Java 7 Fork/Join 框架

    在 Java7引入的诸多新特性中,Fork/Join 框架无疑是重要的一项.JSR166旨在标准化一个实质上可扩展的框架,以将并行计算的通用工具类组织成一个类似java.util中Collection ...

  4. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  5. Fork/Join 框架

    本文部分摘自<Java 并发编程的艺术> Fork/Join 框架概述 Fork/Join 框架是 Java7 提供的一个用于并行执行任务的框架,是把一个大任务分割成若干个小任务,最终汇总 ...

  6. Java开发笔记(一百零二)信号量的请求与释放

    前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:1.某块代码被加锁之后,对其它线程而言就处于繁忙状态,缺乏弹性的阈值范围:2 ...

  7. Java开发笔记(一百零一)通过加解锁避免资源冲突

    前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...

  8. Java开发笔记(一百零四)普通线程池的运用

    前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...

  9. Java开发笔记(一百零三)线程间的通信方式

    前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...

随机推荐

  1. [Go] Slices vs Array

    It is recommended to use 'slice' over 'Array'. An array variable denotes the entire array; it is not ...

  2. 洛谷 P3376 【模板】网络最大流 题解

    今天学了网络最大流,EK 和 Dinic 主要就是运用搜索求增广路,Dinic 相当于 EK 的优化,先用bfs求每个点的层数,再用dfs寻找并更新那条路径上的值. EK 算法 #include< ...

  3. PDB文件会影响性能吗?

    有人问了这样的问题:"我工作的公司正极力反对用生成的调试信息构建发布模式二进制文件,这也是我注册该类的原因之一.他们担心演示会受到影响.我的问题是,在发布模式下生成符号的最佳命令行参数是什么 ...

  4. 关于dword ptr 指令

    dword 双字 就是四个字节ptr pointer缩写 即指针[]里的数据是一个地址值,这个地址指向一个双字型数据比如mov eax, dword ptr [12345678] 把内存地址12345 ...

  5. 10-ESP8266 SDK开发基础入门篇--上位机通过串口控制ESP8266灯亮灭

    https://www.cnblogs.com/yangfengwu/p/11087618.html 其实这一节就是对上三节的综合测试 https://www.cnblogs.com/yangfeng ...

  6. 产品生命周期(Product Life Circle,PLC)

    什么是产品生命周期? 产品生命周期是新产品从开发进入市场到被市场淘汰的整个过程.产品生命周期可分为初创期.成长期.成熟期.衰退期. 产品生命周期有什么用? 在产品不同的生命阶段,公司的业务目的都不同. ...

  7. 50、Spark Streaming实时wordcount程序开发

    一.java版本 package cn.spark.study.streaming; import java.util.Arrays; import org.apache.spark.SparkCon ...

  8. uni-app 模拟器

    1.安装MuMu模拟器 Android模拟器端口: 7555 2.安装夜神模拟器 Android模拟器端口: 62001

  9. Automatic Ship Detection in Optical Remote Sensing Images Based on Anomaly Detection and SPP-PCANet

    基于异常检测和 PCANet 的船舶目标检测 船舶检测会遇到三个问题: 1.船低对比度 2.海平面情况复杂 3.云,礁等错误检测 实验步骤: 1.预处理海陆边界,掩膜陆地 2.异常检测获得感兴趣区域, ...

  10. go -- 测试

    package 测试 import ( "fmt" "github.com/magiconair/properties/assert" "net/ht ...