ForkJoinPool的CountedCompleter源码分析

CountedCompleter直译是计数的完成器,其使用方式和原理相比RecursiveTask和RecursiveAction相对要复杂,因此案例不多,实际上源码中已经提供了较为详细的说明和使用案例,主要能实现在任务完成时触发一个特定的自定义操作。源码文档比较晦涩,直接分析源码。CountedCompleter新增了两个成员变量:

/**
*当前任务挂起的子任务数,ForkJoinTask的一大特色就是分治任务的拆分,一个大的任务拆分成多个子任务,
*子任务在进行细拆,因此任务和任务之间就形成了层级父子和祖先关系,当一个任务拆分成多个子任务时,可通过CountedCompleter的特定方法来维护
*其子任务数,pending值是当前任务需要等待完成的子任务数,当该值为0时意味着当前任务已经完成
*/
volatile int pending;
/**
*上面说到任务和任务之间具备层级父子和祖先关系,该值用于保存一个任务的父任务,因此可以* * 通过该变量来追溯其父任务和祖先任务
*/
final CountedCompleter<?> completer;

上面说到任务拆分后会形成层级关系,因此必然存在一个顶级任务(completer=null),其它子任务直接或间接的方法从该任务拆分而来。在来看一下该类的构造器

	//提供初始化pending和completer的构造器
    protected CountedCompleter(CountedCompleter<?> completer,
                               int initialPendingCount) {
        this.completer = completer;
        this.pending = initialPendingCount;
    }

    //仅初始化completer
    protected CountedCompleter(CountedCompleter<?> completer) {
        this.completer = completer;
    }

    //无参构造器,顶级任务通常使用该构造器
    protected CountedCompleter() {
        this.completer = null;
    }

开始分析CountedCompleter的其它源码之前先看一下RecursiveTask的实现,对比二者的区别可能更便于理解,RecursiveTask源码如下:

public abstract class RecursiveTask<V> extends ForkJoinTask<V> {
    private static final long serialVersionUID = 5232453952276485270L;
	//当前任务执行结果
    V result;
    //抽象出来的方法供用于自定义任务实现
    protected abstract V compute();
	//访问任务结果
    public final V getRawResult() {
        return result;
    }
	//设置任务结果
    protected final void setRawResult(V value) {
        result = value;
    }

    //实现了ForkJoinTask中的exec()方法并调用了抽象方法compute()来设置当前任务结果并返回true,
    //我们知道该方法返回true意味着当前任务已经完成,ForkJoinPool会将该任务标识成完成状态,具体见下方源码
    protected final boolean exec() {
        result = compute();
        return true;
    }
}

ForkJoinTask.doExec()源码,该方法将被ForkJoinPool的工作线程调用来执行任务

    final int doExec() {
        int s; boolean completed;
        if ((s = status) >= 0) {
            try {
                completed = exec();
            } catch (Throwable rex) {
                return setExceptionalCompletion(rex);
            }
            //如果exec()方法返回true则说明当前任务已经完成,设置任务的状态为正常完成状态。setCompletion方法会通知所有阻塞在该任务上的线程恢复运行获取可获取当前任务执行结果
            if (completed)
                s = setCompletion(NORMAL);
        }
        return s;
    }

那CountedCompleter是否也如RecursiveTask那样呢?先看一下CountedCompleter.exec方法,该方法同样被CountedCompleter实现并同抽象出一个compute()方法供用户自定义任务,与RecursiveTask的区别在于compute方法并没有返回执行结果,同时exec方法也没有返回true,从这里可以看出二者的根本区别:

  • 任务执行结果:CountedCompleter对任务执行结果并不由compute方法决定,其内部并没有定义result变量用于保存任务执行结果,同时getRawResult和setRawResult如下均为空方法。
  • exec方法返回false,意味着exec方法完成并不意味着当前任务已完成

