Java并发——Fork/Join框架

本文深入探讨Fork-Join框架的工作原理及其实现细节,包括如何通过Fork分解任务和Join收集结果来实现并行处理,同时介绍了ForkJoinPool的使用方法及其线程管理策略。

转载的一篇好文章(http://www.cnblogs.com/shijiaqi1066/p/4631466.html)。



fork-join框架

fork操作的作用是把一个大的问题划分成若干个较小的问题。在这个划分过程一般是递归进行的。直到可以直接进行计算。需要恰当地选取子问题的大小。太大的子问题不利于通过并行方式来提高性能,而太小的子问题则会带来较大的额外开销。每个子问题计算完成后,可以得到关于整个问题的部分解。join操作的作用是把这些分解手机组织起来,得到完整解。

简单的说,ForkJoin其核心思想就是分治。Fork分解任务,Join收集数据。

wpsCE77.tmp

 

在fork/join框架中,若某个子问题由于等待另一个子问题的完成而无法继续执行。那么处理该子问题的线程会主动寻找其他尚未运行完成的子问题来执行。这种方式减少了线程的等待时间,提高了性能。子问题中应该避免使用synchronized关键词或其他方式方式的同步。也不应该是一阻塞IO或过多的访问共享变量。在理想情况下,每个子问题的实现中都应该只进行CPU相关的计算,并且只适用每个问题的内部对象。唯一的同步应该只发生在子问题和创建它的父问题之间。

wpsCE88.tmp

 

 

Fork/Join框架的主要类

一个fork/join框架之下的任务由ForkJoinTask类表示。ForkJoinTask实现了Future接口,可以按照Future接口的方式来使用。在ForkJoinTask类中之重要的两个方法fork和join。fork方法用以一部方式启动任务的执行,join方法则等待任务完成并返回指向结果。在创建自己的任务是,最好不要直接继承自ForkJoinTask类,而要继承自ForkJoinTask类的子类RecurisiveTask或RecurisiveAction类。两种的区别在于RecurisiveTask类表示的任务可以返回结果,而RecurisiveAction类不行。

简单总结:

ForkJoin主要提供了两个主要的执行任务的接口。RecurisiveActionRecurisiveTask 

  • RecurisiveAction :没有返回值的接口。
  • RecurisiveTask :带有返回值的接口。

 

fork/join框架任务的执行由ForkJoinTask类的对象之外,还可以使用一般的Callable和Runnable接口来表示任务。

ForkJoin要利用线程池ForkJoinPool。每个线程池都有一个WorkQueue实例。ForkJoinPool推荐查看JDK8的源码,比JDK7更利于理解。

在ForkJoinPool类的对象中执行的任务大支可以分为两类,一类通过execute、invoke或submit提交的任务;另一类是ForkJoinTask类的对象在执行过程中产生的子任务,并通过fork方法来运行。一般的做法是表示整个问题的ForkJoinTask类的对象用第一类型是提交,而在执行过程中产生的子任务并不需要进行处理,ForkJoinPool类对象会负责子任务的执行。

wpsCE98.tmp

 

ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。使用方法与Executor框架类似。ForkJoinPool提供如下两个常用的构造器:

ForkJoinPool(int parallelism)  创建一个包含parallelism个并行线程的ForkJoinPool。

ForkJoinPool()  以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool。

ForkJoinPool有如下三个方法启动线程:

使用ForkJoinPool的submit(ForkJoinTask task) 或 invoke(ForkJoinTask task) 方法来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务。

  客户端非fork/join调用 内部调用fork/join
异步执行 execute(ForkJoinTask) ForkJoinTask.fork
等待获取结果 invoke(ForkJoinTask) ForkJoinTask.invoke
执行,获取Future

submit(ForkJoinTask)

ForkJoinTask.fork(ForkJoinTask are Futures)

ForkJoinTask是分支合并的执行任何,分支合并的业务逻辑使用者可以再继承了这个抽先类之后,在抽象方法exec()中实现。其中exec()的返回结果和ForkJoinPool的执行调用方(execute(...),invoke(...),submit(...)),共同决定着线程是否阻塞,具体请看下面的测试用例。

ForkJoinTask 是一个抽象类,它还有两个抽象子类:RecurisiveTask和RecurisiveAction。

RecurisiveTask代表有返回值的任务。RecursiveTask<T>是泛型类。T是返回值的类型。

RecurisiveAction代表没有返回值的任务。

 

 

异常处理

ForkJoinTask在执行的时候可能会抛出异常,但是没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。使用如下代码:

if(task.isCompletedAbnormally()) {
    System.out.println(task.getException());
}

getException方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null。

 

 

 

 

 

 

工作窃取原理

 

 

附上一个简单的代码:


import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;


public class CountTask extends RecursiveTask<Integer>
{
    private static final long serialVersionUID = -3611254198265061729L;
    
    public static final int threshold = 2;
    private int start;
    private int end;
    
    public CountTask(int start, int end)
    {
        this.start = start;
        this.end = end;
    }


    @Override
    protected Integer compute()
    {
        int sum = 0;
        
        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if(canCompute)
        {
            for (int i=start; i<=end; i++)
            {
                sum += i;
            }
        }
        else
        {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end)/2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle+1, end);
            
            // 执行子任务
            leftTask.fork();
            rightTask.fork();
            
            //等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
            
            //合并子任务
            sum = leftResult + rightResult;
    
        }
        
        return sum;
    }
    
    public static void main(String[] args)
    {
        ForkJoinPool forkjoinPool = new ForkJoinPool();
        
        //生成一个计算任务,计算1+2+3+4
        CountTask task = new CountTask(1, 100);
        
        //执行一个任务
        Future<Integer> result = forkjoinPool.submit(task);
        
        try
        {
            System.out.println(result.get());
        }
        catch(Exception e)
        {
            System.out.println(e);
        }
    }


}

