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

Java多线程问题:CompletableFuture线程输出重复及空指针异常

大型机字节文件多线程处理问题排查与解决

问题背景

我正在处理大型机字节文件,流程是逐行读取记录,从中提取数据生成JSONObject,攒够1000个后组成JSONArrayPOST到Web服务器,循环直到处理完所有记录。为优化效率,我尝试给每条记录分配单独线程生成JSONObject,待所有线程完成后再组装成JSONArray发送,但遇到两个问题:

  • 部分线程处理了不属于自己的记录,导致输出重复
  • 出现无法排查的NullPointerException
    此前用FutureTask也遇到相同问题,需要解决思路。

简化复现代码

// 简化的多线程处理代码示例
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    private static List<String> records = new ArrayList<>();
    private static List<JSONObject> jsonObjects = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        // 模拟读取大型机字节文件记录(简化为文本读取)
        BufferedReader reader = new BufferedReader(new FileReader("mainframe_records.txt"));
        String line;
        while ((line = reader.readLine()) != null) {
            records.add(line);
        }
        reader.close();

        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (String record : records) {
            executor.submit(() -> {
                // 模拟从记录提取数据生成JSONObject
                JSONObject json = new JSONObject();
                json.put("data", record);
                jsonObjects.add(json);
            });
        }
        executor.shutdown();
        while (!executor.isTerminated()) {}

        // 组装成JSONArray并发送(简化输出)
        System.out.println("生成的JSONObject数量:" + jsonObjects.size());
    }
}

错误输出示例

生成的JSONObject数量:1200 // 实际记录只有1000条,出现重复
Exception in thread "pool-1-thread-3" java.lang.NullPointerException
    at Main.lambda$main$0(Main.java:23)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

问题分析与解决

1. 线程安全问题导致重复/数据混乱

你使用的ArrayList(records和jsonObjects)不是线程安全集合:

  • 多线程同时调用jsonObjects.add(json)时,会触发竞态条件,导致元素重复、丢失或索引错乱
  • 若实际场景中存在边读取记录边提交线程任务的情况,records的非线程安全也会引发数据异常

解决方法:

  • 替换为线程安全集合,或对集合操作加同步锁:
    // 使用线程安全集合
    private static List<JSONObject> jsonObjects = Collections.synchronizedList(new ArrayList<>());
    
  • 改用invokeAll()批量提交任务,通过Future获取每个线程的返回结果,彻底避免共享集合的线程安全问题:
    List<Callable<JSONObject>> tasks = new ArrayList<>();
    for (String record : records) {
        String finalRecord = record; // 避免lambda变量捕获引用的问题
        tasks.add(() -> {
            JSONObject json = new JSONObject();
            json.put("data", finalRecord);
            return json;
        });
    }
    List<Future<JSONObject>> futures = executor.invokeAll(tasks);
    for (Future<JSONObject> future : futures) {
        jsonObjects.add(future.get());
    }
    

2. NullPointerException排查方向

空指针通常由以下场景引发:

  • lambda变量捕获问题:循环中直接使用record变量时,lambda捕获的是变量引用,循环迭代后record指向的对象会变化,线程可能拿到最后一条记录或null。解决:在循环内创建局部变量finalRecord复制当前记录,确保每个lambda捕获的是当前循环的实例。
  • 空记录未校验:读取文件时可能出现null或空记录,处理前需加非空判断:
    if (record == null || record.trim().isEmpty()) {
        return null; // 或直接跳过该记录
    }
    
  • 字段提取空值:从记录提取数据时,若字段本身为null,部分JSON库的put操作可能触发空指针,需提前校验字段值。

3. 效率优化建议

  • 不要给每条记录单独分配线程,线程创建和切换开销过高,建议按批量(比如每100条记录一个任务)提交,平衡并行度与性能损耗。
  • 不用等待所有线程完成再发送,可在任务执行过程中统计完成的JSONObject数量,达到1000条阈值就组装发送,提升整体吞吐量。

内容的提问来源于stack exchange,提问作者Richard62

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 00:25:19