那CountedCompleter如何实现的标识任务完成以及设置任务结果的?在前面我们提到了CountedCompleter中的一个pending变量和completer,实际上就是通过这两个变量来实现设置任务的完成的,对于任务的返回结果则相对需要自定义实现,因此使用CountedCompleter不太直观,其编码过程也不像RecursiveTask那样简单易懂。

    protected final boolean exec() {
        compute();
        
        return false;
    }
    
    public T getRawResult() { return null; }

    protected void setRawResult(T t) { }

上面说完了CountedCompleter和RecursiveTask二者的区别,在来了解以下其它的方法
onCompletion:本文开篇即说了CountedCompleter是一个在任务完成时触发特定动作的完成器,onCompletion即为该方法,方法为空用户可自定义实现

public void onCompletion(CountedCompleter<?> caller) {
}

onExceptionalCompletion:返回一个布尔值控制异常完成是否向祖先任务传播,默认需要传播,即当compute方法中发生异常时或通过completeExceptionally(Throwable)显示的设置当前任务为异常完成时是否希望祖先任务也异常完成。因此实际上该方法主要用于控制任务和任务之间的异常关系,当一个子任务发生异常不影响其它任务时该方法可以返回false,否则返回true让父任务也异常完成

public boolean onExceptionalCompletion(Throwable ex, CountedCompleter<?> caller) {
    return true;
}

getCompleter:返回当前任务的父任务

    public final CountedCompleter<?> getCompleter() {
        return completer;
    }

getPendingCount:获取当前任务待完成的子任务数量

    public final int getPendingCount() {
        return pending;
    }

setPendingCount:设置当前任务的待完成子任务书数量,通常在编写任务中会使用到该方法或设置pending属性的相关方法

    public final void setPendingCount(int count) {
        pending = count;
    }

addToPendingCount:原子的增加的pending数量,实际上对pending的操作均是原子性的,这也保证了多线程下各层级任务之间的安全型

    public final void addToPendingCount(int delta) {
        U.getAndAddInt(this, PENDING, delta);
    }

其它设置pending的方法将不在详说,从方法名上技能可知其含义,具体如下:

    public final boolean compareAndSetPendingCount(int expected, int count) {
        return U.compareAndSwapInt(this, PENDING, expected, count);
    }
    public final int decrementPendingCountUnlessZero() {
        int c;
        do {} while ((c = pending) != 0 &&
                     !U.compareAndSwapInt(this, PENDING, c, c - 1));
        return c;
    }

getRoot:获取子任务对应的顶级任务

    public final CountedCompleter<?> getRoot() {
        CountedCompleter<?> a = this, p;
        while ((p = a.completer) != null)
            a = p;
        return a;
    }

tryComplete:该方法常用于自定义任务的编码中,通过自旋操作保证将当前任务的pending数量-1,如果pending==0,则说明当前任务已经完成,执行上面说的钩子任务,则将其父任务的pending数量-1,如果父任务不存在,则说明当前任务已经是顶级任务,设置顶级任务为完成状态并通知阻塞在该任务上的其它线程。

  • 这里的实现相比RecrsiveTask任务,CountedCompleter任务通过递减pending属性来判断任务是否完成,子任务完成时会递减父任务的pending,直到顶级任务的pending减为0标志这任务完成。
    public final void tryComplete() {
        CountedCompleter<?> a = this, s = a;
        for (int c;;) {
            if ((c = a.pending) == 0) {
                a.onCompletion(s);
                if ((a = (s = a).completer) == null) {
                    s.quietlyComplete();
                    return;
                }
            }
            else if (U.compareAndSwapInt(a, PENDING, c, c - 1))
                return;
        }
    }

propagateCompletion:与tryComplete几乎一样,唯一区别在于该方法不调用钩子方法,仅传播给父任务子任务已完成让父任务pending-1.

    public final void propagateCompletion() {
        CountedCompleter<?> a = this, s = a;
        for (int c;;) {
            if ((c = a.pending) == 0) {
                if ((a = (s = a).completer) == null) {
                    s.quietlyComplete();
                    return;
                }
            }
            else if (U.compareAndSwapInt(a, PENDING, c, c - 1))
                return;
        }
    }

