Java多线程 -- JUC包源码分析19 -- ForkJoinPool/ForkJoinTask

JDK 1.7 提供了一个并行计算的框架,本文就来分析一下它在使用和实现原理上,和ThreadPool有什么区别。

何为Fork/Join?

我们在大学算法课本上,学过的一种基本算法就是:分治。其基本思路就是:把一个大的任务分成若干个子任务,这些子任务分别计算,最后再Merge出最终结果。这个过程通常都会用到递归。

而Fork/Join其实就是一种利用多线程来实现“分治算法”的并行框架。

另外一方面,可以把Fori/Join看作一个单机版的Map/Reduce,只不过这里的并行不是多台机器并行计算,而是多个线程并行计算。

下面看2个简单例子:

例子1: 快排
我们都知道,快排有2个步骤:
第1步,拿数组的第1个元素,把元素划分成2半,左边的比该元素小,右边的比该元素大;
第2步,对左右的2个子数组,分别排序。

可以看出,这里左右2个子数组,可以相互独立的,并行计算。因此可以利用ForkJoin框架, 代码如下:

//定义一个Task,基础自RecursiveAction,实现其compute方法
class SortTask extends RecursiveAction {
    final long[] array;
    final int lo;
    final int hi;
    private int THRESHOLD = 0; //For demo only

    public SortTask(long[] array) {
        this.array = array;
        this.lo = 0;
        this.hi = array.length - 1;
    }

    public SortTask(long[] array, int lo, int hi) {
        this.array = array;
        this.lo = lo;
        this.hi = hi;
    }

    protected void compute() {
        if (hi - lo < THRESHOLD)
            sequentiallySort(array, lo, hi);
        else {
            int pivot = partition(array, lo, hi);  //划分
            coInvoke(new SortTask(array, lo, pivot - 1), new SortTask(array,
                    pivot + 1, hi));  //递归调,左右2个子数组
        }
    }

    private int partition(long[] array, int lo, int hi) {
        long x = array[hi];
        int i = lo - 1;
        for (int j = lo; j < hi; j++) {
            if (array[j] <= x) {
                i++;
                swap(array, i, j);
            }
        }
        swap(array, i + 1, hi);
        return i + 1;
    }

    private void swap(long[] array, int i, int j) {
        if (i != j) {
            long temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }

    private void sequentiallySort(long[] array, int lo, int hi) {
        Arrays.sort(array, lo, hi + 1);
    }
}


//测试函数
    public void testSort() throws Exception {
        ForkJoinTask sort = new SortTask(array);   //1个任务
        ForkJoinPool fjpool = new ForkJoinPool();  //1个ForkJoinPool
        fjpool.submit(sort); //提交任务
        fjpool.shutdown(); //结束。ForkJoinPool内部会开多个线程,并行上面的子任务

        fjpool.awaitTermination(30, TimeUnit.SECONDS);
    }

例子2: 求1到n个数的和

//定义一个Task,基础自RecursiveTask,实现其commpute方法
public class SumTask extends RecursiveTask<Long>{  
    private static final int THRESHOLD = 10;  

    private long start;  
    private long end;  

    public SumTask(long n) {  
        this(1,n);  
    }  

    private SumTask(long start, long end) {  
        this.start = start;  
        this.end = end;  
    }  

