我尝试使用经典的Fibonacci算法学习RecursiveTask类。该算法正常工作,直到斐波纳契数超过17000,然后抛出StackOverflowError。我不知道问题是线程的数量还是我使用缓存存储计算出的数字。我知道有更好的算法来计算斐波纳契数,但这只是为了学习fork / join架构及其局限性。较低数字(例如数字17800)所需的时间需要153毫秒,然后缓存的大小为13 MB。
问题:如何使用相同的算法使这个代码比例更好(计算更高的数字)?
Fibonacci代码:
public class FibonacciTask extends RecursiveTask<BigInteger>{
// this theshold is used because the overhead of the architecture
// makes the process crazy slow if we try to use it on easy problems
private static final int EASYCALC = 10;
// limit of the cache
private static final int CACHELIM = 20000;
private int n;
private static Map<Integer, BigInteger> fibCache = new ConcurrentHashMap<>();
public BigInteger result;
public FibonacciTask(int x, Map<Integer, BigInteger> fibCache){
n = x;
this.fibCache = fibCache;
// calculate the first 10 numbers without threads
if(!fibCache.containsKey(EASYCALC)){
fibCache.put(EASYCALC, this.fibonacci(EASYCALC));
result = fibCache.get(x);
}
}
@Override
protected BigInteger compute() {
if(!fibCache.containsKey(n)){
FibonacciTask worker1 = new FibonacciTask(n-1, fibCache);
FibonacciTask worker2 = new FibonacciTask(n-2, fibCache);
worker1.fork(); // fork this work to new thread
result = worker2.compute().add(worker1.join());
if(n >= CACHELIM){
return result;
}
fibCache.put(n, result);
}
return fibCache.get(n);
}
// calculate without threads
public BigInteger fibonacci(int n){
if(!fibCache.containsKey(n) ){
fibCache.put(n, fibonacci(n-1).add(fibonacci(n-2)) );
}
return fibCache.get(n);
}
}
主:
int n = 17950;
ForkJoinPool pool = new ForkJoinPool(processors);
FibonacciTask task = new FibonacciTask(n, fibCache);
pool.invoke(task);
BigInteger result = task.result;
错误输出:
run:
No. of processors: 8
Exception in thread "main" java.lang.StackOverflowError
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
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 java.util.concurrent.ForkJoinPool.invoke(ForkJoinPool.java:1521)
at cachedThreadedFibonacci.SmartWorker.main(SmartWorker.java:62)
Caused by: java.lang.StackOverflowError
at cachedThreadedFibonacci.FibonacciTask.compute(FibonacciTask.java:48)
at cachedThreadedFibonacci.FibonacciTask.compute(FibonacciTask.java:55)
at cachedThreadedFibonacci.FibonacciTask.compute(FibonacciTask.java:55)
... same line repeating
修改 我也很困惑,因为如果我在Netbeans中将堆栈大小设置为2Mb(-Xss2M)那么它工作正常(即使我的测试缓存可以高达17Mb)?如果我将大小设置为1 Mb它再也不起作用(在前面描述的同一点失败),我错过了什么?
答案 0 :(得分:2)
以下(不完整)代码段来自您的FibonacciTask
课程。
@Override
protected BigInteger compute() {
if(!fibCache.containsKey(n)){
FibonacciTask worker1 = new FibonacciTask(n-1, fibCache);
FibonacciTask worker2 = new FibonacciTask(n-2, fibCache);
worker1.fork(); // fork this work to new thread
result = worker2.compute().add(worker1.join());
// ... snip ...
}
您的问题过于关注fork-join池使用的线程数。虽然对worker1.fork()
的调用确实会在不同的线程上产生效果,但它不是您问题的根源。对worker2.compute()
的调用发生在当前线程上,即使该方法是在另一个(新)实例上调用的。每个此类调用都会创建自己的worker2
实例,并继续在该非常相同的线程上调用该对象的compute()
方法。
这是递归算法,更不用说你的任务扩展RecursiveTask
了。每个线程每个线程都有可用的内存用于维护方法调用堆栈。任何足够深的递归算法都可以超过这个限制;正如您所观察到的,在Java中,这会产生StackOverflowException
。
尝试将此错误与基于地图的缓存的大小相关联是毫无意义的。您的地图及其内容位于堆上,这是一个与线程调用堆栈完全独立的内存区域。
FibonacciTask task = new FibonacciTask(n, fibCache);
pool.invoke(task);
这会导致compute()
选择的单线程上的pool.invoke()
的堆栈深度至少为({1}},用于执行第一次迭代任务。如果值足够大n
,则可以超过允许的最大堆栈深度。正如您所发现的,increasing the Java stack size可以针对给定的n
值阻止此错误。但是,使用诸如此类的递归算法,增加堆栈大小不会“修复”#34;问题是,它只会延迟问题,直到你为n
选择一个足够高的新值来超过新的每线程堆栈内存区域。