ForkJoinPool 应用和原理

ForkJoinPool 类是位于 java.util.concurrent 包下的一个类。它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。

一、应用

1. 累加
import java.util.*;
import java.util.concurrent.*;

public class ForkJoinSum {

    
    /**
     * 这是一个简单的Join/Fork计算过程,将1—1001数字相加
     */

    private static final Integer MAX = 200;
 
    static class MySumTask extends RecursiveTask<Integer> {
        // 子任务开始计算的值
        private Integer startValue;
 
        // 子任务结束计算的值
        private Integer endValue;
 
        public MySumTask(Integer startValue , Integer endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }
 
        @Override
        protected Integer compute() {
            // 如果条件成立,说明这个任务所需要计算的数值分为足够小了
            // 可以正式进行累加计算了
            if(endValue - startValue < MAX) {
                System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
                Integer totalValue = 0;
                for(int index = this.startValue ; index <= this.endValue  ; index++) {
                    totalValue += index;
                }
                return totalValue;
            }
            // 否则再进行任务拆分,拆分成两个任务
            else {
                MySumTask subTask1 = new MySumTask(startValue, (startValue + endValue) / 2);
                subTask1.fork();
                MySumTask subTask2 = new MySumTask((startValue + endValue) / 2 + 1 , endValue);
                subTask2.fork();
                return subTask1.join() + subTask2.join();
            }
        }
    }
 
    public static void main(String[] args) {
        // 这是Fork/Join框架的线程池
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Integer> taskFuture =  pool.submit(new MySumTask(1,1001));
        try {
            Integer result = taskFuture.get();
            System.out.println("result = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace(System.out);
        }
    }

}

运行得到的结果是:

result = 501501

首先是通过 ForkJoinPool 来提交子任务 MySumTask 得到一个 ForkJoinTask 对象,然后调用 forkJoinTask.get() 方法开启任务。这个子任务 MySumTask 就是自定义的求和子任务,继承自 RecursiveTask,重写它的 compute() 方法,在这个方法里面进行具体的求和逻辑:如果起始和末尾间隔大于阈值,则继续分割区间并创建子任务调用 fork() 方法,然后返回子任务的 join() 方法执行结果;否则对这个区间内所有的数求和并返回结果。

RecursiveTask 和 RecursiveAction:

  • RecursiveTask:递归有返回值的ForkJoinTask子类;
  • RecursiveAction:递归无返回值的ForkJoinTask子类;
2. 排序
import java.util.*;
import java.util.concurrent.*;

public class ForkJoinDemo {

    private static int MAX = 100;
 
    private static int inits[] = new int[MAX];

    static {
        Random r = new Random();
        for(int index = 1 ; index <= MAX ; index++) {
            inits[index - 1] = r.nextInt(100);
        }
    }

    public static void main(String[] args) throws Exception {   
        // 正式开始
        long beginTime = System.currentTimeMillis();
        ForkJoinPool pool = new ForkJoinPool();
        MySortTask task = new MySortTask(inits);
        ForkJoinTask<int[]> taskResult = pool.submit(task);
        try {
            System.out.println(Arrays.toString(taskResult.get()));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace(System.out);
        }
        long endTime = System.currentTimeMillis();

        
        System.out.println("耗时=" + (endTime - beginTime));      
    }
 
    /**
     * 单个排序的子任务
     * @author yinwenjie
     */
    static class MySortTask extends RecursiveTask<int[]> {
 
        private int source[];
 
        public MySortTask(int source[]) {
            this.source = source;
        }
 
        /* (non-Javadoc)
         * @see java.util.concurrent.RecursiveTask#compute()
         */
        @Override
        protected int[] compute() {
            int sourceLen = source.length;
            // 如果条件成立,说明任务中要进行排序的集合还不够小
            if(sourceLen > 2) {
                int midIndex = sourceLen / 2;
                // 拆分成两个子任务
                MySortTask task1 = new MySortTask(Arrays.copyOf(source, midIndex));
                task1.fork(); //要调用 fork() 方法
                MySortTask task2 = new MySortTask(Arrays.copyOfRange(source, midIndex , sourceLen));
                task2.fork();
                // 将两个有序的数组,合并成一个有序的数组,
                int result1[] = task1.join(); //要调用 join() 方法
                int result2[] = task2.join();
                int mer[] = joinInts(result1 , result2);
                return mer; //返回排序后的数组
            } 
            // 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
            else {
                // 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
                if(sourceLen == 1
                    || source[0] <= source[1]) {
                    return source;
                } else {
                    int targetp[] = new int[sourceLen];
                    targetp[0] = source[1];
                    targetp[1] = source[0];
                    return targetp;
                }
            }
        }
 
