ForkJoinPool-为什么程序抛出OutOfMemoryError?

时间:2018-08-02 10:01:19

标签: java multithreading java-8 fork-join forkjoinpool

我想尝试Java 8中的ForkJoinPool,所以我写了一个小程序来搜索所有名称在给定目录中包含特定关键字的文件。

程序

public class DirectoryService {

    public static void main(String[] args) {
        FileSearchRecursiveTask task = new FileSearchRecursiveTask("./DIR");
        ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
        List<String> files = pool.invoke(task);
        pool.shutdown();
        System.out.println("Total  no of files with hello" + files.size());
    }

}

    class FileSearchRecursiveTask extends RecursiveTask<List<String>> {
        private String path;
        public FileSearchRecursiveTask(String path) {
            this.path = path;
        }

        @Override
        protected List<String> compute() {
            File mainDirectory = new File(path);
            List<String> filetedFileList = new ArrayList<>();
            List<FileSearchRecursiveTask> recursiveTasks = new ArrayList<>();
            if(mainDirectory.isDirectory()) {
                System.out.println(Thread.currentThread() + " - Directory is " + mainDirectory.getName());
                if(mainDirectory.canRead()) {
                    File[] fileList = mainDirectory.listFiles();
                    for(File file : fileList) {
                        System.out.println(Thread.currentThread() + "Looking into:" + file.getAbsolutePath());
                        if(file.isDirectory()) {
                            FileSearchRecursiveTask task = new FileSearchRecursiveTask(file.getAbsolutePath());
                            recursiveTasks.add(task);
                            task.fork();
                        } else {
                            if (file.getName().contains("hello")) {
                                System.out.println(file.getName());
                                filetedFileList.add(file.getName());
                            }
                        }
                    }
                }

                for(FileSearchRecursiveTask task : recursiveTasks) {
                  filetedFileList.addAll(task.join());
                }

        }
        return filetedFileList;

    }
}

当目录中没有太多子目录和文件时,该程序运行良好,但是如果目录确实很大,则会抛出OutOfMemoryError。

我的理解是最大线程数(包括补偿线程)是有界的,那么为什么会出现此错误?我的程序中缺少任何内容吗?

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:714)
at java.util.concurrent.ForkJoinPool.createWorker(ForkJoinPool.java:1486)
at java.util.concurrent.ForkJoinPool.tryCompensate(ForkJoinPool.java:2020)
at java.util.concurrent.ForkJoinPool.awaitJoin(ForkJoinPool.java:2057)
at java.util.concurrent.ForkJoinTask.doJoin(ForkJoinTask.java:390)
at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:719)
at FileSearchRecursiveTask.compute(DirectoryService.java:51)
at FileSearchRecursiveTask.compute(DirectoryService.java:20)
at java.util.concurrent.RecursiveTask.exec(RecursiveTask.java:94)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.tryRemoveAndExec(ForkJoinPool.java:1107)
at java.util.concurrent.ForkJoinPool.awaitJoin(ForkJoinPool.java:2046)
at java.util.concurrent.ForkJoinTask.doJoin(ForkJoinTask.java:390)
at java.util.concurrent.ForkJoinTask.join(ForkJoinTask.java:719)
at FileSearchRecursiveTask.compute(DirectoryService.java:51)
at FileSearchRecursiveTask.compute(DirectoryService.java:20)
at java.util.concurrent.RecursiveTask.exec(RecursiveTask.java:94)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)   

2 个答案:

答案 0 :(得分:5)

您不应分叉所有新任务。基本上,只要有可能另一个工作线程可以接替分叉的工作并在本地进行评估,就应该进行分叉。然后,一旦您完成了一项任务,就不要马上拨打join()。虽然底层框架将启动补偿线程以确保您的工作能够继续进行,而不是仅阻塞所有线程来等待子任务,但这将创建大量线程,这些线程可能超出系统的功能。

这是您代码的修订版:

public class DirectoryService {

    public static void main(String[] args) {
        FileSearchRecursiveTask task = new FileSearchRecursiveTask(new File("./DIR"));
        List<String> files = task.invoke();
        System.out.println("Total no of files with hello " + files.size());
    }

}

class FileSearchRecursiveTask extends RecursiveTask<List<String>> {
    private static final int TARGET_SURPLUS = 3;
    private File path;
    public FileSearchRecursiveTask(File file) {
        this.path = file;
    }

    @Override
    protected List<String> compute() {
        File directory = path;
        if(directory.isDirectory() && directory.canRead()) {
            System.out.println(Thread.currentThread() + " - Directory is " + directory.getName());
            return scan(directory);
        }
        return Collections.emptyList();
    }

    private List<String> scan(File directory)
    {
        File[] fileList = directory.listFiles();
        if(fileList == null || fileList.length == 0) return Collections.emptyList();
        List<FileSearchRecursiveTask> recursiveTasks = new ArrayList<>();
        List<String> filteredFileList = new ArrayList<>();
        for(File file: fileList) {
            System.out.println(Thread.currentThread() + "Looking into:" + file.getAbsolutePath());
            if(file.isDirectory())
            {
                if(getSurplusQueuedTaskCount() < TARGET_SURPLUS)
                {
                    FileSearchRecursiveTask task = new FileSearchRecursiveTask(file);
                    recursiveTasks.add(task);
                    task.fork();
                }
                else filteredFileList.addAll(scan(file));
            }
            else if(file.getName().contains("hello")) {
                filteredFileList.add(file.getAbsolutePath());
            }
        }

        for(int ix = recursiveTasks.size() - 1; ix >= 0; ix--) {
            FileSearchRecursiveTask task = recursiveTasks.get(ix);
            if(task.tryUnfork()) task.complete(scan(task.path));
        }

        for(FileSearchRecursiveTask task: recursiveTasks) {
            filteredFileList.addAll(task.join());
        }
        return filteredFileList;
    }
}

进行处理的方法已被分解为接收目录作为参数的方法,因此我们能够在本地将其用于不一定与FileSearchRecursiveTask实例相关联的任意目录。

然后,该方法使用getSurplusQueuedTaskCount()来确定尚未由其他工作线程处理的本地排队任务的数量。确保有一些有助于工作平衡。但是,如果此数目超过阈值,则将在本地完成处理,而不会派生更多工作。

在本地处理之后,它遍历任务并使用tryUnfork()来识别尚未被其他工作线程窃取的作业,并在本地进行处理。向后迭代以从最年轻的工作开始,这增加了找到工作的机会。

此后,join()包含所有子任务,这些子任务现在已经由另一个工作线程完成或正在处理。

请注意,我将启动代码更改为使用默认池。这将使用“ CPU核心数”减去一个工作线程,再加上启动线程,即本示例中的main线程。

答案 1 :(得分:2)

只需稍作更改即可。 您需要为newWorkStealingPool指定并行性,如下所示:

ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool(5);

根据其文档:

  

newWorkStealingPool(int parallelism)->创建一个线程池,该线程池维护足以支持给定并行度级别的线程,并且可以使用多个队列来减少争用。并行度级别对应于活动参与或可用于参与任务处理的最大线程数。实际的线程数可能会动态增长和收缩。工作窃取池不能保证提交任务的执行顺序。

根据所附的Java Visual VM屏幕快照,此并行性使程序可以在指定的内存中运行,而不会耗尽内存。 enter image description here

还有一件事(不确定是否会产生效果):

更改调用fork并将任务添加到列表的顺序。也就是说,更改

FileSearchRecursiveTask task = new FileSearchRecursiveTask(file.getAbsolutePath());
recursiveTasks.add(task);
task.fork();

FileSearchRecursiveTask task = new FileSearchRecursiveTask(file.getAbsolutePath());
task.fork();
recursiveTasks.add(task);