Java多线程结合POI同时读写同一Excel(HSSF/XSSF)工作表的最优方案?
好的,针对你这个Excel多线程处理的需求,得先明确一个关键前提:Apache POI的Workbook、Sheet、Row这些核心对象都不是线程安全的——直接让多个线程操作同一个Sheet实例大概率会导致数据混乱、抛出异常,所以不能直接在多线程里读写Excel对象。
最优的方案应该是「数据读取→多线程并行处理数据→单线程统一写入」的流程,把CPU密集的逻辑处理部分并行化,而IO/Excel操作部分保持单线程,既保证安全又提升效率。下面是具体的实现步骤和代码示例:
1. 定义数据实体类
先封装每条记录的信息,包含行号(方便后续写入对应位置)、Name、Age和处理后的Remarks:
public class PersonRecord { private int rowIndex; // POI中行索引从0开始,若表头是第1行,数据行从索引1开始 private String name; private int age; private String remarks; // 生成构造函数、getter、setter方法 public PersonRecord(int rowIndex, String name, int age) { this.rowIndex = rowIndex; this.name = name; this.age = age; } // getter和setter省略,按需生成 }
2. 单线程读取Excel数据
先把所有需要处理的Name和Age数据读取到内存中,避免多线程操作POI对象:
import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class ExcelMultiThreadProcessor { private static final String EXCEL_PATH = "File.xlsx"; // 假设列索引:Name在第1列(0)、Age在第2列(1)、Remarks在第3列(2) private static final int NAME_COL = 0; private static final int AGE_COL = 1; private static final int REMARKS_COL = 2; // 单线程读取Excel数据 private static List<PersonRecord> readExcelData() throws IOException { List<PersonRecord> records = new ArrayList<>(); try (FileInputStream fis = new FileInputStream(EXCEL_PATH); Workbook workbook = new XSSFWorkbook(fis)) { Sheet sheet = workbook.getSheetAt(0); // 取第一个工作表 // 跳过表头,从第2行开始读取(索引1) for (int i = 1; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); if (row == null) continue; Cell nameCell = row.getCell(NAME_COL); Cell ageCell = row.getCell(AGE_COL); String name = nameCell != null ? nameCell.getStringCellValue() : ""; int age = ageCell != null ? (int) ageCell.getNumericCellValue() : 0; records.add(new PersonRecord(i, name, age)); } } return records; }
3. 编写并行处理的任务
把数据处理逻辑封装成Runnable任务,让多线程只处理内存中的数据,不碰POI对象:
// 模拟你的业务处理逻辑,替换成实际代码 private static String generateRemarks(String name, int age) { // 示例逻辑:根据姓名和年龄生成备注 return age >= 18 ? name + "是成年人" : name + "是未成年人"; } // 处理子数据集的任务类 static class RecordProcessTask implements Runnable { private final List<PersonRecord> subRecords; public RecordProcessTask(List<PersonRecord> subRecords) { this.subRecords = subRecords; } @Override public void run() { for (PersonRecord record : subRecords) { String remarks = generateRemarks(record.getName(), record.getAge()); record.setRemarks(remarks); } } }
4. 主流程:拆分数据+并行处理+写入结果
public static void main(String[] args) throws IOException, InterruptedException { // 1. 单线程读取所有数据 List<PersonRecord> allRecords = readExcelData(); int totalCount = allRecords.size(); // 2. 拆分数据为两组:前500条和剩余部分(兼容非1000条的情况) int splitPoint = Math.min(500, totalCount); List<PersonRecord> group1 = allRecords.subList(0, splitPoint); List<PersonRecord> group2 = allRecords.subList(splitPoint, totalCount); // 3. 启动两个线程并行处理 Thread thread1 = new Thread(new RecordProcessTask(group1)); Thread thread2 = new Thread(new RecordProcessTask(group2)); thread1.start(); thread2.start(); // 等待两个线程处理完成 thread1.join(); thread2.join(); // 4. 单线程写入处理后的Remarks列 try (FileInputStream fis = new FileInputStream(EXCEL_PATH); Workbook workbook = new XSSFWorkbook(fis)) { Sheet sheet = workbook.getSheetAt(0); for (PersonRecord record : allRecords) { Row row = sheet.getRow(record.getRowIndex()); if (row == null) { row = sheet.createRow(record.getRowIndex()); } Cell remarksCell = row.createCell(REMARKS_COL); remarksCell.setCellValue(record.getRemarks()); } // 写入到新文件(避免覆盖原文件,验证后可替换) try (FileOutputStream fos = new FileOutputStream("Processed_File.xlsx")) { workbook.write(fos); } System.out.println("Excel处理完成!"); } } }
关键注意事项
- POI线程安全问题:绝对不要在多线程中共享Workbook、Sheet、Row实例,我们的方案中读写Excel都是单线程,多线程只处理内存数据,完全规避了并发风险。
- 数据边界处理:用
Math.min处理拆分点,避免记录数不足1000时出现索引越界。 - 文件写入安全:建议写入新文件,验证无误后再替换原文件,防止数据丢失。
- 线程池扩展:如果后续需要更多线程,推荐使用
ExecutorService(比如Executors.newFixedThreadPool(2)),比手动创建线程更易管理。
内容的提问来源于stack exchange,提问作者Arun Patil
相关产品推荐
相关产品推荐

