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
相关产品推荐
相关产品推荐

