Java并发Concurrent 之fork/join 框架学习

1  fork/join 框架包括多个类

 1 ForkJoinPool 线程池类 ,

  2 ForkJoinTask  任务类

任务子类有2种

//有返回值
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {}
//无返回值
public abstract class RecursiveAction extends ForkJoinTask<Void> {}

 

2 接下来我们看一个例子:(求两数之间所有数之和,如1-100——>5050):

class ForkJoinSumCalculate extends RecursiveTask<Long>{

    private static final long serialVersionUID = -1812835340478767238L;
    
    private long start;
    private long end;
    
    private static final long THURSHOLD = 10000L;  //临界值
    
    public ForkJoinSumCalculate(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;
        //小于临界值,则不进行拆分,直接计算初始值到结束值之间所有数之和
        if(length <= THURSHOLD){
            long sum = 0L;
            
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            
            return sum;
        }else{  //大于临界值,取中间值进行拆分,递归调用
            long middle = (start + end) / 2;
            
            ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle); 
            left.fork(); //进行拆分,同时压入线程队列
            
            ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle+1, end);
            right.fork(); //
            
            return left.join() + right.join();
        }
    }    
}


public static void main(String[] args) {
     Instant start = Instant.now();        
     ForkJoinPool pool = new ForkJoinPool();        
     ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);        
     Long sum = pool.invoke(task);        
     System.out.println(sum);        
     Instant end = Instant.now();        
     System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());
 }

 

从例子上看,程序自定义了一个Forkjoin线程池 ,然后定义了一个 ForkJoinTask ,然后使用pool.invoke(task) 方法执行任务

这样线程池内部 取出一个空闲线程 就会去执行这个任务,然后发现这个任务可以分解成许多小任务,然后就执行分解逻辑,并且把小任务放入线程池队列,这样其他线程就可以获取小任务执行了。最后每个小任务的结果 合并 成一个大的结果。

 

3 换位思考 ThreadPoolExecotor 的实现逻辑

如果不用fork/join 框架而自定义拆分逻辑然后每部分让一个线程执行的话,伪代码应该是这个样子

List<Future> list=new ArrayList();

for(int i =0 i<group.size;i++){

list.add(executorservice.submit(task))

}

然后遍历list ,获取执行结果。这样写起来麻烦,而且还要通过遍历获取结果,不好,

总结  如果任务需要拆分的需求,则可以使用fork/join

 

4 另外 fork/join 还有一个特性 任务窃取。

有3个任务,1个简单1个中等1个复杂,

如果把这三个交给ThreadPoolExecutor 的时候 如果线程池有3个线程,则最快执行完时间是复杂任务的执行时间。

如果把这三个任务交给ForkJoinPool(同样有3个线程) ,然后把三个任务拆分放入任务队列,则最快执行时间 ,就是三个任务的平均时间,因为 任务拆分之后,三个线程执行的都是子任务,每个线程都可以执行其他父任务拆分出来的子任务。

 

5 综上所述,Fork/Join 模式有自己的适用范围。如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能够获得最终的答案,那么这个应用就适合用 Fork/Join 模式来解决. 

ThreadPoollExector 适合于 不能拆分的任务;

 

 6 ForkJoinPool api 和 简单介绍:

使用的工作窃取的方式能够在最大方式上充分利用CPU的资源,一般流程是fork分解,join结合。本质是将一个任务分解成多个子任务,每个子任务用单独的线程去处理,主要几个常用方法有 

fork( ForkJoinTask)

异步执行一个线程

join( ForkJoinTask)

等待任务完成并返回执行结果

execute( ForkJoinTask)

执行不带返回值的任务

submit( ForkJoinTask)

执行带返回值的任务

invoke( ForkJoinTask)

执行指定的任务,等待完成,返回结果。

invokeAll(ForkJoinTask)

执行指定的任务,等待完成,返回结果。

shutdown()

执行此方法之后,ForkJoinPool 不再接受新的任务,但是已经提交的任务可以继续执行。如果希望立刻停止所有的任务,可以尝试 shutdownNow() 方法。

awaitTermination(int, TimeUnit.SECONDS)

阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束。

compute()

执行任务的具体方法

Runtime.getRuntime().availableProcessors()

获取CPU个数的方法

它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。这一点与当前很火的大数据处理框架Hadoop的map/reduce思想非常类似,但是他们的使用场合却不一样,ForkJoinPool适合在一台PC多核CPU上运行,而hadoop则适合在分布式环境中进行大规模集群部署。  典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。

那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

 

7 自动并行化(Automatic Parallelization)

在Java 8中,引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,前提是使用了ForkJoinPool。

Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。

当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。

比如下面的代码用来遍历列表中的元素并执行需要的计算:

Stream<Integer> stream = arrayList.parallelStream();stream.forEach(a -> {String symbol = StockPriceUtils.makeSymbol(a);StockPriceHistory sph = new StockPriceHistoryImpl(symbol, startDate, endDate, entityManager);});

对于列表中的元素的计算都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的通用线程池处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。

对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。如果需要调整线程数量,可以通过设置系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N

下面的一组数据用来比较使用ThreadPoolExecutor和ForkJoinPool中的通用线程池来完成上面简单计算时的性能:

线程数

ThreadPoolExecutor(秒)

ForkJoinPool Common Pool(秒)

1

255.6

135.4

2

134.8

110.2

4

77.0

96.5

8

81.7

84.0

16

85.6

84.6

注意到当线程数为1,2,4时,性能差异的比较明显。线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor的性能十分接近。

出现这种现象的原因是,forEach方法用了一些小把戏。它会将执行forEach本身的线程也作为线程池中的一个工作线程。因此,即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此在使用forEach的时候,线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。

所以当ForkJoinPool通用线程池实际需要4个工作线程时,可以将它设置成3,那么在运行时可用的工作线程就是4了。

总结

  1. 当需要处理递归分治算法时,考虑使用ForkJoinPool。

  2. 仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。

  3. Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值