为何代码创建过多线程引发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

