• Stream 分支/合并 框架的实际例子


    //继承RecursiveTask来创建可以用于分支/合并框架的任务
    public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {
    	private final long[] numbers;//要求和	的数组
    	private final int start;//子任务处理的数组的起始和终止位置
    	private final int end;
    	public static final long THRESHOLD = 10_000;//不再将任务分解为子任务的数组大小
    	public ForkJoinSumCalculator(long[] numbers) {	//公共构造函数用于创建主任务
    		this(numbers, 0, numbers.length);
    	}
        //私有构造函数用于以递归方式为主任务创建子任务
    	private ForkJoinSumCalculator(long[] numbers, int start, int end) {
    		this.numbers = numbers;
    		this.start = start;
    		this.end = end;
    	}
    	//覆盖RecursiveTask抽象方法
    	@Override
    	protected Long compute() {
    		int length = end - start;//该任务负责求和的部分的大小
    		if (length <= THRESHOLD) {
    			//如果大小小于	或等于阈值,顺序计算结果
    			return computeSequentially();
    		}
    		//创建一个子任务来为数组的前一半求和
    		ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length/2);
    		//利用另一个ForkJoinPool线程异步执行新创建的子任务
    		leftTask.fork();
    		//创建一个任务为数组的后一半求和
    		ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length/2, end);
    		//同步执行第二个子任务,有可能允许进	一步递归划分
    		Long rightResult = rightTask.compute();
    		//读取第一个子任务的结果,如果尚未完成就等待
    		Long leftResult = leftTask.join();
    		//该任务的结果是两个	子任务结果的组合
    		return leftResult + rightResult;
    	}
    	//在子任务不再可分时计算结果的简单算法
    	private long computeSequentially() {
    		long sum = 0;
    		for (int i = start; i < end; i++) {
    			sum += numbers[i];
    		}
    		return sum;
    	}
    	public static long forkJoinSum(long n) {
    		long[] numbers = LongStream.rangeClosed(1, n).toArray();//生成包含前n个自然数的数组
    		ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
    		return new ForkJoinPool().invoke(task);
    	}
    	
    }
    

     

    import java.util.function.Function;
    
    
    public class Test2 {
    	public static long measureSumPerf(Function<Long, Long> adder, long n) {
    		long fastest = Long.MAX_VALUE;
    		for (int i = 0; i < 10; i++) {
    			long start = System.nanoTime();
    			long sum = adder.apply(n);
    			long duration = (System.nanoTime() - start) / 1_000_000;
    			//System.out.println("Result: " + sum);
    			if (duration < fastest) fastest = duration;
    		}
    		return fastest;
    	}
    	
    	
    	public static void main(String[] args) {
    		 System.out.println("ForkJoin sum done in: " + measureSumPerf( ForkJoinSumCalculator::forkJoinSum, 10_000_000) + " msecs" );
    	}
    }
    

      

      请注意在实际应用时,使用多个ForkJoinPool是没有什么意义的。正是出于这个原因,一
    般来说把它实例化一次,然后把实例保存在静态字段中,使之成为单例,这样就可以在软件中任
    何部分方便地重用了。这里创建时用了其默认的无参数构造函数,这意味着想让线程池使用JVM
    能够使用的所有处理器。更确切地说,该构造函数将使用Runtime.availableProcessors的
    返回值来决定线程池使用的线程数。请注意availableProcessors方法虽然看起来是处理器,
    但它实际上返回的是可用内核的数量,包括超线程生成的虚拟内核。
      运行ForkJoinSumCalculator
      当把ForkJoinSumCalculator任务传给ForkJoinPool时,这个任务就由池中的一个线程
    执行,这个线程会调用任务的compute方法。该方法会检查任务是否小到足以顺序执行,如果不
    够小则会把要求和的数组分成两半,分给两个新的ForkJoinSumCalculator,而它们也由
    ForkJoinPool安排执行。因此,这一过程可以递归重复,把原任务分为更小的任务,直到满足
    不方便或不可能再进一步拆分的条件(本例中是求和的项目数小于等于10 000)。这时会顺序计
    算每个任务的结果,然后由分支过程创建的(隐含的)任务二叉树遍历回到它的根。接下来会合
    并每个子任务的部分结果,从而得到总任务的结果

      虽然分支/合并框架还算简单易用,不幸的是它也很容易被误用。以下是几个有效使用它的
    最佳做法。
     对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子
        任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,
        因为每个子任务都必须等待另一个子任务完成才能启动。
     不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直
        接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
     对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用
        它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为
        其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
     调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面
       看栈跟踪(stack trace)来找问题,但放在分支合并计算上就不行了,因为调用compute
       的线程并不是概念上的调用方,后者是调用fork的那个。
     和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计
    算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时
    有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方
    法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出
    同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就
    像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编
    译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么
    做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死
    码分析——删去从未被使用的计算)。

     

  • 相关阅读:
    CodeSmith功能和技巧收集
    简繁转换js兼容各种浏览器
    40 个轻量级 JavaScript 库
    AJAX处理Session
    对项目管理的几点认识(转)
    extjs
    数据采集需要的方法
    JavaScript 浮动定位提示效果
    一个类别表设计
    ExtJS 源码剖析 —— Ext类
  • 原文地址:https://www.cnblogs.com/sg9527/p/7911974.html
Copyright © 2020-2023  润新知