首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 章节 26 | Fork/Join:单机版的MapReduce #### 引言 在现代软件开发的浪潮中,并发编程已成为提升程序性能、优化资源利用率的不可或缺的技术手段。Java平台凭借其强大的并发支持库,尤其是`java.util.concurrent`包,为开发者提供了丰富的工具来应对复杂的并发问题。其中,Fork/Join框架作为Java 7引入的一个重要特性,以其独特的分而治之(Divide and Conquer)策略,被誉为单机环境下的MapReduce实现,为处理大规模数据集提供了高效的解决方案。 #### Fork/Join框架概述 Fork/Join框架是一种基于工作窃取(Work-Stealing)算法的并行执行框架,旨在将大任务分解成多个小任务,并在多个线程上并行执行这些任务,最后将结果合并以得到最终答案。这一框架特别适用于那些可以递归分解为更小任务的计算密集型问题,如大规模数组处理、图像处理、科学计算等。 ##### 核心组件 - **ForkJoinPool**:Fork/Join框架的执行器,负责管理线程的创建、任务的分配和执行。 - **ForkJoinTask**:所有任务的基类,无论是递归分解的任务(RecursiveTask)还是不可分解的任务(RecursiveAction),都继承自此类。 - **Work-Stealing算法**:当某个线程完成自己的任务后,会尝试从其他线程的队列中窃取任务来执行,以此提高线程的利用率。 ##### 原理与优势 Fork/Join框架通过递归地将任务分解为更小的子任务,并在多个线程上并行执行这些子任务,从而实现了高效的并行计算。其优势在于: - **自然并行性**:对于可递归分解的问题,Fork/Join框架能够自动利用多核处理器的并行计算能力。 - **负载平衡**:通过工作窃取算法,系统能够动态地调整各线程的负载,确保资源的有效利用。 - **简化编程模型**:开发者只需关注任务的分解与合并逻辑,无需直接管理线程的生命周期和同步问题。 #### Fork/Join框架的使用 ##### 创建ForkJoinPool `ForkJoinPool`是执行Fork/Join任务的主要环境。你可以通过调用其构造函数来创建一个新的`ForkJoinPool`,或者使用静态方法`commonPool()`来获取一个全局共享的`ForkJoinPool`实例。 ```java ForkJoinPool pool = ForkJoinPool.commonPool(); ``` ##### 定义ForkJoinTask 要利用Fork/Join框架,你需要定义自己的任务类,继承自`RecursiveTask<V>`(对于需要返回值的任务)或`RecursiveAction`(对于不需要返回值的任务)。在这些类中,你需要重写`compute()`方法,该方法定义了任务的分解逻辑和合并逻辑。 ```java public class SumTask extends RecursiveTask<Long> { private final long[] numbers; private final int start; private final int end; // 构造函数 public SumTask(long[] numbers, int start, int end) { this.numbers = numbers; this.start = start; this.end = end; } @Override protected Long compute() { int length = end - start; if (length < THRESHOLD) { // THRESHOLD为分解阈值 long sum = 0; for (int i = start; i < end; i++) { sum += numbers[i]; } return sum; } else { int mid = (start + end) / 2; SumTask leftTask = new SumTask(numbers, start, mid); SumTask rightTask = new SumTask(numbers, mid, end); leftTask.fork(); // 异步执行左子任务 long rightResult = rightTask.compute(); // 同步执行右子任务 long leftResult = leftTask.join(); // 等待左子任务完成并获取结果 return leftResult + rightResult; } } } ``` ##### 提交并执行任务 定义好任务后,你可以通过`ForkJoinPool`的`invoke`方法或`submit`方法来提交并执行任务。对于`RecursiveTask`,通常使用`invoke`方法,因为它会返回任务的执行结果。 ```java long[] numbers = {/* ... 大规模数组 ... */}; SumTask task = new SumTask(numbers, 0, numbers.length); long sum = ForkJoinPool.commonPool().invoke(task); System.out.println("Sum is: " + sum); ``` #### 注意事项与优化 尽管Fork/Join框架为并行计算提供了强大的支持,但在使用时仍需注意以下几点: - **任务分解阈值**:合理设置任务分解的阈值,以避免过细的任务划分导致的线程管理开销大于计算本身。 - **任务均衡性**:确保分解后的子任务工作量大致相等,以充分利用所有可用的计算资源。 - **避免共享数据**:尽量避免在任务间共享数据,以减少同步和锁的开销。如果必须共享,应谨慎设计同步机制。 - **资源竞争**:注意系统资源的竞争情况,如CPU、内存等,确保不会因为资源不足而影响任务的执行效率。 #### 实战案例 假设我们需要对一个非常大的数组进行排序,并计算排序后数组中所有元素的和。虽然排序本身并不是Fork/Join框架的直接应用场景(因为排序通常更适合使用归并排序等算法直接在多线程中实现),但我们可以通过Fork/Join框架来计算排序后数组的和,以展示其在实际问题中的应用。 首先,我们可以使用其他并行排序算法(如并行归并排序)对数组进行排序,然后利用Fork/Join框架计算排序后数组的和。这里,我们假设数组已经通过某种方式被排序。 ```java // 假设sortedNumbers是已排序的数组 SumTask sortedSumTask = new SumTask(sortedNumbers, 0, sortedNumbers.length); long sumOfSorted = ForkJoinPool.commonPool().invoke(sortedSumTask); ``` #### 总结 Fork/Join框架作为Java并发编程中的一颗璀璨明珠,以其独特的分而治之策略和工作窃取算法,为处理大规模数据集提供了高效、灵活的并行计算方案。通过合理使用Fork/Join框架,开发者可以轻松地编写出高性能的并行程序,充分利用现代多核处理器的计算能力。然而,值得注意的是,虽然Fork/Join框架为并行计算提供了强大的支持,但在使用时仍需注意任务分解的合理性、任务间的数据隔离以及系统资源的竞争情况,以确保程序的正确性和高效性。
上一篇:
25 | CompletionService:如何批量执行异步任务?
下一篇:
27 | 并发工具类模块热点问题答疑
该分类下的相关小册推荐:
SpringBoot合辑-初级篇
Java语言基础7-Java中的异常
Java语言基础14-枚举和注解
Java语言基础8-Java多线程
Java语言基础4-数组详解
Java必知必会-Maven初级
Java性能调优实战
Mybatis合辑2-Mybatis映射文件
Java语言基础12-网络编程
Java语言基础11-Java中的泛型
SpringBoot合辑-高级篇
Java语言基础10-Java中的集合