complete:设置当前任务完成状态并设置任务结果,此外调用了钩子方法

    public void complete(T rawResult) {
        CountedCompleter<?> p;
        //设置任务结果,如果需要返回结果
        setRawResult(rawResult);
        onCompletion(this);
        quietlyComplete();
        if ((p = completer) != null)
            p.tryComplete();
    }

quietlyCompleteRoot:上面3各方法都是一个递进型的方法,但在某些需求中当某个子任务完成时标记这整个任务完成则可用过该方法实现,比如在数组中找到一个符合要求的元素,任务拆分后只要有一个子线程找到这样的元素即说明任务完成

    public final void quietlyCompleteRoot() {
        for (CountedCompleter<?> a = this, p;;) {
            if ((p = a.completer) == null) {
                a.quietlyComplete();
                return;
            }
            a = p;
        }
    }

firstComplete:该方法常用于编码,不过其含义比较晦涩:如果当前任务已完成则返回当前任务,否则递减当前任务的pending值并返回null,通常当我们需要合并子任务结果的时候可以配合该方法来完成,而该方法通常需要配合nextComplete方法实现迭代合并子任务结果

    public final CountedCompleter<?> firstComplete() {
        for (int c;;) {
            if ((c = pending) == 0)
                return this;
            else if (U.compareAndSwapInt(this, PENDING, c, c - 1))
                return null;
        }
    }

nextComplete:同上,如果当前任务存在父任务,则返回父任务的firstComplete(),通过该方法实现子任务向父任务的结果的迭代合并

    public final CountedCompleter<?> nextComplete() {
        CountedCompleter<?> p;
        if ((p = completer) != null)
            return p.firstComplete();
        else {
            quietlyComplete();
            return null;
        }
    }

helpComplete:帮助完成任务,看名字和helpStrealer有些相同,实际上作用也差不多,当当前线程需要等待任务完成而阻塞等待时,实际上可以通过该方法帮助等待任务执行,上面说过ForkJoinPool默认parallelism=处理器核数-1,因此如果当前线程直接阻塞等待显然浪费cpu,通过该方法可以增加cpu利用率

    public final void helpComplete(int maxTasks) {
        Thread t; ForkJoinWorkerThread wt;
        if (maxTasks > 0 && status >= 0) {
            if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
                (wt = (ForkJoinWorkerThread)t).pool.
                    helpComplete(wt.workQueue, this, maxTasks);
            else
                ForkJoinPool.common.externalHelpComplete(this, maxTasks);
        }
    }

internalPropagateException:内部传播异常,该方法被称赞为Dog Lea的代码神迹,一个没有方法体的while循环,其含义如下:如果子任务发生异常需要传播到父任务(用户可自定义,默认需要传播)并且父任务还没有完成,则将异常传播给给父任务,循环迭代直到顶级任务位置。

    void internalPropagateException(Throwable ex) {
        CountedCompleter<?> a = this, s = a;
        while (a.onExceptionalCompletion(ex, s) &&
               (a = (s = a).completer) != null && a.status >= 0 &&
               a.recordExceptionalCompletion(ex) == EXCEPTIONAL)
            ;
    }

源码分析告一段落,开始编写实际例子来说明如何使用CountedCompleter,为了体现CountedCompleter的应用原理,这里编码不考虑实际性能,现有需求如下:

1. 一个任意类型的数组
2. 从该数组中根据自定义条件找到符合条件的子元素
3. 基ForkJoinPool多线程优势
4. 实现两种查找
	a. 只要找到符合一个符合条件的元素即终止查找并返回结果
	b.找到所有符合条件的元素并返回

代码实现如下:

public class Searcher {
	//查找数组中所有符合要求的元素并返回结果,继承自定义的BaseTask
    private static class SearchAllOccurrenceTask<E, R extends List<E>> extends BaseTask<E, R> {
    	//父任务拆分为2各子任务,父任务保存子任务引用,用于后续结果合并
        private SearchAllOccurrenceTask<E, R> left, right;
		//构造器
        public SearchAllOccurrenceTask(CountedCompleter<R> completer, E[] array, int lo, int hi, Predicate<E> matches) {
            super(completer, array, lo, hi, matches);
        }