    @Override  //有返回值
    protected Long compute() {  
        long sum = 0;  
        if((end - start) <= THRESHOLD){  
            for(long l = start; l <= end; l++){  
                sum += l;  
            }  
        }else{  
            long mid = (start + end) >>> 1;  
            SumTask left = new SumTask(start, mid);   //分治,递归
            SumTask right = new SumTask(mid + 1, end);  
            left.fork();  
            right.fork();  
            sum = left.join() + right.join();  
        }  
        return sum;  
    }  
    private static final long serialVersionUID = 1L;  
}  

//测试函数
    public void testSum() throws Exception {
        SumTask sum = new SumTask(100);   //1个任务
        ForkJoinPool fjpool = new ForkJoinPool();  //1个ForkJoinPool
        Future<Long> future = fjpool.submit(sum); //提交任务
        Long r = future.get(); //获取返回值
        fjpool.shutdown(); 

    }

与ThreadPool的区别

通过上面例子,我们可以看出,它在使用上,和ThreadPool有共同的地方,也有区别点:
(1) ThreadPool只有“外部任务”,也就是调用者放到队列里的任务。 ForkJoinPool有“外部任务”,还有“内部任务”,也就是任务自身在执行过程中,分裂出”子任务“,递归,再次放入队列。
(2)ForkJoinPool里面的任务通常有2类,RecusiveAction/RecusiveTask,这2个都是继承自FutureTask。在使用的时候,重写其compute算法。

工作窃取算法

上面提到,ForkJoinPool里有”外部任务“,也有“内部任务”。其中外部任务,是放在ForkJoinPool的全局队列里面,而每个Worker线程,也有一个自己的队列,用于存放内部任务。

窃取的基本思路就是:当worker自己的任务队列里面没有任务时,就去scan别的线程的队列,把别人的任务拿过来执行。

//ForkJoinPool的成员变量
ForkJoinWorkerThread[] workers;  //worker thread集合
private ForkJoinTask<?>[] submissionQueue; //外部任务队列
private final ReentrantLock submissionLock; 

//ForkJoinWorkerThread的成员变量
ForkJoinTask<?>[] queue;   //每个worker线程自己的内部任务队列

//提交任务
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {  
    if (task == null)  
        throw new NullPointerException();  
    forkOrSubmit(task);  
    return task;  
} 

private <T> void forkOrSubmit(ForkJoinTask<T> task) {  
    ForkJoinWorkerThread w;  
    Thread t = Thread.currentThread();  
    if (shutdown)  
        throw new RejectedExecutionException();  
    if ((t instanceof ForkJoinWorkerThread) &&   //如果当前是worker线程提交的任务,也就是worker执行过程中,分裂出来的子任务,放入worker自己的内部任务队列
        (w = (ForkJoinWorkerThread)t).pool == this)  
        w.pushTask(task);  
    else  
        addSubmission(task);  //外部任务,放入pool的全局队列
}   

//worker的run方法
public void run() {  
    Throwable exception = null;  
    try {  
        onStart();  
        pool.work(this);  
    } catch (Throwable ex) {  
        exception = ex;  
    } finally {  
        onTermination(exception);  
    }  
}  

final void work(ForkJoinWorkerThread w) {  
    boolean swept = false;                // true on empty scans 
    long c;  
    while (!w.terminate && (int)(c = ctl) >= 0) {  
        int a;                            // active count 
        if (!swept && (a = (int)(c >> AC_SHIFT)) <= 0)  
            swept = scan(w, a);   //核心代码都在这个scan函数里面
        else if (tryAwaitWork(w, c))  
            swept = false;  
    }  
}  

//scan的基本思路:从别人的任务队列里面抢,没有,再到pool的全局的任务队列里面去取。
private boolean scan(ForkJoinWorkerThread w, int a) {  
    int g = scanGuard;   

    int m = (parallelism == 1 - a && blockedCount == 0) ? 0 : g & SMASK;  
    ForkJoinWorkerThread[] ws = workers;  
    if (ws == null || ws.length <= m)         // 过期检测 
        return false;  

    for (int r = w.seed, k = r, j = -(m + m); j <= m + m; ++j) {  
        ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;  
        //随机选出一个牺牲者(工作线程)。 
        ForkJoinWorkerThread v = ws[k & m];  
        //一系列检查... 
        if (v != null && (b = v.queueBase) != v.queueTop &&  
            (q = v.queue) != null && (i = (q.length - 1) & b) >= 0) {  
            //如果这个牺牲者的任务队列中还有任务,尝试窃取这个任务。 
            long u = (i << ASHIFT) + ABASE;  
            if ((t = q[i]) != null && v.queueBase == b &&  
                UNSAFE.compareAndSwapObject(q, u, t, null)) {  
                //窃取成功后,调整queueBase 
                int d = (v.queueBase = b + 1) - v.queueTop;  
                //将牺牲者的stealHint设置为当前工作线程在pool中的下标。 
                v.stealHint = w.poolIndex;  
                if (d != 0)  
                    signalWork();             // 如果牺牲者的任务队列还有任务,继续唤醒(或创建)线程。 
                w.execTask(t); //执行窃取的任务。 
            }  
            //计算出下一个随机种子。 
            r ^= r << 13; r ^= r >>> 17; w.seed = r ^ (r << 5);  
            return false;                     // 返回false,表示不是一个空扫描。 
        }  
        //前2*m次,随机扫描。 
        else if (j < 0) {                     // xorshift 
            r ^= r << 13; r ^= r >>> 17; k = r ^= r << 5;  
        }  
        //后2*m次,顺序扫描。 
        else  
            ++k;  
    }  
    if (scanGuard != g)                       // staleness check 
        return false;  
    else {                                     
        //如果扫描完毕后没找到可窃取的任务,那么从Pool的提交任务队列中取一个任务来执行。 
        ForkJoinTask<?> t; ForkJoinTask<?>[] q; int b, i;  
        if ((b = queueBase) != queueTop &&  
            (q = submissionQueue) != null &&  
            (i = (q.length - 1) & b) >= 0) {  
            long u = (i << ASHIFT) + ABASE;  
            if ((t = q[i]) != null && queueBase == b &&  
                UNSAFE.compareAndSwapObject(q, u, t, null)) {  
                queueBase = b + 1;  
                w.execTask(t);  
            }  
            return false;  
        }  
        return true;                         // 如果所有的队列(工作线程的任务队列和pool的任务队列)都是空的,返回true。 
    }  
}  

关于ForkJoinPool/FutureTask,本文只是分析了其基本使用原理。还有很多实现细节,留待读者自己去分析。

    原文作者:JUC
    原文地址: https://blog.csdn.net/chunlongyu/article/details/52518502
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