You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何代码创建过多线程引发java.lang.OutOfMemoryError错误?

为什么你的并行搜索代码会抛出java.lang.OutOfMemoryError: unable to create new native thread?

看起来你用Fork/Join框架做字符串并行搜索时踩了线程创建过多的坑,结合你给出的代码片段和Fork/Join的工作逻辑,主要原因大概率出在任务拆分的逻辑缺陷或者对Fork/Join线程池的错误使用上,具体分析如下:

1. 拆分逻辑没有收敛,导致无限递归生成任务

你的代码里有个findMiddle方法用来找空格拆分点,避免拆分单词,但如果这个方法没有正确返回有效的拆分位置(比如返回的middle等于lo或者hi),就会导致子任务的区间和原任务完全一样,触发无限递归拆分——每次拆分都会生成两个新的ParallelSearchComment任务,而这些任务又会继续拆分,瞬间生成海量任务对象,不仅耗尽堆内存,还会让ForkJoinPool不断尝试处理这些任务,最终超出系统的线程创建上限。

举个例子,如果你的findMiddle实现不完整,比如只算了中间位置但没调整到空格处:

private int findMiddle(String text){
    int middle = lo + ((hi - lo)/2);
    // 没有检查这个位置是不是空格,也没调整到最近的空格
    return middle;
}

这种情况下,拆分点可能在单词中间,你如果继续拆分,就可能陷入无法终止的递归循环,任务越变越多。

另外,如果你的阈值T设置得太小(比如比单个单词的长度还小),也会生成远超预期的子任务——虽然ForkJoinPool会复用线程,但过多的任务对象会占用大量内存,同时线程池如果遇到任务积压,可能会临时扩容线程数,最终触发线程创建上限。

2. 没有用ForkJoinPool管理任务,手动创建了大量线程

Fork/Join框架的核心是线程复用,它通过ForkJoinPool来管理固定数量的线程(默认是CPU核心数),所有子任务都会提交到这个池子里复用线程。但如果你没有通过ForkJoinPool执行根任务,而是直接调用task.fork()或者错误地手动启动线程(比如调用start()),那每个子任务都会创建一个新的原生线程,很快就会达到系统的线程数上限,抛出内存错误。

正确的打开方式应该是这样的:

// 创建ForkJoinPool,默认用CPU核心数作为并行度
ForkJoinPool pool = new ForkJoinPool();
// 初始化根任务
ParallelSearchComment rootTask = new ParallelSearchComment(yourComment, 0, yourComment.length(), yourQuery, yourThreshold);
// 提交任务并获取结果
Integer result = pool.invoke(rootTask);

3. 任务中存在阻塞操作,导致线程池无限扩容

如果你的compute方法里有阻塞操作(比如IO等待、锁竞争),ForkJoinPool会认为当前线程无法处理任务,会创建新的线程来补充线程池。如果阻塞操作太多,线程数量会持续增长,直到超过系统允许的最大线程数,触发错误。

给你的修复建议

  • 完善拆分逻辑,确保递归能终止:让findMiddle正确找到空格拆分点,如果找不到有效拆分点,就直接处理当前区间,不要继续拆分。比如:
    @Override
    protected Integer compute() {
        // 区间长度小于等于阈值,直接搜索,终止递归
        if (hi - lo <= T) {
            return com.substring(lo, hi).contains(query) ? 1 : 0;
        }
        
        int middle = findMiddle(com);
        // 如果找不到有效拆分点,直接处理当前区间
        if (middle == lo || middle == hi) {
            return com.substring(lo, hi).contains(query) ? 1 : 0;
        }
        
        // 创建子任务
        ParallelSearchComment leftTask = new ParallelSearchComment(com, lo, middle, query, T);
        ParallelSearchComment rightTask = new ParallelSearchComment(com, middle, hi, query, T);
        
        // 异步执行左任务,当前线程执行右任务(工作窃取的最佳实践)
        leftTask.fork();
        int rightResult = rightTask.compute();
        int leftResult = leftTask.join();
        
        // 合并结果
        return leftResult + rightResult;
    }
    
    private int findMiddle(String text) {
        int middle = lo + ((hi - lo) / 2);
        
        // 从中间位置向左找最近的空格
        while (middle > lo && !Character.isWhitespace(text.charAt(middle))) {
            middle--;
        }
        
        // 如果左边没找到,向右找
        if (middle == lo) {
            middle = lo + ((hi - lo) / 2);
            while (middle < hi && !Character.isWhitespace(text.charAt(middle))) {
                middle++;
            }
        }
        
        // 还是没找到的话,返回hi,不拆分
        return middle == lo ? hi : middle;
    }
    
  • 合理设置阈值:把T设置为一个合理的值,比如单个单词平均长度的2-3倍,避免生成过多子任务。
  • 必须通过ForkJoinPool执行任务:永远不要手动创建线程,依赖ForkJoinPool的线程复用机制来控制线程数量,避免超出系统限制。

内容的提问来源于stack exchange,提问作者Robz de la Puenta

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:46:22