        @Override
        public void compute() {
        	//拆分逻辑:直至任务拆分为只有两个元素为止,直接判断array[lo]是否符合条件
            if (hi - lo >= 2) {
            	//因为拆分成了2各子任务,应该将挂起任务数设置为2,但实际上当前任务没有执行pending-1,直接通过子任务递减该任务的pending,因此直接设置为1即可
                setPendingCount(1);
                int mid = hi + lo >>> 1;
                SearchAllOccurrenceTask l = new SearchAllOccurrenceTask(this, array, lo, mid, matches);
                SearchAllOccurrenceTask r = new SearchAllOccurrenceTask(this, array, mid, hi, matches);
                //当前任务保存子任务的引用
                this.left = l;
                this.right = r;
                //将子任务提交到当工作队列
                l.fork();
                r.fork();
            } else {
            	//否则说明不再拆分任务开始执行具体任务逻辑,如果符合要求,则创建List保存结果,因为拆分逻辑只可能有一个或0个符合要求,因此创建集合大小为1即可
                if (matches.test(array[lo])) {
                    if (result == null) {
                        result = (R) new ArrayList<E>(1);
                        result.add(array[lo]);
                    }
                } else {
                	//否则执行钩子函数
                    onCompletion(this);
                }
                //通过nextComplete方法返回非空确定父任务已完成,实际上当兄弟任务任意一个完成时会将父任务的pending-1置为0,
                //此时当前任务在执行nextComplete时将返回父任务
                //将当前任务和其兄弟任务的结果合并到父任务,实际上for循环内部的逻辑只可能被left或right二者之一执行,因此不存在并发安全问题
                //合并完成后循环向祖先节点直至顶级任务进行合并
                for (SearchAllOccurrenceTask<E, R> c = (SearchAllOccurrenceTask) nextComplete(); c != null; c = (SearchAllOccurrenceTask) c.nextComplete()) {
                    List<E> lr = c.left.result;
                    List<E> rr = c.right.result;
                    int size = (lr == null ? 0 : lr.size()) + (rr == null ? 0 : rr.size());
                    if (size > 0) {
                        c.result = (R) new ArrayList(size);
                        if (lr != null) {
                            c.result.addAll(lr);
                        }
                        if (rr != null) {
                            c.result.addAll(rr);
                        }
                    }
                }
            }
        }
    }
	
	//查找数组中符合条件的元素,找到一个即返回
    private static class SearchOnceTask<E> extends BaseTask<E, E> {
        public SearchOnceTask(CountedCompleter<E> completer, E[] array, int lo, int hi, Predicate<E> matches) {
            super(completer, array, lo, hi, matches);
        }

        @Override
        public void compute() {
        	//判断顶级任务是否完成,若已完成则不在执行任务
            if (!getRoot().isDone()){
                if (hi - lo >= 2) {
                    setPendingCount(1);
                    int mid = hi + lo >>> 1;
                    SearchOnceTask l = new SearchOnceTask(this, array, lo, mid, matches);
                    SearchOnceTask r = new SearchOnceTask(this, array, mid, hi, matches);
                    l.fork();
                    r.fork();
                } else {
                	//如何元素符合条件,直接将元素的值设置为顶级任务的结果并将顶级任务的结果设置为完成
                    if (matches.test(array[lo])) {
                        ((BaseTask) getRoot()).result = array[lo];
                        quietlyCompleteRoot();
                    } else {
                    	//否则仅完成当前任务并尝试递减父任务pending值
                        tryComplete();
                    }

                }
            }
        }
    }
	//任务基类,继承CountedCompleter,主要是因为任务的数据结构相同而拆分和并逻辑不同,因此将抽取相同部分形成基类
    private static abstract class BaseTask<E, R> extends CountedCompleter<R> {
        E[] array;
        final int lo, hi;
        Predicate<E> matches;
        R result;

