Java中Fork/Join框架在什么时候使用合适?

看到JDK1.8中新增的WorkStealingPool线程池,突然很好奇,这个线程池实际作用对象为ForkJoinTask,也就是JDK1.7新增的Fork/Join框架,看了一下Fork/Join框架原理,感觉这种分而治之策略很不错,将大任务分解为一个又一个小任务,然后将结果归并。想想这种策略用在递归中不是刚好合适吗?于是写了一个快排程序,比较一下单线程与多线程速度差距到底有多大。

注意

  • 这里我的统一以快排300000个随机数为例子,每次执行均会创建那么多随机数,所以每次排序的数组都不一样,对排序可能有一些影响,但多次统计,影响这里忽略不计

单线程快排程序NonWorkStealingPoolTask.java

package cn.crabime;

import java.util.List;

/**
 * 直接使用快排(单线程)进行排序
 */
public class NonWorkStealingPoolTask {

    private final static int CUTOFF = 10;

    public static void quickSort(int[] nums, int left, int right) {
        if (left + CUTOFF < right) {
            int pivot = median(nums, left, right);
            int i = left, j = right;
            for (;;) {
                while (nums[++i] < pivot) {}
                while (nums[--j] > pivot) {}
                if (i < j)
                    swap(nums, i, j);
                else
                    break;
            }
            swap(nums, i, j - 1);
            quickSort(nums, left, i - 1);
            quickSort(nums, i + 1, right);
        } else {
            int j;
            for (int i = left + 1; i <= right; i++) {
                int tmp = nums[i];
                for (j = i; j > 0 && nums[j - 1] > tmp; j--) {
                    nums[j] = nums[j - 1];
                }
                nums[j] = tmp;
            }
        }
    }

    private static int median(int[] nums, int left, int right) {
        int center = (left + right) / 2;
        if (nums[left] > nums[center])
            swap(nums, left, center);
        if (nums[left] > nums[right])
            swap(nums, left, right);
        if (nums[center] > nums[right])
            swap(nums, center, right);
        swap(nums, center, right - 1);
        return nums[right - 1];
    }

    private static void swap(int[] nums, int left, int right) {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
    }

    public static void main(String[] args) {
        List list = MillionNumberGenerator.generateNumbers(300000);
        int[] num = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            num[i] = list.get(i);
        }
        long start = System.currentTimeMillis();
        quickSort(num, 0, num.length - 1);
        long end = System.currentTimeMillis();
        System.out.println("总共耗时:" + (end - start));
    }
}

上面代码大体逻辑即当排序的数据总量小于10时,直接执行插入排序,否则进行快排,参照《数据结构与算法分析 第2版》,这样排序性能有15%提升。

多线程快排程序WorkStealingPoolTask.java

package cn.crabime;

import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

/**
 * 演示如何使用jdk1.8新增的WorkStealingPool
 */
public class WorkStealingPoolTask extends RecursiveAction {

    private static final int CUTOFF = 10;

    private int[] nums;

    private int left;

    private int right;

    public WorkStealingPoolTask(int[] nums, int left, int right) {
        this.nums = nums;
        this.left = left;
        this.right = right;
    }

    public static void insertSort(int[] nums) {
        int j;
        for (int i = 1; i < nums.length; i++) {
            int tmp = nums[i];
            for (j = i; j > 0 && tmp < nums[j - 1]; j--) {
                nums[j] = nums[j - 1];
            }
            nums[j] = tmp;
        }
    }

    public static void quickSort(int[] nums, int left, int right) {
        if (left + CUTOFF < right) {
            int pivot = median3(nums, left, right);
            int i = left, j = right - 1;
            for (;;) {
                while (nums[++i] < pivot) {}
                while (nums[--j] > pivot) {}
                if (i < j) {
                    swap(nums, i, j);
                } else {
                    // i >= j情况即可直接跳出循环
                    break;
                }
            }

            // i此时在j右侧,将num[i]值与pivot进行互换
            swap(nums, i, right - 1);
            quickSort(nums, left, i - 1);
            quickSort(nums, i + 1, right);
        } else {
            insertSort(nums);
        }
    }

    /**
     * 三数中值法获取pivot值
     */
    public static int median3(int[] nums, int left, int right) {
        int center = (left + right) / 2;
        if (nums[left] > nums[center]) {
            swap(nums, left, center);
        }
        if (nums[left] > nums[right]) {
            swap(nums, left, right);
        }
        if (nums[center] > nums[right]) {
            swap(nums, center, right);
        }

        // center与right-1位置元素进行交换
        swap(nums, center, right - 1);
        return nums[right - 1];
    }

    private static void swap(int[] nums, int left, int right) {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        List list = MillionNumberGenerator.generateNumbers(30000000);
        int[] num = new int[list.size()];
        for (int i = 0; i < list.size(); i++) {
            num[i] = list.get(i);
        }
        WorkStealingPoolTask stealingPoolTask = new WorkStealingPoolTask(num, 0, num.length - 1);
        forkJoinPool.submit(stealingPoolTask);
        long start = System.currentTimeMillis();
        try {
            stealingPoolTask.get();
            long end = System.currentTimeMillis();
            System.out.println("总共耗时:" + (end - start));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

    @Override
    protected void compute() {
        WorkStealingPoolTask task1 = null;
        WorkStealingPoolTask task2 = null;
        //在左右间距小于10时,可直接采用插入排序
        if (left + CUTOFF < right) {
            int pivot = median3(nums, left, right);
            int i = left, j = right - 1;
            for (;;) {
                while (nums[++i] < pivot) {}
                while (nums[--j] > pivot) {}
                if (i < j) {
                    swap(nums, i, j);
                } else {
                    // i >= j情况即可直接跳出循环
                    break;
                }
            }

            // i此时在j右侧,将num[i]值与pivot进行互换
            swap(nums, i, right - 1);

            task1 = new WorkStealingPoolTask(nums, left, i - 1);
            task1.fork();
            task2 = new WorkStealingPoolTask(nums, i + 1, right);
            task2.fork();
            if (!task1.isDone()) {
                task1.join();
            }
            if (!task2.isDone()) {
                task2.join();
            }
        } else {
            insertSort(nums);
        }
    }
}

统计结果

Java中Fork/Join框架在什么时候使用合适?_第1张图片
这里我使用的PC配置为8G内存Intel i5处理器单处理器2核Mac Air,上面NULL表示很长时间跑不出结果,很长时间表示t > 3m,后面由于电脑疯狂散热且发烫,没办法,手动终止了进程。
这里我借助了Mac三的activity软件分析进程上下文切换和CPU时钟情况,下面是对比图:
Java中Fork/Join框架在什么时候使用合适?_第2张图片

单线程快排结果截图

Java中Fork/Join框架在什么时候使用合适?_第3张图片

Fork/Join快排结果截图

数据分析

由于这里统计的是某个jvm进程信息,我们排序线程只是其中的主线程,还有一些jvm内置线程如GC线程、C1编译线程等,所以还是存在一些上下文切换的。当然我们还是看上面截图结果可以发现:Fork/Join子任务线程间上下文切换非常的频繁,是单线程的5倍;其次,CPU time远高于单线程。那么大部分CPU时间都花在了线程间上下文切换了,说明Fork/Join在我们的这次测试中是失败的,准确的说是在快速排序中不合适。

总结

  1. Fork/Join在快速排序中不合适,效率远低于单线程排序
  2. Fork/Join在执行过程中会产生大量的上下文切换,如果每个子任务本身执行就很快时,采用该框架效果并不理想,并且会产生大量的系统资源浪费

你可能感兴趣的:(Java)