java中Fork-Join框架原理及应用

java中Fork-Join框架原理及应用

一、使用场景

大任务拆解成多个子任务,子任务还可以继续拆解成更小的子任务,最后将这些最小的子任务用多个线程并行执行,然后合并执行结果,例如,对超过1000万个元素的数组进行排序。

需求:有一个大数据量的List,现在需要根据List中每个对象model的id属性去另一个接口(接口只能接受一个id参数)获取这个对象的另外一个属性并set到这个对象中,很显然我们需要循环这个list,如果数据量很大,程序会变得非常慢,于是我们考虑用Fork-Join框架来处理(比线程池简单,不需要手动开启关闭线程池)。

二、基本思想

ForkJoin模型利用了分治算法的思想,将大任务不断拆解,多线程执行,最后合并结果。它的本质是一个线程池。

二、工作逻辑

每一个工作线程维护一个本地的双端队列用来存放任务。线程在运行的过程中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,从队尾取出任务来执行。当某个工作线程的本地队列为空时,它会尝试窃取一个任务,也叫工作窃取(可能是来自于刚刚提交到 pool 的任务,也可能是来自于其他工作线程的工作队列),窃取其他线程的工作队列的任务时,从队首取出,加到自己的队列中,然后执行下面两步:

  • 如果任务足够小就直接执行。
  • 否则将任务拆分成更小的子任务。

从上面的过程可以看出,Fork join并不是预先拆分所有任务,而是在执行时动态的决定拆分。

ForkJoin有三个比较重要的方法(操作)
1. fork:开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。
2. join:等待子任务的处理线程处理完毕,获得返回值。
3. compute:拆解并执行任务

代码:

ForkJoinPool forkJoinPool = new ForkJoinPool(4);
QTask task = new QTask(0, list.size(),list);
ForkJoinTask<List<Model>> submit = forkJoinPool.submit(task);
list2=(ArrayList<Model>) submit.get();
forkJoinPool.shutdown();

QTask.java

import java.util.List;
import java.util.concurrent.RecursiveTask;

public class QTask extends RecursiveTask {
    /**
     * 
     */
    private static final long serialVersionUID = 6253771003381008573L;
    // 分片阈值
    public static final int threshold = 100;
    private int start;
    private int end;
    private List<Model> list;
    public QTask(int start, int end, List<Model> list) {
        this.start = start;
        this.end = end;
        this.list = list;
    }

    /**
     */
    @Override
    protected List<Model> compute() {
        String threadName = Thread.currentThread().getName();
        // 小于阈值直接执行
        if (end - start <= threshold) {
            for (int i = start; i < end; i++) {
                        list.get(i).setTitle("测试"+i);
                    }
                } catch (NumberFormatException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (OspException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

        } else {
            // 递归拆解任务
            int middle = (start + end) / 2;
            QTask leftTask = new QTask(start, middle,list);
            QTask rightTask = new QTask(middle, end,list);
            invokeAll(leftTask, rightTask);
            // 等待计算完成并返回计算结果。
            leftTask.join();
            rightTask.join();
        }
        System.err.println(threadName + "==========start:" + start+"====================end:"+end);
        return list;
    }
}

为什么不用fork而用invokeAll?

这是因为执行compute()方法的线程本身也是一个Worker线程,当对两个子任务调用fork()时,这个Worker线程就会把任务分配给另外两个Worker,但是它自己却停下来等待不干活了(监工)!这样就白白浪费了Fork/Join线程池中的一个Worker线程,导致了4个子任务至少需要7个线程才能并发执行。 
而从invokeAll()方法的源码中发现,invokeAll的N个任务中,其中N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行,这样,就充分利用了线程池,保证没有空闲的不干活的线程。 

那么它和线程池有什么区别呢?

假设有一个包含4个线程的线程池,当一个任务到达时,我们把任务放到队列里。当一个线程空闲时,这个线程就从队头取一个待执行的任务并且执行,这种方式在任务大小类似的情况下效果很好,但是当任务大小不一致的时候呢?比如,有四个任务ABCD,其中有A任务所需要的执行时间非常长,而BCD任务是比较简单的,假设线程1拿到了A任务,线程234拿到了其余的任务,于是,线程234早早的就会执行完,处在空闲的状态,而A任务比较复杂,所以线程1则会执行较长时间。

故而,与线程池模型相比,ForkJoin模型有以下两个优点:

  • 自动完成任务到线程的映射,我们只关心任务的拆解机制、设置阈值以及小任务的执行。
  • 将大任务拆解,合理利用线程资源,提高效率,注意,子任务被分配到不同的核上执行时,效率最高。

猜你喜欢

转载自blog.csdn.net/jingshuiliushen_zj/article/details/81634257