        public BaseTask(CountedCompleter<R> completer, E[] array, int lo, int hi, Predicate<E> matches) {
            super(completer);
            this.array = array;
            this.lo = lo;
            this.hi = hi;
            this.matches = matches;
        }
		//必须要覆盖原方法,原方法返回null
        @Override
        public R getRawResult() {
            return result;
        }
		
		//钩子方法,输入子任务中不符合条件的元素,这里仅对叶子任务进行判断,父任务和祖先任务不输出
		//测试小数组时可以开启查看输出结果,大数组避免刷屏关闭
        /*@Override
        public void onCompletion(CountedCompleter<?> caller) {
            if (this instanceof SearchOnceTask && hi - lo < 2) {
                System.out.println("SearchOnceTask不符合条件的结果:" + array[lo] + " lo=" + lo + ",hi=" + hi);
            } else if (this instanceof SearchAllOccurrenceTask && ((SearchAllOccurrenceTask) this).left == null) {
                System.out.println("SearchAllOccurrenceTask不符合条件的结果:" + array[lo] + " lo=" + lo + ",hi=" + hi);

            }
        }*/
    }
	//静态工具方法类:查找符合条件的元素,找到一个立即返回
    public static <E, R> R searchOnce(E[] array, Predicate<E> matches) {
        return (R) new SearchOnceTask<>(null, array, 0, array.length, matches).invoke();
    }
	
	//静态工具方法类:查找符合条件的元素,返回所有符合结果的元素
    public static <E, R> R searchAllOccurrence(E[] array, Predicate<E> matches) {
        return (R) new SearchAllOccurrenceTask<>(null, array, 0, array.length, matches).invoke();
    }

    public static void main(String[] args) {
        Object[] arr = IntStream.range(1, 1000000).mapToObj(e -> e).toArray();
        long s = System.currentTimeMillis();
        System.out.println("查找首个符合条件的结果:" + searchOnce(arr, e -> (int) e < 299999 && (int)e > 299990)+",耗时:"+(System.currentTimeMillis() - s));
        s = System.currentTimeMillis();
        System.out.println("查找所有符合条件的结果:" + searchAllOccurrence(arr, e -> (int) e < 299999 && (int)e > 299990)+",耗时:"+(System.currentTimeMillis() - s));
    }
}

测试结果如下:

查找首个符合条件的结果:299998,耗时:142
查找所有符合条件的结果:[299991, 299992, 299993, 299994, 299995, 299996, 299997, 299998],耗时:1161

实际上上述代码可以进行适当优化,主要优化点在于减少子任务的fork,这里仅对SearchAllOccurrenceTask进行优化,优化后的代码如下:

public class Searcher {
    private static class SearchAllOccurrenceTask<E, R extends List<E>> extends BaseTask<E, R> {
		//fork标识当前任务的子任务,next标识兄弟任务
		//fork只有当任务进行拆分时才会赋值,叶子任务没有子任务,next属性只有当任务拆分2次以上时子任务任务才有值,
        private SearchAllOccurrenceTask<E, R> fork, next;

        public SearchAllOccurrenceTask(CountedCompleter<R> completer, E[] array, int lo, int hi, Predicate<E> matches, SearchAllOccurrenceTask<E, R> next) {
            super(completer, array, lo, hi, matches);
            this.next = next;
        }