<think>嗯,用户这次聚焦在Fork/Join框架的具体应用场景上,结合他之前对Java并发设计模式的整体询问,说明他正在系统性地学习并发编程。从引用内容看,用户已经接触过Fork/Join的基本概念(分治思想、工作窃取算法等),现在需要的是落地实践的指导。 用户可能正在面临实际开发中的性能优化问题,比如大数据处理或递归任务拆分。他特别强调"设计模式中的应用",暗示希望了解如何将框架融入架构设计而非单纯API使用。注意到他引用了ForkJoinPool和ForkJoinTask的类比关系(引用4),这提示需要强化组件协作的解说。 需要重点突出三点:第一是典型场景(如递归计算),第二是性能对比数据(引用5提到的线程池优化),第三是避免常见误区(比如引用3提到的异步模式误解)。对于源码解析部分保持克制,用户更关注应用层。 在案例设计上,选择斐波那契数列既能展示递归分治,又能暴露框架局限——当任务过轻时反而性能下降,这点必须警示。另补充JDK内部应用案例(如Arrays.parallelSort)增强说服力。 最后决策树要体现框架选型逻辑,尤其需要对比普通线程池。用户可能纠结"什么时候该用Fork/Join",这个必须量化说明(任务粒度>100μs的阈值参考Doug Lea的推荐值)。</think>### Java并发设计模式中Fork/Join框架的应用解析 --- #### 一、Fork/Join框架核心机制 Fork/Join框架Java 7引入的**分治任务并行处理框架**,核心组件括: 1. **ForkJoinPool** - 特殊线程池(工作窃取算法实现) - 默认线程数 = `Runtime.getRuntime().availableProcessors()` - 任务队列采用双端队列(支持LIFO/FIFO)[^3] 2. **ForkJoinTask** - 抽象任务基类(实际使用`RecursiveTask`/`RecursiveAction`) - `fork()`:异步执行子任务 - `join()`:获取子任务结果 ```mermaid graph TB MainTask[主任务] --> Fork[Fork分解] Fork --> SubTask1[子任务1] Fork --> SubTask2[子任务2] SubTask1 --> Join1[Join结果] SubTask2 --> Join2[Join结果] Join1 --> Merge[结果合并] Join2 --> Merge Merge --> FinalResult ``` --- #### 二、典型应用场景 1. **递归任务分解** ```java class FibonacciTask extends RecursiveTask<Integer> { final int n; FibonacciTask(int n) { this.n = n; } protected Integer compute() { if (n <= 1) return n; FibonacciTask f1 = new FibonacciTask(n - 1); f1.fork(); // 异步执行子任务 FibonacciTask f2 = new FibonacciTask(n - 2); return f2.compute() + f1.join(); // 合并结果 } } // 调用方式 ForkJoinPool pool = new ForkJoinPool(); int result = pool.invoke(new FibonacciTask(10)); ``` 2. **大数据处理** - 场景:大型数组排序/统计(如`Arrays.parallelSort()`底层实现) - 优势:自动任务拆分,充分利用多核CPU[^5] 3. **并行流计算** ```java // 使用并行流(底层基于Fork/Join) long count = list.parallelStream() .filter(s -> s.length() > 5) .count(); ``` --- #### 三、性能优化关键点 | 配置参数 | 优化建议 | 默认值 | |-------------------|-----------------------------------|------------------| | 并行度(parallelism) | $CPU_{core} \leq 并行度 \leq CPU_{core} \times 4$ | CPU核心数 | | 任务粒度 | 计算耗时 > 100μs才值得拆分[^2] | - | | 异步模式(asyncMode) | true时使用FIFO(适合事件型任务) | false(LIFO)[^3] | **工作窃取算法优势**: $$ \text{总耗时} \approx \frac{\text{最慢线程耗时}}{\text{线程数}} + \text{合并开销} $$ > 避免线程空闲,最大化CPU利用率 --- #### 四、VS传统线程池 | 特性 | ForkJoinPool | ThreadPoolExecutor | |---------------------|--------------------|--------------------| | 任务队列 | 工作窃取双端队列 | 阻塞队列 | | 任务调度 | 主动窃取任务 | 被动拉取任务 | | 适用场景 | 递归可分解任务 | 独立任务 | | 资源竞争 | 低(本地队列优先) | 高(全局队列竞争) | --- #### 五、最佳实践 1. **避免过度拆分** ```java protected Integer compute() { if (taskSize < THRESHOLD) { // 设置合理阈值 return sequentialCompute(); } // 否则拆分任务... } ``` 2. **结果合并优化** - 使用`invokeAll()`同步执行子任务 ```java protected Integer compute() { if (taskSize < THRESHOLD) return sequentialCompute(); SubTask left = new SubTask(part1); SubTask right = new SubTask(part2); invokeAll(left, right); // 优于分开fork() return left.join() + right.join(); } ``` 3. **异常处理** - 重写`getExceptionHandler()`处理未捕获异常 - 通过`isCompletedAbnormally()`检查任务状态 --- #### 六、框架选型决策树 ```mermaid graph TD A[任务类型] --> B{是否可递归分解?} B --> |是| C{单任务耗时>100μs?} C --> |是| D[使用ForkJoinPool] C --> |否| E[改用单线程] B --> |否| F{任务是否独立?} F --> |是| G[ThreadPoolExecutor] F --> |否| H[CompletableFuture组合] ``` > **注**:I/O密集型任务建议配合`CompletableFuture`使用[^1] --- ### 相关问题 1. Fork/Join框架在处理I/O阻塞任务时为何效率低下?如何优化? 2. 工作窃取算法在分布式系统中是否可行?存在哪些挑战? 3. 如何动态调整ForkJoinPool的并行度以适应系统负载变化? 4. ForkJoinTask与FutureTask在结果获取机制上有何本质区别? 5. 递归深度过大时,Fork/Join框架可能引发哪些问题?如何规避?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值