        /**
         * 这个方法用于合并两个有序集合
         * @param array1
         * @param array2
         */
        private static int[] joinInts(int array1[] , int array2[]) {
            int destInts[] = new int[array1.length + array2.length];
            int array1Len = array1.length;
            int array2Len = array2.length;
            int destLen = destInts.length;
    
            // 只需要以新的集合destInts的长度为标准,遍历一次即可
            for(int index = 0 , array1Index = 0 , array2Index = 0 ; index < destLen ; index++) {
                int value1 = array1Index >= array1Len?Integer.MAX_VALUE:array1[array1Index];
                int value2 = array2Index >= array2Len?Integer.MAX_VALUE:array2[array2Index];
                // 如果条件成立,说明应该取数组array1中的值
                if(value1 < value2) {
                    array1Index++;
                    destInts[index] = value1;
                }
                // 否则取数组array2中的值
                else {
                    array2Index++;
                    destInts[index] = value2;
                }
            }
    
            return destInts;
        }
    }
   
}

运行得到的结果是:

[1, 2, 2, 2, 4, 6, 7, 7, 8, 9, 11, 14, 14, 14, 15, 16, 17, 19, 20, 21, 23, 23, 24, 26, 28, 29, 33, 33, 33, 34, 35, 37, 38, 39, 40, 42, 44, 44, 44, 47, 48, 48, 48, 49, 49, 50, 51, 52, 53, 53, 53, 54, 54, 55, 56, 56, 56, 57, 58, 58, 59, 59, 60, 61, 62, 62, 63, 65, 67, 68, 68, 69, 70, 70, 72, 72, 73, 73, 75, 75, 77, 77, 78, 78, 81, 81, 82, 82, 83, 87, 88, 88, 89, 90, 92, 93, 98, 99, 99, 99]
耗时=29

同样重点是这个 MySortTask 子任务,看它的 compute() 方法:如果子任务的排序区间长度大于2则继续分割成两个子区间并对这两个子区间执行排序,然后合并两个区间的元素;否则如果区间只有一个元素直接返回,如果有两个元素则对两个元素排序并返回。这其实就是归并排序的思想。

注意合并两个区间的元素遍历两个数组,从中取出头部较小的值放入已排好序的结果中。

其实上面这两种应用场景也可以使用 ExecutorService + Callable 来实现,不过这样就要手动去计算并分割子任务数量。没有 ForkJoinPool 来的方便。此外还可以使用并行流(JDK8+,并行流底层还是Fork/Join框架,只是任务拆分优化得很好)来实现:

public static void main(String[] args) {
    Instant start = Instant.now();
    long result = LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum);
    Instant end = Instant.now();
    System.out.println("耗时:" + Duration.between(start, end).toMillis() + "ms");
    System.out.println("结果为:" + result); // 打印结果500500

}

二、原理

ForkJoin主要适用于:计算密集型任务,且任务可被分解执行。不建议使用Block IO以及额外的同步、锁等,这种阻塞操作,会导致ForkJoin的并发能力受限,而且严重时会导致框架的线程池调度失效。

特性:

  • ForkJoinPool内部仍为线程池模式,且没有调度线程。
  • Pool中在存取任务、worker线程执行和stealing任务时,几乎没有使用到任何的显式锁,其内部主要使用Unsafe + CAS;所有标记状态、计数等可能被并发访问的字段,都经过CAS操作。尽管JDK中已经存在很多并发API,比如ArrayBlockingQueue等,但是ForkJoinPool并没有直接使用它们,而是继续CAS重建了相关语义。
  • 此外,因为ForkJoin中没有调度线程,比如均衡分配任务、唤醒等Master线程;所以所有的提交任务的线程、Worker线程,都会在各自的时机上,承担一定的调度职能,worker线程的stealing机制潜在引入并发问题,ForkJoinPool的设计巧妙之处,就是在没有使用Lock的情况下,能够达到并发安全。通过源码发现,Pool中很多实现,都是刻意避免并发(线程的join),而不是lock来兼容和支持并发。
  • ForkJoinPool框架中,并没有太多的、复杂的数据结构,用于保存比如线程池状态、任务列表和状态等。而是使用简单类型的字段,通过位运算来实现很多的状态标记,通过使用CAS原子性更新实现类似于锁状态(qlock)。
工作窃取算法(work-stealing)

大任务被分割为独立的子任务,并且子任务分别放到不同的队列里,并为每个队列创建一个线程来执行队列里的任务。假设线程A优先把分配到自己队列里的任务执行完毕,此时如果线程E对应的队列里还有任务等待执行,空闲的线程A会窃取线程E队列里任务执行,并且为了减少窃取任务时线程A和被窃取任务线程E之间的发生竞争,窃取任务的线程A会从队列的尾部获取任务执行,被窃取任务线程E会从队列的头部获取任务执行。