        @Override
        public void compute() {
            int h = hi, l = lo;
            while (h > l) {
                if (h - l >= 2) {
                	//每拆分一次将挂起数+1
                    addToPendingCount(1);
                    int mid = h + l >>> 1;
                    //循环覆盖fork属性为最新的fork子任务,同时将上一个fork值赋值给当前fork子任务的next属性构成任务链
                    (fork = new SearchAllOccurrenceTask(this, array, mid, h, matches, fork)).fork();
                    h = mid;
                } else {
                    if (matches.test(array[lo])) {
                        if (result == null) {
                            result = (R) new ArrayList<E>(1);
                            result.add(array[lo]);
                        }
                    } else {
                        onCompletion(this);
                    }
                    //外层循环向父以及祖先任务遍历合并结果,内存循环横向合并任务链结果
                    for (SearchAllOccurrenceTask<E, R> c = (SearchAllOccurrenceTask) firstComplete(); c != null; c = (SearchAllOccurrenceTask) c.nextComplete()) {
                        for (SearchAllOccurrenceTask<E, R> f = c.fork; f != null; f = c.fork = f.next) {
                            List<E> fResult = f.result;
                            List<E> cResult = c.result;
                            if (fResult != null) {
                                if (cResult == null) {
                                    cResult = fResult;
                                    c.result = (R) cResult;
                                } else
                                    cResult.addAll(fResult);
                            }
                        }
                    }
                    break;
                }
            }
        }
    }

    private static class SearchOnceTask<E> extends BaseTask<E, E> {
        public SearchOnceTask(CountedCompleter<E> completer, E[] array, int lo, int hi, Predicate<E> matches) {
            super(completer, array, lo, hi, matches);
        }

        @Override
        public void compute() {
            if (!getRoot().isDone()) {
                if (hi - lo >= 2) {
                    setPendingCount(1);
                    int mid = hi + lo >>> 1;
                    SearchOnceTask l = new SearchOnceTask(this, array, lo, mid, matches);
                    SearchOnceTask r = new SearchOnceTask(this, array, mid, hi, matches);
                    l.fork();
                    r.fork();
                } else {
                    if (matches.test(array[lo])) {
                        ((BaseTask) getRoot()).result = array[lo];
                        quietlyCompleteRoot();
                    } else {
                        tryComplete();
                    }

                }
            }
        }
    }

    private static abstract class BaseTask<E, R> extends CountedCompleter<R> {
        E[] array;
        final int lo, hi;
        Predicate<E> matches;
        R result;

        public BaseTask(CountedCompleter<R> completer, E[] array, int lo, int hi, Predicate<E> matches) {
            super(completer);
            this.array = array;
            this.lo = lo;
            this.hi = hi;
            this.matches = matches;
        }

        @Override
        public R getRawResult() {
            return result;
        }

    }

    public static <E, R> R searchOnce(E[] array, Predicate<E> matches) {
        return (R) new SearchOnceTask<>(null, array, 0, array.length, matches).invoke();
    }

    public static <E, R> R searchAllOccurrence(E[] array, Predicate<E> matches) {
        return (R) new SearchAllOccurrenceTask<>(null, array, 0, array.length, matches, null).invoke();
    }

    public static void main(String[] args) {
        Object[] arr = IntStream.range(1, 1000000).mapToObj(e -> e).toArray();
        long s = System.currentTimeMillis();
        System.out.println("查找首个符合条件的结果:" + searchOnce(arr, e -> (int) e > 0 && (int) e < 299999 && (int) e > 299990) + ",耗时:" + (System.currentTimeMillis() - s));
        s = System.currentTimeMillis();
        System.out.println("查找所有符合条件的结果:" + searchAllOccurrence(arr, e -> (int) e > 0 && (int) e < 299999 && (int) e > 299990) + ",耗时:" + (System.currentTimeMillis() - s));
    }
}

优化后的测试结果如下,发现耗时结果大大降低,甚至比查找首个符合条件的结果还要小,究其原因包含两个方面

  • 循环拆分任务入队,无需再进行入队后窃取或再次出队消耗
  • 结果合并通过构建任务链直接通过循环结构合并
查找首个符合条件的结果:299998,耗时:189
查找所有符合条件的结果:[299991, 299992, 299993, 299994, 299995, 299996, 299997, 299998],耗时:145

上述测试为100万个元素,当元素个数上升到1000万个时效率差距更加明显:
结果如下:可见合适的任务拆分和结果合并对性能影响相对较大

查找首个符合条件的结果:299998,耗时:715
查找所有符合条件的结果:[299991, 299992, 299993, 299994, 299995, 299996, 299997, 299998],耗时:397
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值