Fork/Join框架介绍 顺便测试jvm的内存溢出问题

1. 什么是Fork/Join框架

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

我们再通过Fork和Join这两个单词来理解下Fork/Join框架,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+。。+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。Fork/Join的运行流程图如下:

Fork/Join框架介绍 顺便测试jvm的内存溢出问题_第1张图片

2. Fork/Join框架的介绍

我们已经很清楚Fork/Join框架的需求了,那么我们可以思考一下,如果让我们来设计一个Fork/Join框架,该如何设计?这个思考有助于你理解Fork/Join框架的设计。

第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。

第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

java.util.concurrent

类 ForkJoinTask

  • java.lang.Object
    • java.util.concurrent.ForkJoinTask
  • 所有已实现的接口:
    Serializable
    , Future

    直接已知子类:
    RecursiveAction
    , RecursiveTask

Fork/Join使用两个类来完成以上两件事情:

  • ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
    • RecursiveAction:用于没有返回结果的任务。
    • RecursiveTask :用于有返回结果的任务。
  • ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

3. 使用Fork/Join框架

让我们通过一个简单的需求来使用下Fork/Join框架,需求是:计算1+2+3+4的结果。

使用Fork/Join框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。因为是有结果的任务,所以必须继承RecursiveTask,实现代码如下:

package Concurrent;
import java.util.concurrent.RecursiveTask;
/**
 *
 * @author zhengchao
 */
public class CalculatorFork_Join extends RecursiveTask {  
  
    private static final int THRESHOLD = 10000;  
    private int start;  
    private int end;  
  
    public CalculatorFork_Join(int start, int end) {  
        this.start = start;  
        this.end = end;  
    }  
  
    @Override  
    protected Long compute() {  
        long sum = 0;  
        if((end - start) < THRESHOLD){  
            for(int i = start; i<= end;i++){  
                sum += i;  
            }  
        }else{  
            int middle = (start + end) /2;  
            CalculatorFork_Join left = new CalculatorFork_Join(start, middle);  
            CalculatorFork_Join right = new CalculatorFork_Join(middle + 1, end);  
            left.fork();  
            right.fork();  
  
            sum = left.join() + right.join();  
        }  
        return sum;  
    }  
}  

package Concurrent;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;

/**
 *
 * @author zhengchao
 */
public class testMain {
    
     public static void main(String[] args) throws ExecutionException, InterruptedException{
         
         long start = System.currentTimeMillis();
         ForkJoinPool forkJoinPool = new ForkJoinPool();  
         Future result = forkJoinPool.submit(new CalculatorFork_Join(0, 1000000));  
         System.out.println("ForkJoinCostTime:"+(System.currentTimeMillis()-start));
         System.out.println(result.get());
         
         start = System.currentTimeMillis();
         long sum = 0;
         for(int i=0;i<=1000000;i++){
             sum+=i;
         }
         System.out.println("CostTime:"+(System.currentTimeMillis()-start));
         System.out.println(sum);
         forkJoinPool.shutdown();
             
     }
}

通过这个例子让我们再来进一步了解ForkJoinTask,ForkJoinTask与一般的任务的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进入compute方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

性能分析:

1、计算1-10000

run:
ForkJoinCostTime:1
50005000
CostTime:0
50005000
成功构建 (总时间: 0 秒)
数据量较小时,是用Fork/Join效果不明显,甚至不如正常的计算,这是因为创建 Fork/Join耗时了。
2、计算1-100000

run:
ForkJoinCostTime:1
5000050000
CostTime:2
5000050000
成功构建 (总时间: 0 秒)

3、计算1-1000000

run:
ForkJoinCostTime:1
500000500000
CostTime:3
500000500000
成功构建 (总时间: 0 秒)

如果我们将

private static final int THRESHOLD = 10000; 
设置为100,运行计算 计算1-1000000:

run:
ForkJoinCostTime:2
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError
	at java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:942)
	at Concurrent.testMain.main(testMain.java:26)
Caused by: java.lang.OutOfMemoryError
	at sun.reflect.GeneratedConstructorAccessor1.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:536)
	at java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:941)
	... 1 more
Caused by: java.lang.OutOfMemoryError
	at sun.reflect.GeneratedConstructorAccessor1.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:536)
	at java.util.concurrent.ForkJoinTask.reportResult(ForkJoinTask.java:596)
	at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:640)
	at Concurrent.CalculatorFork_Join.compute(CalculatorFork_Join.java:40)
	at Concurrent.CalculatorFork_Join.compute(CalculatorFork_Join.java:15)
	at java.util.concurrent.RecursiveTask.exec(RecursiveTask.java:93)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:334)
	at java.util.concurrent.ForkJoinWorkerThread.execTask(ForkJoinWorkerThread.java:604)
	at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:784)
	at java.util.concurrent.ForkJoinPool.work(ForkJoinPool.java:646)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:398)
Caused by: java.lang.OutOfMemoryError
	at sun.reflect.GeneratedConstructorAccessor1.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:536)
	at java.util.concurrent.ForkJoinTask.reportResult(ForkJoinTask.java:596)
	at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:640)
	at Concurrent.CalculatorFork_Join.compute(CalculatorFork_Join.java:40)
	at Concurrent.CalculatorFork_Join.compute(CalculatorFork_Join.java:15)
	at java.util.concurrent.RecursiveTask.exec(RecursiveTask.java:93)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:334)
	at java.util.concurrent.ForkJoinWorkerThread.execTask(ForkJoinWorkerThread.java:604)
	at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:762)
	... 2 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError
	... 14 more
Caused by: java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:713)
	at java.util.concurrent.ForkJoinPool.addWorker(ForkJoinPool.java:1132)
	at java.util.concurrent.ForkJoinPool.tryPreBlock(ForkJoinPool.java:1009)
	at java.util.concurrent.ForkJoinPool.tryAwaitJoin(ForkJoinPool.java:1042)
	at java.util.concurrent.ForkJoinWorkerThread.joinTask(ForkJoinWorkerThread.java:731)
	at java.util.concurrent.ForkJoinTask.doJoin(ForkJoinTask.java:362)
	at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:639)
	... 8 more
Java Result: 1
成功构建 (总时间: 0 秒)

内存溢出了,为什么呢?因为我们创建了太大的线程,导致内存消耗殆尽。

所以,阈值要设定的恰当,否则可能效率并不会提高,设置导致异常的产生。


你可能感兴趣的:(Java并发,Java虚拟机,性能优化,深入理解Java虚拟机,多线程,JVM,线程池,并发)