工作窃取算法(work-stealing)的优点:线程间的竞争很少,充分利用线程进行并行计算,但是在任务队列里只有一个任务时,也可能会存在竞争情况。

ForkJoinPool:

内部使用使用一个可以按需扩容的数组表示线程池:WorkQueue[] workerQueues,此数据总是2倍扩容,其实际最大容量与“ parallelism”保持一致。WorkQueue实例中持有一个Worker线程和Task队列,可以简单认为一个WorkQueue就是一个工作线程单元;

  • 在任务提交时(submit/execute),如果workQueues尚未达到“并发级别”,即当前线程池未满,则直接创建新的WorkerQueue(worker线程)并由其执行当前任务。此时还有workerQueues扩容问题。
  • 如果workerQueues容量达到阈值(即线程池满),将随机(ThreadLocalRandom)从workerQueues中找个workQueue并将此task添加到其任务队列中,之所以随机查找workQueues,也是为了避免并发提交时,submission线程并发操作workQueues的概率;有一种特殊的情况,就是随机找到的这个workQueue可能为null,因为此workQueue持有的工作线程因为各种原因(比如空闲超时,异常退出)被注销了(deregister),此时将会创建一个新的WorkQueue实例并补偿在此位置。
  • ForkJoinPool内部并没有scheduler线程。
  • 我们可以通过shutdown/shutdownNow来关闭Pool,此后Pool将不会接收新的submission;此时将会触发所有worker线程的退出操作(deregister):修改线程状态、唤醒阻塞、清空本地任务列表等。
WorkQueue:
  • 逻辑上的worker线程,内部持有实际的 ForkJoinWorkerThreadTask 列表。同时,还持有pool的引用。
  • ForkJoinWorkerThread 工作线程,类似于线程池中的worker线程。
  • Task列表,作为WorkQueue的本地任务队列,此任务队列,是一个Deque,但是并没有使用JDK中已有的LinkedBlockDeque,而是使用ArrayBlockingQueue的思想(循环数组) + CAS实现的简单双端队列,最终由WorkQueue支持三种操作:poll、push、pop用于操作此Task列表;其中poll和push有当前ForkJoinWorkerThread操作,poll使用FIFO方式弹出任务并执行,push有submission线程添加任务到队列尾部,此外当前线程窃取操作也会执行push;对于pop,将有其他worker线程窃取任务时,从队列尾部窃取(LIFO)。
  • WorkQueue对象,将有submission线程提交任务时创建。此外,Fork子任务时,优先将子任务添加到本地队列,同时也会触发线程池容量检测,如果workQueues容量未满,也会创建新的worker线程和workQueue对象。(参见signalWorker())
ForkJoinWorkerThread:
  • 同ExecutorService中的worker线程,守护线程。有WorkQueue引用并持有,同时也持有workQueue和pool的引用。
  • 在submission线程提交任务时,如果线程池容量尚未达到并发级别上限,将会优先创建新的worker线程并由其执行task。在其他worker线程中fork操作时,优先将子任务添加到本地任务列表,并检测线程池容量(即workQueues大小,当然源码中有单独的便捷字段来控制),如果未达到上线,也将会触发创建新的worker线程。
  • worker线程启动后,将会优先检测其workQueue中的本地队列是否有亟待执行的任务,如果有则逐个执行。直到任务队列全部执行完毕,开始steal。
  • steal操作,也是ForkJoinPool的核心特性;当worker线程的本地任务队列执行完毕后,触发stealing;从Pool的workQueues的随机位置开始,按照一定的节奏推进(跳)(随机 + 跳,主要目的就是避免与其他worker线程的并发冲突,因为stealing操作可能在多个worker线程同时发生),检测遇到的每个workQueue的本地队列,如果有剩余任务,则使用workQueue.pop窃取一个;直到遍历完workQueues,如果还没有steal到任何任务,这说明整个线程池中任务数量相对较少(饥饿),为了避免整个线程池都处于盲目的、无休止的steal,此时worker线程将会被“钝化”(park)一段时间,并等待submission线程或者其他worker线程fork操作唤醒;如果一个worker线程IDLE一段时间之后(2秒),将会被释放销毁。(scan(),tryRelease())。
  • worker线程在执行任何任务时,都会尝试捕捉异常,并将异常作为Futrue的结果,在使用ForkJoinTask.get()时重新抛出。
  • 由此可见,我们不能再ForkJoinTask中使用时间不可控的block操作,假如所有的ForkJoinTask的work线程都阻塞,整个线程池也将完全失效(即不能fork新任务触发唤醒,worker线程也无法触发其他线程的唤醒)。

THE END.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值